from __future__ import annotations import hashlib import json import os import tempfile from pathlib import Path from typing import Any def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def atomic_write_json(path: Path, data: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(data, handle, indent=2, sort_keys=True) handle.write("\n") Path(tmp_name).replace(path) except Exception: Path(tmp_name).unlink(missing_ok=True) raise def read_json(path: Path, default: Any) -> Any: if not path.exists(): return default with path.open("r", encoding="utf-8") as handle: return json.load(handle) def ensure_relative(path: str) -> Path: if "\\" in path: raise ValueError(f"unsafe relative path: {path}") rel = Path(path) if rel.is_absolute() or ".." in rel.parts or any(part.endswith(":") for part in rel.parts): raise ValueError(f"unsafe relative path: {path}") return rel def ensure_inside(root: Path, target: Path) -> Path: root_resolved = root.resolve() target_resolved = target.resolve(strict=False) try: target_resolved.relative_to(root_resolved) except ValueError as exc: raise ValueError(f"target escapes instance root: {target}") from exc return target_resolved