diff --git a/scripts/region_audit.py b/scripts/region_audit.py index ce67ce6d..2eac6da9 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -19,7 +19,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import region -from common import load_emulator_profiles, load_database, parse_md5_list +from common import load_database, load_emulator_profiles, resolve_local_file # No-Intro filename tokens: full English territory names. NOINTRO = { @@ -100,24 +100,21 @@ def catalog_regions(db: dict) -> dict[str, tuple[set[str], str]]: return out -def resolve_sha1(file_entry: dict, db: dict) -> str | None: - """Resolve a profile file entry to a repo SHA1, or None when ambiguous.""" - files = db["files"] - indexes = db["indexes"] - raw_sha1 = file_entry.get("sha1") - declared = raw_sha1 if isinstance(raw_sha1, list) else [raw_sha1] - sha1_hits = [str(value).lower() for value in declared if value] - sha1_hits = [value for value in sha1_hits if value in files] - if len(sha1_hits) == 1: - return sha1_hits[0] - for md5 in parse_md5_list(file_entry.get("md5")): - hit = indexes["by_md5"].get(md5) - if hit: - return hit - hits = indexes["by_name"].get(file_entry.get("name", ""), []) - if isinstance(hits, str): - hits = [hits] - return hits[0] if len(hits) == 1 else None +def resolve_sha1(file_entry: dict, db: dict, by_path: dict | None = None) -> str | None: + """The repo SHA1 a profile entry resolves to, by the pack's own resolver. + + A private resolver read sha1, then md5, then a lone name: entries that + identify their file by path (Dolphin's three IPL.bin), by crc32 + (geargrafx) or by sha256 (mednafen) were never checked at all. + """ + if by_path is None: + by_path = {entry.get("path", ""): sha1 for sha1, entry in db["files"].items()} + path, status = resolve_local_file( + file_entry, db, dest_hint=file_entry.get("path") or "" + ) + if not path or status in ("not_found", "hash_mismatch"): + return None + return by_path.get(path) def build_report(profiles: dict, db: dict) -> dict: @@ -130,6 +127,7 @@ def build_report(profiles: dict, db: dict) -> dict: for slugs, _name in catalog.values(): vocabulary.update(slugs) + by_path = {entry.get("path", ""): sha1 for sha1, entry in db["files"].items()} for emu_name, profile in sorted(profiles.items()): if profile.get("type") in ("launcher", "alias"): continue @@ -137,7 +135,7 @@ def build_report(profiles: dict, db: dict) -> dict: if not isinstance(f, dict) or not f.get("region"): continue declared = region.normalize_declared(f["region"]) - sha1 = resolve_sha1(f, db) + sha1 = resolve_sha1(dict(f, source_profile=emu_name), db, by_path) if not sha1 or sha1 not in catalog: unchecked += 1 continue diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 80d33955..b0147100 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -939,17 +939,37 @@ class RegionRegressions(unittest.TestCase): ) self.assertNotEqual(north_america, europe) - def test_region_audit_accepts_list_valued_md5(self): - sha1 = "a" * 40 - md5 = "b" * 32 - db = { - "files": {sha1: {}}, - "indexes": {"by_md5": {md5: sha1}, "by_name": {}}, - } - self.assertEqual( - region_audit.resolve_sha1({"name": "bios.bin", "md5": [md5]}, db), - sha1, - ) + def test_region_audit_resolves_the_way_the_pack_does(self): + """A private resolver read sha1, md5 and a lone name: an entry + identified by its path (Dolphin's three IPL.bin) was never checked.""" + import hashlib # noqa: PLC0415 + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + previous = os.getcwd() + os.chdir(directory) + self.addCleanup(os.chdir, previous) + files, shas = {}, {} + for rel, payload in (("bios/GC/USA/IPL.bin", b"usa"), ("bios/GC/JAP/IPL.bin", b"jap")): + Path(rel).parent.mkdir(parents=True, exist_ok=True) + Path(rel).write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + shas[rel] = sha1 + files[sha1] = {"path": rel, "name": "IPL.bin", "size": len(payload), "sha1": sha1, + "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000001"} + from scripts import generate_db # noqa: PLC0415 + + db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + by_md5 = {e["md5"]: s for s, e in files.items()} + self.assertEqual( + region_audit.resolve_sha1({"name": "IPL.bin", "md5": [files[shas["bios/GC/USA/IPL.bin"]]["md5"]]}, db), + shas["bios/GC/USA/IPL.bin"], + ) + self.assertEqual( + region_audit.resolve_sha1({"name": "IPL.bin", "path": "GC/JAP/IPL.bin"}, db), + shas["bios/GC/JAP/IPL.bin"], + ) + self.assertTrue(by_md5) class ArchiveSecurityRegressions(unittest.TestCase):