From 14b32b38ba815f8274930583b135073ed8957898 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:08:13 +0200 Subject: [PATCH] fix: verify data-dir pack members by content --- scripts/packverify.py | 16 +++++++++++++++- tests/test_pack_member_identity.py | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/packverify.py b/scripts/packverify.py index 4a18bef7..c4eecdb0 100644 --- a/scripts/packverify.py +++ b/scripts/packverify.py @@ -19,6 +19,7 @@ from ziptools import check_inside_zip from nativemode import digest_algorithm from validation import settle_mismatch from nativemode import hash_mismatch_excludes_file +import functools import hashlib from common import filter_systems_by_target from common import load_emulator_profiles @@ -35,6 +36,16 @@ import io from collections.abc import Callable +@functools.lru_cache(maxsize=None) +def _file_sha1(path: str) -> str: + """SHA-1 of a cached data-directory file, read once per run.""" + digest = hashlib.sha1() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + def _members_are_held(data: bytes, by_md5: dict, held_inside: dict) -> bool: """Whether every member of an archive is a dump the collection holds.""" try: @@ -215,7 +226,10 @@ def verify_pack( for _dp in _cands: if not os.path.exists(_dp): continue - if os.path.getsize(_dp) == size: + # The content, not the size: a member written corrupt + # keeps its length, and a same-sized homonym from another + # cache is another file. + if os.path.getsize(_dp) == size and _file_sha1(_dp) == sha1: status = "verified_data" file_name = _bn break diff --git a/tests/test_pack_member_identity.py b/tests/test_pack_member_identity.py index b9f3f869..4302442e 100644 --- a/tests/test_pack_member_identity.py +++ b/tests/test_pack_member_identity.py @@ -43,6 +43,29 @@ class MembersMustBeKnown(unittest.TestCase): self.assertTrue(any("garbled.bin" in e for e in manifest["errors"])) +class DataMembersAreCheckedByContent(unittest.TestCase): + """A data-directory member was verified by name and size: bytes altered + while the pack was written kept their length and passed.""" + + def test_same_size_other_bytes_is_an_error(self): + good = b"A" * 64 + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp: + cache = Path(tmp) / "cache" + (cache / "Sys").mkdir(parents=True) + (cache / "Sys" / "font.bin").write_bytes(good) + registry = {"demo": {"local_cache": str(cache)}} + db = {"files": {}, "indexes": {"by_md5": {}, "by_name": {}}} + pack = Path(tmp) / "P_BIOS_Pack.zip" + with zipfile.ZipFile(pack, "w") as zf: + zf.writestr("system/Sys/font.bin", good) + zf.writestr("system/Sys/other/font.bin", b"B" * 64) + ok, manifest = verify_pack(str(pack), db, registry) + statuses = {f["path"]: f["status"] for f in manifest["files"]} + self.assertEqual(statuses["system/Sys/font.bin"], "verified_data") + self.assertNotEqual(statuses["system/Sys/other/font.bin"], "verified_data") + self.assertFalse(ok) + + class SchemaAcceptsEveryStatus(unittest.TestCase): """verified_members reached every pack manifest and the schema refused it."""