aeat.core.paths module

Shared path normalization and containment helpers.

Centralises the small set of Path primitives that every other aeat module needs: resolving repo-relative paths against PROJECT_ROOT via resolve_project_path(), normalising user-provided settings with normalize_project_relative_path(), and safely resolving caller-provided sub-paths under a fixed root without allowing path-traversal escapes.

The containment helpers (resolve_relative_subpath() and resolve_record_json_path()) refuse backslashes, parent references, absolute components, and any resolved path that escapes the owning root. They raise CoreValidationError and are the load-bearing defence against caller-controlled identifier injection on the on-disk store paths.

These helpers validate and compose paths only. They do not read, write, create, or secure files; persistence adapters that need registered storage errors wrap this module in their own typed containment layer.

is_windows_long_path_error(), windows_long_paths_enabled(), and windows_storage_root_long_path_margin() are the Windows MAX_PATH (260-character) hardening surface: classifying an OSError that legacy Windows raises once a resolved path exceeds the limit, reading the machine-wide long-path opt-in, and computing whether a candidate storage root leaves enough headroom for the deepest object path the bucket / outbound storage layout can produce.

PROJECT_ROOT

Absolute filesystem path to the repository root.

Used to anchor repo-relative defaults. It is not the process cwd and not a runtime storage root selected from settings.

WINDOWS_MAX_PATH: int

Legacy Windows CreateFileW path-length ceiling in UTF-16 code units. Windows 10 1607+ can lift this per-application via the LongPathsEnabled registry value combined with a longPathAware application manifest; a workstation that has neither still enforces this ceiling.

WINDOWS_WORST_CASE_OBJECT_PATH_SUFFIX_LENGTH: int

Worst-case path suffix (leading separator through file extension) that the bucket-directory layout can append below a configured storage root: \buckets\<uuid-36>\blobs\<hmac-8>--<label-64>.meta.json. Mirrors aeat.adapters.persistence.storage._namespace_registry.BUCKETS_DIRNAME / BUCKET_BLOBS_DIRNAME and the outbound LocalFileSystemProvider filename shape (<hmac_prefix_8>--<label>.meta.json, label capped at 64 chars). Kept as a literal here (not imported) because this module sits below the persistence and outbound-storage layers in the dependency graph; the two call sites that use this constant assert their real deepest-suffix shapes against it in tests.

is_windows_long_path_error(exc)[source]

Return whether exc is a Windows path-length-ceiling failure.

Classifies WinError 3 (ERROR_PATH_NOT_FOUND) and WinError 206 (ERROR_FILENAME_EXCED_RANGE) — the two concrete Windows API error codes a legacy (non long-path-aware) workstation raises once a resolved path walks past WINDOWS_MAX_PATH. Always False on non-Windows platforms and for any other OSError, so callers can unconditionally probe every caught OSError without a platform guard of their own.

Parameters:

exc (OSError) – The caught OSError (or subclass, e.g. FileNotFoundError) to classify.

Return type:

bool

Returns:

True when exc.winerror names a known long-path failure.

windows_long_paths_enabled()[source]

Report the machine-wide Windows long-path opt-in, if determinable.

Reads HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem \LongPathsEnabled — the registry value Windows 10 1607+ consults to lift WINDOWS_MAX_PATH for manifest-declared long-path-aware applications (this CLI is built with a longPathAware manifest via its packaging).

Return type:

bool | None

Returns:

True when the value is present and non-zero, False when present and zero (or when the platform is Windows but the value is absent — the pre-1607 / not-yet-opted-in default), and None on a non-Windows platform where the concept does not apply, or when the registry cannot be read at all (a probe is best-effort; it never raises).

windows_storage_root_long_path_margin(root)[source]

Return the headroom, in characters, before an object write risks MAX_PATH.

Computes WINDOWS_MAX_PATH - len(str(root.resolve())) - WINDOWS_WORST_CASE_OBJECT_PATH_SUFFIX_LENGTH. A positive result is the number of characters of slack remaining; zero or negative means the deepest object the bucket / outbound-storage layout can write already meets or exceeds the legacy MAX_PATH ceiling from this root. Platform-independent by design: the margin is informative on every OS, but only Windows without the long-path opt-in enforces the ceiling it measures against.

Parameters:

root (Path) – The candidate storage root (aeat_local_storage_root or an outbound-storage provider root).

Return type:

int

Returns:

The signed character margin described above.

resolve_project_path(value)[source]

Resolve a repo-relative path against PROJECT_ROOT.

Absolute paths are returned as absolute resolved paths. Relative paths are interpreted as repository-relative, not cwd-relative, which keeps config defaults stable regardless of where the CLI process starts. This helper is not a containment guard: callers that accept subpaths under an owning root should use resolve_relative_subpath().

Parameters:

value (str | Path) – An absolute or repo-relative path; user-style ~ references are expanded.

Return type:

Path

Returns:

The fully resolved absolute pathlib.Path.

normalize_project_relative_path(value)[source]

Normalise an optional path setting to an absolute repo-rooted path.

Used by settings validators for optional path fields. It preserves None and delegates path semantics to resolve_project_path(); it does not verify that the resulting path exists.

Parameters:

value (Path | None) – Optional configured path, or None.

Return type:

Path | None

Returns:

None when value is None; otherwise the resolved absolute path produced by resolve_project_path().

resolve_relative_subpath(root, relative_path, *, context)[source]

Resolve relative_path under root and enforce containment.

The returned path is resolved and proven to stay under root after normalization. The helper performs no filesystem mutation and does not assert that the target exists; callers decide whether a missing file is valid for their operation.

Parameters:
  • root (Path) – The fixed parent directory that the result must live under.

  • relative_path (str) – A POSIX-style sub-path supplied by an untrusted-ish caller. Backslashes, absolute components, empty parts, . and .. parts are all rejected.

  • context (str) – Short human-readable label used in raised error messages so the caller can attribute the failure.

Return type:

Path

Returns:

The resolved absolute path inside root.

Raises:

CoreValidationError – When relative_path is malformed or when the resolved path escapes root.

resolve_record_json_path(root, record_id, *, context)[source]

Resolve a file-backed record id to <root>/<record_id>.json safely.

The token allow-list prevents path separators, dot components, and overlong filename material from becoming a filesystem path. It is not domain-id validation: callers still own UUID/modelo/CSV/hash shape checks before they choose record_id.

Parameters:
  • root (Path) – The directory that owns the JSON sidecar files.

  • record_id (str) – A simple filename token. Must match a strict [A-Za-z0-9][A-Za-z0-9._-]{0,127} shape so the resolved path cannot escape root via path separators or parent references.

  • context (str) – Short human-readable label used in raised error messages.

Return type:

Path

Returns:

The resolved absolute path of the JSON sidecar.

Raises:

CoreValidationError – When record_id is not a simple filename token or the resolved path escapes root.

file_stat_fingerprint(path)[source]

Return a cache-key fingerprint triple for a single file.

The triple (name, size_bytes, mtime_ns) is a stable, low-cost proxy for file identity used by file-backed loader caches. Any in-place modification that changes size or mtime invalidates the cache without requiring a full content hash.

This is not an integrity hash or evidence digest. For byte-level verification use aeat.core.hashing.hash_file() or aeat.core.hashing.sha256_file().

Parameters:

path (Path) – The file to fingerprint. Must be an existing, stat-able path.

Return type:

tuple[str, int, int]

Returns:

(path.name, stat.st_size, stat.st_mtime_ns). path.stat() propagates OSError when the file is unreadable or disappears.