Source code for aeat.adapters.persistence.storage._path_safety

"""Typed path-containment helpers for the persistence substrate.

The wider project ships :func:`core.paths.resolve_relative_subpath` and
:func:`core.paths.resolve_record_json_path` which raise plain
:class:`ValueError` on traversal violations. New persistence code uses
the typed wrappers in this module instead so:

- the failure carries the registered ``PathContainmentError`` code
  (``INTEGRITY_STORAGE_PATH_CONTAINMENT``) and lands in the standard
  CLI error envelope;
- callers can write narrow ``except PathContainmentError`` clauses
  rather than broad ``except ValueError``;
- :class:`PathContainmentError` still inherits from :class:`ValueError`
  so callers that handle Python path-shape errors remain correct.
"""

from __future__ import annotations

from pathlib import Path

from ....core.paths import resolve_record_json_path, resolve_relative_subpath
from .errors import PathContainmentError


def _containment_error(message: str, *, context: str, violation: str) -> PathContainmentError:
    return PathContainmentError(
        message,
        context={
            "path_context": context,
            "violation": violation,
        },
    )


[docs] def safe_subpath(root: Path, relative_path: str, *, context: str) -> Path: """Resolve ``relative_path`` under ``root`` and enforce containment. Wraps :func:`core.paths.resolve_relative_subpath`. Any :class:`ValueError` raised by the wrapped helper is re-raised as a localized :class:`PathContainmentError` with the same diagnostic ``args`` message and ``__cause__``. Args: root: Configured root directory the path must stay under. relative_path: Forward-slash-separated relative path. context: Stable label embedded in the error message; used for log diagnostics. Returns: The resolved absolute :class:`Path` known to live under ``root``. Raises: PathContainmentError: On any traversal or shape violation. """ try: return resolve_relative_subpath(root, relative_path, context=context) except ValueError as exc: raise _containment_error( str(exc), context=context, violation="relative_subpath", ) from exc
[docs] def safe_record_path(root: Path, record_id: str, *, context: str) -> Path: """Resolve a record-id-keyed JSON file under ``root``. Wraps :func:`core.paths.resolve_record_json_path` and re-raises its :class:`ValueError` as a localized :class:`PathContainmentError`. Args: root: Configured root directory. record_id: Simple filename token; the helper enforces a strict allow-list of characters so traversal sequences cannot slip through. context: Stable label embedded in the error message. Returns: The resolved absolute :class:`Path` of ``<root>/<record_id>.json``. Raises: PathContainmentError: When ``record_id`` is not a safe token or when the resolved path escapes ``root``. """ try: return resolve_record_json_path(root, record_id, context=context) except ValueError as exc: raise _containment_error( str(exc), context=context, violation="record_json_path", ) from exc
[docs] def safe_repository_id(token: str, *, context: str) -> str: """Reject repository-id tokens that would compose into an unsafe filename. Repositories store records as ``<store_dir>/<token>.envelope.json``. A token containing a path separator, a dot-prefix, or one of the relative-path tokens (``"."`` / ``".."``) would either escape the store dir or collide with a hidden file. This helper is the early-rejection layer at the public-method boundary; the substrate's :func:`safe_record_path` enforces containment downstream. The validation is intentionally minimal — non-empty, no path separator, no dot-token. It does not claim knowledge of any domain-specific id alphabet (UUIDs, AEAT CSVs, modelo numerics, etc.) so a single helper covers every governance repository. Args: token: The free-string id supplied by the repository caller. context: Stable label (``"submission_id"`` / ``"draft_id"`` / etc.) embedded in the error message. Lets the failure message remain byte-identical to the per-domain validators it replaces. Returns: ``token`` unchanged. Returning the validated value lets the helper appear inline (``safe_repository_id(record_id, ...)`` as both check and pass-through). Raises: PathContainmentError: When ``token`` is empty, contains a path separator, is the bare ``.`` / ``..`` token, or starts with a dot. """ if not token: raise _containment_error( f"{context} must be non-empty", context=context, violation="empty_repository_id", ) if "/" in token or "\\" in token: raise _containment_error( f"{context} must not contain path separators", context=context, violation="repository_id_separator", ) if token in {".", ".."} or token.startswith("."): raise _containment_error( f"{context} must not be a relative-path token", context=context, violation="repository_id_dot_token", ) return token
__all__ = [ "safe_record_path", "safe_repository_id", "safe_subpath", ]