diff --git a/scripts/generate_db.py b/scripts/generate_db.py index a3afdc1f..53d4d48c 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -437,6 +437,15 @@ def _collect_all_aliases(files: dict) -> dict: sha1 = file_entry.get("sha1", "") md5 = file_entry.get("md5", "") + # A zipped_file entry verifies a ROM INSIDE the + # archive, so its hash describes the member and not + # the file the name designates. Registering the name + # against that hash made d2fdc.zip an alias of a + # loose 256-byte state-machine-16.rom, which three + # platforms were then served in place of the archive. + if file_entry.get("zipped_file"): + continue + matched = None if sha1 and sha1 in files: matched = sha1 diff --git a/tests/test_shipped_content.py b/tests/test_shipped_content.py index dd7af502..118a0c3e 100644 --- a/tests/test_shipped_content.py +++ b/tests/test_shipped_content.py @@ -17,6 +17,8 @@ from __future__ import annotations import hashlib import json +import os +import re import sys import unittest import zipfile @@ -182,5 +184,39 @@ class ProfileContradictionsAreKnown(unittest.TestCase): ) +class ArchiveNamesDesignateArchives(unittest.TestCase): + """A name ending in .zip must not designate a loose file. + + The alias collector matched a platform entry's name against the SHA1 its + md5 pointed at without looking at zipped_file, and a zipped_file md5 is + the member's, not the container's. d2fdc.zip became an alias of a loose + 256-byte state-machine-16.rom, and three platforms were served that ROM + where they had asked for the archive. + """ + + def test_no_archive_name_resolves_to_a_loose_file(self): + database = ROOT / "database.json" + if not database.is_file(): + self.skipTest("database.json not built") + db = common.load_database(str(database)) + loose = [] + for name, ids in db["indexes"]["by_name"].items(): + if not name.lower().endswith(".zip"): + continue + for sha1 in ids: + path = db["files"].get(sha1, {}).get("path", "") + base = os.path.basename(path).lower() + # .zip, or the repo's variant form .zip. + if not re.match(r".*\.zip(\.[0-9a-f]{6,})?$", base): + loose.append(f"{name} -> {path}") + self.assertLessEqual( + len(loose), + 1, + "an archive name designates a loose file; the only accepted case is " + "ngpc.zip, whose md5 RetroDECK itself declares against the loose " + f"Neo Geo Pocket Color BIOS:\n " + "\n ".join(sorted(loose)), + ) + + if __name__ == "__main__": unittest.main()