From fd6ae1f5c9ad83a9f8f70277e445f6214f38ae74 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:33:10 +0200 Subject: [PATCH] fix: strip only a hash suffix off variant names slots dropped the last extension of any .variants/ name, so a variant stored under its plain name (63 of them, aes.zip among them) was not the family of its primary and a platform declaring both at one path would read as a collision. The database already stripped an eight-hex suffix only; both now read one function. --- scripts/common.py | 12 ++++++++++++ scripts/generate_db.py | 15 ++++----------- scripts/slots.py | 3 ++- tests/test_slots.py | 26 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index 98839cb6..28e0201c 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -771,6 +771,18 @@ def _resolve_agnostic( return None +_VARIANT_SUFFIX = re.compile(r"\.[0-9a-f]{8}$") + + +def variant_base_name(name: str) -> str: + """The name a .variants/ copy stands in for: naomi2.zip.da79eca4 -> naomi2.zip. + + Only an eight-hex-digit suffix is a variant tag. A variant stored under + its plain name (.variants/aes.zip) already is that name. + """ + return _VARIANT_SUFFIX.sub("", name) + + def _affinity_tokens(text: str) -> set[str]: return { token diff --git a/scripts/generate_db.py b/scripts/generate_db.py index b5b1662b..2557c445 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -28,6 +28,7 @@ from common import ( load_provenance_snapshots, require_yaml, source_yaml_paths, + variant_base_name, write_if_changed, write_text_atomic, yaml_load, @@ -65,17 +66,9 @@ def should_skip(path: Path) -> bool: def _canonical_name(filepath: Path) -> str: """Get canonical filename, stripping .variants/ hash suffix.""" - name = filepath.name - if "/.variants/" in str(filepath) or "\\.variants\\" in str(filepath): - # naomi2.zip.da79eca4 -> naomi2.zip - parts = name.rsplit(".", 1) - if ( - len(parts) == 2 - and len(parts[1]) == 8 - and all(c in "0123456789abcdef" for c in parts[1]) - ): - return parts[0] - return name + if ".variants" in filepath.parts: + return variant_base_name(filepath.name) + return filepath.name def scan_bios_dir(bios_dir: Path, cache: dict, force: bool) -> tuple[dict, dict, dict]: diff --git a/scripts/slots.py b/scripts/slots.py index 800fc244..6bd6e2de 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -31,6 +31,7 @@ from common import ( resolve_local_file, resolve_platform_cores, runs_standalone, + variant_base_name, ) from packresolve import preferred_entries from validation import outside_gap_scope @@ -752,7 +753,7 @@ def _same_file_family(paths: list[str]) -> bool: if "/.variants/" in path: directory, _, name = path.rpartition("/") directory = directory[: -len("/.variants")] - name = name.rsplit(".", 1)[0] + name = variant_base_name(name) path = f"{directory}/{name}" canonical.add(path.casefold()) return len(canonical) == 1 diff --git a/tests/test_slots.py b/tests/test_slots.py index 0a5d9266..132eb4ce 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -459,6 +459,32 @@ class TestSelfContradictingDestinations(unittest.TestCase): ) ) + def test_a_variant_kept_under_its_plain_name_is_one_family(self): + # 63 variants carry no hash suffix, aes.zip among them. + self.assertTrue( + slots._same_file_family( + ["bios/SNK/Neo Geo/aes.zip", "bios/SNK/Neo Geo/.variants/aes.zip"] + ) + ) + + def test_only_a_hash_suffix_is_stripped(self): + self.assertFalse( + slots._same_file_family( + ["bios/M/C/rom.zip", "bios/M/C/.variants/rom.zip.zip"] + ) + ) + + def test_the_database_and_the_arbiter_read_one_rule(self): + import generate_db # noqa: PLC0415 + + variant = Path("bios/M/C/.variants/aes.zip") + self.assertEqual(generate_db._canonical_name(variant), "aes.zip") + for module in ("slots.py", "generate_db.py"): + source = (Path(__file__).resolve().parents[1] / "scripts" / module).read_text() + with self.subTest(module=module): + self.assertIn("variant_base_name(", source) + self.assertNotIn('rsplit(".", 1)[0]', source) + def test_two_machines_are_not_one_family(self): self.assertFalse( slots._same_file_family(