Source code for mafw.storage.logical_reference

#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Portable logical file references, independent of runtime storage roots."""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import PurePosixPath


[docs] @dataclass(frozen=True, slots=True) class LogicalFileReference: """Identify a file by storage volume name and a portable relative path. The serialized form is ``<volume>:<relative/path>``. Volume names contain only ASCII letters, digits, underscores, and hyphens. Paths use POSIX separators and cannot be absolute or contain ``..`` components. Redundant separators and ``.`` components are normalized away. The reference is lexical; it does not validate a configured volume or resolve a filesystem path. :param volume: Case-sensitive logical volume name. :param relative_path: POSIX-style path relative to that volume. .. versionadded:: 2.4 """ volume: str relative_path: PurePosixPath def __post_init__(self) -> None: """Validate and canonicalize both fields at construction time.""" if not isinstance(self.volume, str): raise TypeError('volume must be a string') if not re.fullmatch(r'[A-Za-z0-9_-]+', self.volume, flags=re.ASCII): raise ValueError(f'Invalid volume name {self.volume!r}; expected [A-Za-z0-9_-]+') if not isinstance(self.relative_path, (str, PurePosixPath)): raise TypeError('relative_path must be a POSIX path string or PurePosixPath') raw_path = str(self.relative_path) if not raw_path: raise ValueError('relative_path must not be empty') if '\\' in raw_path: raise ValueError("relative_path must use POSIX '/' separators; backslashes are not allowed") if raw_path.startswith('/'): raise ValueError('relative_path must be relative, not absolute or UNC') if re.match(r'^[A-Za-z]:', raw_path, flags=re.ASCII): raise ValueError('relative_path must not be a Windows drive path') if ':' in raw_path: raise ValueError('relative_path components must not contain colons') if ';' in raw_path: raise ValueError('relative_path components must not contain semicolons') raw_parts = raw_path.split('/') if '..' in raw_parts: raise ValueError('relative_path must not contain ".." traversal components') normalized = PurePosixPath(raw_path) if not normalized.parts or str(normalized) == '.': raise ValueError('relative_path must identify a file below the volume root') object.__setattr__(self, 'relative_path', normalized)
[docs] @classmethod def from_string(cls, value: str) -> LogicalFileReference: """Parse and validate a serialized logical file reference. :param value: String in ``<volume>:<relative/path>`` form. :returns: The validated logical reference. :raises TypeError: If ``value`` is not a string. :raises ValueError: If the serialized reference is malformed. .. versionadded:: 2.4 """ if not isinstance(value, str): raise TypeError('serialized logical reference must be a string') if ':' not in value: raise ValueError('logical reference must have the form <volume>:<relative/path>') volume, relative_path = value.split(':', maxsplit=1) return cls(volume=volume, relative_path=PurePosixPath(relative_path))
[docs] @classmethod def from_parameter_value(cls, value: object) -> LogicalFileReference: """Convert a serialized processor parameter value into this type. :param value: An existing reference or its serialized string. :returns: The typed logical file reference. :raises TypeError: If *value* is neither a reference nor a string. :raises ValueError: If the serialized reference is malformed. .. versionadded:: 2.4 Added the optional processor-parameter conversion hook. """ if isinstance(value, cls): return value if isinstance(value, str): return cls.from_string(value) raise TypeError(f'Cannot convert {type(value).__name__} to {cls.__name__} from a processor parameter value')
[docs] def to_string(self) -> str: """Return the canonical portable serialization. :returns: Canonical ``<volume>:<relative/path>`` representation. .. versionadded:: 2.4 """ return f'{self.volume}:{self.relative_path.as_posix()}'