From dce9c6e8f11acbaa2248b98753cc921e7dad269a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:21:27 +0200 Subject: [PATCH] fix: hold the MAME clone step to name rules --- scripts/common.py | 16 ++++++++++---- tests/test_audit_regressions.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index 3d84adb3..2520359e 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1335,12 +1335,20 @@ def resolve_local_file( if os.path.exists(path): return path, "zip_exact" - # MAME clone fallback: if a file was deduped, resolve via canonical - if _depth < 3 and not has_strong_hash: + # MAME clone fallback: if a file was deduped, resolve via canonical. The + # clone name is a name step like the others: an unsourceable entry skips + # it, and the size the code checks travels with it (dedup only merges + # identical bytes, so the canonical copy has the clone's size). + if _depth < 3 and not has_strong_hash and not unsourceable: clone_map = get_mame_clone_map(db.get("bios_dir")) canonical = clone_map.get(name) if canonical and canonical != name: - canonical_entry = {"name": canonical} + canonical_entry = { + field: file_entry[field] + for field in ("size", "min_size", "max_size", "validation") + if field in file_entry + } + canonical_entry["name"] = canonical result = resolve_local_file( canonical_entry, db, @@ -1349,7 +1357,7 @@ def resolve_local_file( _depth=_depth + 1, data_dir_registry=data_dir_registry, ) - if result[0]: + if result[0] and result[1] != "hash_mismatch": return result[0], "mame_clone" # Data directory fallback: scan data/ caches for matching filename diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index fcf0af22..3eba6746 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2936,3 +2936,42 @@ class ADataDirectoryCopyPassesTheNameChecks(unittest.TestCase): def test_another_adler32_is_a_contradiction(self): status = self.resolve(validation=["adler32"], known_hash_adler32="0xdeadbeef")[1] self.assertEqual(status, "hash_mismatch") + + +class AMameCloneNameIsANameStep(unittest.TestCase): + """The clone fallback relaunched resolution on the bare canonical name: + an unsourceable entry and a size the code checks were both served the + parent set.""" + + def setUp(self): + self.tmp = Path(tempfile.mkdtemp(dir=TMP_ROOT)) + self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, True)) + bios = self.tmp / "bios" / "Arcade" + bios.mkdir(parents=True) + parent = bios / "parent.zip" + parent.write_bytes(b"PARENTSET") + (self.tmp / "_mame_clones.json").write_text( + json.dumps({"parent.zip": {"clones": ["clone.zip"]}}) + ) + sha1 = hashlib.sha1(parent.read_bytes()).hexdigest() + self.db = { + "bios_dir": str(self.tmp / "bios"), + "files": {sha1: {"path": str(parent), "name": "parent.zip", + "sha1": sha1, "size": 9}}, + "indexes": {"by_name": {"parent.zip": [sha1]}, "by_md5": {}, + "by_path_suffix": {}, "by_crc32": {}}, + } + + def resolve(self, **fields): + return resolve_local_file({"name": "clone.zip", **fields}, self.db) + + def test_a_clone_resolves_to_its_parent(self): + self.assertEqual(self.resolve()[1], "mame_clone") + + def test_an_unsourceable_entry_is_not_served_the_parent(self): + self.assertEqual(self.resolve(unsourceable="not held"), (None, "not_found")) + + def test_a_size_the_parent_contradicts_is_not_served(self): + self.assertEqual( + self.resolve(size=5, validation=["size"]), (None, "not_found") + )