diff --git a/scripts/slots.py b/scripts/slots.py index f43f567c..d6bea225 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -21,7 +21,9 @@ from dataclasses import dataclass, field import nativemode from common import ( build_zip_contents_index, + check_inside_zip, load_data_dir_registry, + parse_md5_list, resolution_is_hash_exact, resolve_local_file, runs_standalone, @@ -415,10 +417,40 @@ def find_collisions( if local and local not in resolved: resolved.append(local) if len(resolved) > 1 and not _same_file_family(resolved): + # A file every declaration accepts settles the path: RetroDECK + # declares ATARIOSB.ROM twice, the second with an md5 list that + # also names the first one's file. + if any(_accepted_by_all(path, entries, db) for path in resolved): + continue collisions.append(Collision(destination=key, resolved=resolved)) return collisions +def _accepted_by_all(path: str, entries: list[dict], db: dict) -> bool: + """Whether every declaration's own hashes accept the file at path.""" + record = next( + (e for e in db.get("files", {}).values() if e.get("path") == path), None + ) + if record is None: + return False + for entry in entries: + if entry.get("zipped_file"): + declared = parse_md5_list(entry.get("md5")) + if declared and not any( + check_inside_zip(path, entry["zipped_file"], m) == "ok" for m in declared + ): + return False + continue + md5s = parse_md5_list(entry.get("md5")) + if md5s and str(record.get("md5", "")).lower() not in md5s: + return False + sha1 = entry.get("sha1") + sha1s = [sha1] if isinstance(sha1, str) else list(sha1 or []) + if sha1s and str(record.get("sha1", "")).lower() not in {h.lower() for h in sha1s}: + return False + return True + + def _same_file_family(paths: list[str]) -> bool: """Whether the paths are one file and its own pinned variants. diff --git a/tests/test_slots.py b/tests/test_slots.py index dedd8b89..a6bc7ba0 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -369,6 +369,17 @@ class TestSelfContradictingDestinations(unittest.TestCase): ) self.assertEqual(slots.find_collisions(config, REGIONS_DB), []) + def test_a_file_both_declarations_accept_settles_the_path(self): + """RetroDECK: ATARIOSB.ROM under one system with one md5, under + another with a list naming that same md5; the shipped file passes + both, so --strict has nothing to correct there.""" + config = self._config( + {"name": "IPL.bin", "destination": "disk.rom", "md5": "m" * 32}, + {"name": "IPL.bin", "destination": "disk.rom", + "md5": f"{'n' * 32},{'m' * 32}"}, + ) + self.assertEqual(slots.find_collisions(config, REGIONS_DB), []) + def test_a_primary_and_its_pinned_variant_are_one_family(self): self.assertTrue( slots._same_file_family(