diff --git a/scripts/validation.py b/scripts/validation.py index d4529bf5..481386f2 100644 --- a/scripts/validation.py +++ b/scripts/validation.py @@ -793,7 +793,12 @@ def digest_verdict( actual = hashes()[algorithm].lower() if algorithm != "md5": return actual in declared - if any(actual == d or (len(d) < 32 and actual.startswith(d)) for d in declared): + # Recalbox hashes a .zip by its members only (Bios.cpp:64-68), so the + # container's own md5 answers nothing there. + composite_only = file_entry.get("zip_digest") == "composite" + if not composite_only and any( + actual == d or (len(d) < 32 and actual.startswith(d)) for d in declared + ): return True member_digest = composite() return bool(member_digest) and member_digest.lower() in declared diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index d12efd68..34011ff7 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -1206,6 +1206,30 @@ class OneBaselineVerdict(unittest.TestCase): ("frontend_digest_exact", None), ) + def test_a_composite_only_archive_ignores_its_raw_md5(self): + """Recalbox hashes a .zip by its members (Bios.cpp:64-68), and the + pack check accepted the container's own md5 before the composite: + neocd.bin shipped as neocdz.zip passed while Recalbox marks it red.""" + import hashlib + + from scripts.common import md5_composite + from scripts.validation import digest_verdict + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + archive = self._archive(Path(directory), "rom.bin") + raw = hashlib.md5(Path(archive).read_bytes()).hexdigest() + members = md5_composite(archive) + for declared, accepted in ((raw, False), (members, True)): + entry = {"name": "dragon32.zip", "md5": declared, "zip_digest": "composite"} + with self.subTest(declared=declared): + self.assertIs( + digest_verdict(entry, 0, lambda: {"md5": raw}, "md5", "", + lambda: members), + accepted, + ) + plain = {"name": "dragon32.zip", "md5": raw} + self.assertTrue(digest_verdict(plain, 0, lambda: {"md5": raw}, "md5")) + def test_a_release_asset_keeps_its_answer(self): from scripts.slots import apply_override diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 99669ff0..e2414d26 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -322,6 +322,18 @@ class Md5CompositeDefinition(unittest.TestCase): self.assertIn("contents of every member", section) self.assertNotIn("inner file MD5s", section) + def test_composite_only_is_read_by_both_checks(self): + """The page said the direct MD5 is tried before the composite, which + Recalbox never does for a .zip, and the pack check did the same.""" + import common + import validation + + section = _section(_page("verification-modes.md"), "md5_composite (Recalbox ZIP verification)") + self.assertIn("zip_digest: composite", section) + self.assertNotIn("is tried before reporting a mismatch", section) + for function in (common.resolve_local_file, validation.digest_verdict): + self.assertIn('get("zip_digest") == "composite"', inspect.getsource(function)) + class ValidationIndexScope(unittest.TestCase): """The page said the index reads every profile. verify and the builder diff --git a/wiki/verification-modes.md b/wiki/verification-modes.md index 5b43d469..3990eda5 100644 --- a/wiki/verification-modes.md +++ b/wiki/verification-modes.md @@ -60,9 +60,16 @@ characters by prefix, and only for a file whose name the entry also names. Recalbox hashes a ZIP with `Zip::Md5Composite` (`es-core/src/utils/Zip.cpp:71-96` in recalbox-emulationstation at `f4cd0ae4`): one MD5 over the contents of every member, -read in filename order. The members' own MD5s are not involved. `verify.py` computes -the same value with `md5_composite()` (`scripts/hashing.py`). When a ZIP file's direct -MD5 does not match, the composite is tried before reporting a mismatch. +read in filename order. The members' own MD5s are not involved, and neither is the +MD5 of the ZIP file itself: `Bios::Scan` takes the composite for every `.zip` +(`es-app/src/bios/Bios.cpp:64-68`). `verify.py` computes the same value with +`md5_composite()` (`scripts/hashing.py`). + +The registry gives Recalbox `zip_digest: composite`. For a platform so marked, a +`.zip` entry is checked against its composite alone, by the resolver +(`resolve_local_file`) and by the pack check (`digest_verdict` in +`scripts/validation.py`). On other platforms the ZIP file's direct MD5 is tried first +and the composite after it. ### Multi-hash (Recalbox)