Paper2Agent_decoupleRpy / src /core /integrity.py
anne-voigt's picture
feat(uploads): ADR-0011 manual-upload safety gate (Now slice)
74d4e89 verified
Raw
History Blame
1.65 kB
"""Content-hash helpers for integrity / tamper verification.
A tiny, dependency-free module so the *same* hashing code backs two controls:
- **ADR-0011** (manual-upload safety gate) records ``sha256`` in an upload's
provenance at validation time.
- **ADR-0010** (dataset integrity on load) will reuse :func:`verify_sha256` to
refuse a tampered file at ``resolve_to_local_path`` time.
Streamed so a multi-GB h5ad never has to be read into memory at once.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
# 4 MiB — matches the download chunk size used in src/core/data_io.py.
_DEFAULT_CHUNK = 4 * 1024 * 1024
def compute_sha256(path: str | Path, chunk_size: int = _DEFAULT_CHUNK) -> str:
"""Return the hex SHA-256 of the file at ``path``, read in chunks.
Raises ``FileNotFoundError`` if the path does not exist (callers that stage
a file should have materialized it first).
"""
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(chunk_size), b""):
h.update(chunk)
return h.hexdigest()
def verify_sha256(path: str | Path, expected: str, chunk_size: int = _DEFAULT_CHUNK) -> bool:
"""Return True iff the file at ``path`` hashes to ``expected`` (case-insensitive).
A missing/empty ``expected`` returns False — an absent baseline is not a
pass. (ADR-0010 handles "no baseline recorded" as a distinct, explicit case
at its call site; this helper only answers "does it match".)
"""
if not expected:
return False
return compute_sha256(path, chunk_size).lower() == str(expected).strip().lower()