From 62bae63bbf72245a5356c510a247729d1502b54f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:05:14 +0200 Subject: [PATCH] fix: fail a pack member the collection cannot name --- scripts/packverify.py | 32 +++++++++++++++++++++ tests/test_pack_member_identity.py | 46 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/test_pack_member_identity.py diff --git a/scripts/packverify.py b/scripts/packverify.py index 35170763..c61bb57b 100644 --- a/scripts/packverify.py +++ b/scripts/packverify.py @@ -31,6 +31,25 @@ from packresolve import resolve_file from common import resolve_platform_cores from common import sanitize_pack_path import zipfile +def _members_are_held(data: bytes, by_md5: dict, db: dict) -> bool: + """Whether every member of an archive is a dump the collection holds.""" + import io + + held_inside = build_zip_contents_index(db) + try: + with zipfile.ZipFile(io.BytesIO(data)) as archive: + members = [i for i in archive.infolist() if not i.is_dir()] + if not members: + return False + for info in members: + digest = hashlib.md5(archive.read(info)).hexdigest() + if digest not in by_md5 and digest not in held_inside: + return False + except (zipfile.BadZipFile, OSError): + return False + return True + + def verify_pack( zip_path: str, db: dict, data_registry: dict | None = None ) -> tuple[bool, dict]: @@ -183,6 +202,19 @@ def verify_pack( except (zipfile.BadZipFile, OSError): continue + # An archive the builder assembled (a MAME clone set): every + # member must be a dump the collection holds, loose or inside a + # romset. + if status == "untracked" and name.endswith(".zip"): + if _members_are_held(zf.read(name), by_md5, db): + status = "verified_members" + file_name = os.path.basename(name) + + if status == "untracked": + # Bytes nothing recognises: written wrong, or a source that + # no longer matches. Counted and passed, it went unseen. + errors.append(f"{name}: content matches no collected file") + manifest["files"].append( { "path": name, diff --git a/tests/test_pack_member_identity.py b/tests/test_pack_member_identity.py new file mode 100644 index 00000000..e72a23b8 --- /dev/null +++ b/tests/test_pack_member_identity.py @@ -0,0 +1,46 @@ +"""Every pack member is bytes the collection recognises. + +A member whose sha1 and md5 matched nothing was counted `untracked` with +zero errors and the pack passed, so bytes altered while the ZIP was written +went unseen. Archives the builder assembles (MAME clone sets) are +recognised by their members. +""" + +from __future__ import annotations + +import hashlib +import io +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + + +class MembersMustBeKnown(unittest.TestCase): + def test_unknown_bytes_are_an_error_and_assembled_sets_pass(self): + from packverify import verify_pack + + known = b"known rom" * 10 + md5 = hashlib.md5(known).hexdigest() + inner = io.BytesIO() + with zipfile.ZipFile(inner, "w") as zf: + zf.writestr("rom.bin", known) + db = {"files": {}, "indexes": {"by_md5": {md5: "s"}, "by_name": {}}} + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp: + pack = Path(tmp) / "P_BIOS_Pack.zip" + with zipfile.ZipFile(pack, "w") as zf: + zf.writestr("clone.zip", inner.getvalue()) + zf.writestr("garbled.bin", b"no such dump") + ok, manifest = verify_pack(str(pack), db) + statuses = {f["path"]: f["status"] for f in manifest["files"]} + self.assertEqual(statuses["clone.zip"], "verified_members") + self.assertFalse(ok) + self.assertTrue(any("garbled.bin" in e for e in manifest["errors"])) + + +if __name__ == "__main__": + unittest.main()