From 1101c7640b082a5d15e5f0d08783dbef2c9c34f1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:36:12 +0200 Subject: [PATCH] fix: let a primary beat its variant across casing Only a .variants/ copy matched IPLROM.X1 exactly, so x1 got Recalbox's second md5 while iplrom.x1, the dump every platform pins, sat beside it. When every exact-case match is a variant, a primary in the same directory under its own name joins the candidates. Another machine's font.rom or an alias pointing at a different file does not. --- scripts/common.py | 29 +++++++++++++++++-- tests/test_casefold_cache.py | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index 28e0201c..ca90a94e 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1269,11 +1269,34 @@ def resolve_local_file( path = files_db[match_sha1]["path"] if os.path.exists(path) and path not in candidates: candidates.append(path) + if candidates and all("/.variants/" in p for p in candidates): + # Only variants answer the exact casing, but the primary they + # stand in for may be stored under another one: x1 asked for + # IPLROM.X1 and got Recalbox's second md5 while iplrom.x1, the + # dump every platform pins, sat in the same directory. A primary + # counts here only in a variant's own directory and under its own + # file name: an alias (iplrom.x1t names a font in System.dat) or + # another machine's font.rom is not that file. + homes = {p.split("/.variants/", 1)[0] for p in candidates} + folded = _casefold_name_index(by_name) + for try_name in names_to_try: + wanted = try_name.rsplit("/", 1)[-1].casefold() + for match_sha1 in folded.get(try_name.casefold(), []): + path = files_db.get(match_sha1, {}).get("path", "") + if ( + "/.variants/" not in path + and os.path.dirname(path) in homes + and os.path.basename(path).casefold() == wanted + and _size_ok(match_sha1) + and os.path.exists(path) + and path not in candidates + ): + candidates.append(path) if not candidates: # Case-insensitive fallback: upstream platforms disagree on - # casing (VEC_MineStorm.vec vs VEC_Minestorm.vec). Safe by - # invariant: dedup keeps a single casing per content, so a - # casefold match cannot pick a different file. + # casing (VEC_MineStorm.vec vs VEC_Minestorm.vec). Two casings + # can name two contents (FONT.ROM and font.rom), so this only + # runs when no casing matches exactly. folded = _casefold_name_index(by_name) for try_name in names_to_try: for match_sha1 in folded.get(try_name.casefold(), []): diff --git a/tests/test_casefold_cache.py b/tests/test_casefold_cache.py index efb55069..41250986 100644 --- a/tests/test_casefold_cache.py +++ b/tests/test_casefold_cache.py @@ -26,5 +26,60 @@ class CasefoldIndexFollowsItsSource(unittest.TestCase): self.assertIn("b.bin", _casefold_name_index(index)) +class APrimaryUnderAnotherCasing(unittest.TestCase): + """x1 asked for IPLROM.X1; only Recalbox's second md5 sat under that + casing, in .variants/, and it won over iplrom.x1, the dump every + platform pins, in the same directory.""" + + def setUp(self): + import shutil # noqa: PLC0415 + import tempfile # noqa: PLC0415 + + self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, self.root, True) + self.files: dict = {} + self.by_name: dict = {} + + def _put(self, relative: str, data: bytes, indexed_as: str | None = None) -> None: + import hashlib # noqa: PLC0415 + + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + sha1 = hashlib.sha1(data).hexdigest() + self.files[sha1] = {"path": str(path), "sha1": sha1, "size": len(data)} + name = indexed_as or path.name.split(".variants/")[-1] + if "/.variants/" in relative: + name = name.rsplit(".", 1)[0] + self.by_name.setdefault(name, []).append(sha1) + + def _resolve(self, name: str) -> str: + from common import _casefold_index_cache, resolve_local_file # noqa: PLC0415 + + _casefold_index_cache.clear() + db = {"files": self.files, "indexes": {"by_name": self.by_name, "by_md5": {}, + "by_path_suffix": {}, "by_crc32": {}}} + path, _status = resolve_local_file({"name": name}, db) + return str(Path(path).relative_to(self.root)) + + def test_the_primary_beside_the_variant_wins(self): + self._put("bios/Sharp/X1/iplrom.x1", b"pinned") + self._put("bios/Sharp/X1/.variants/IPLROM.X1.c4db9a6e", b"second md5") + self.assertEqual(self._resolve("IPLROM.X1"), "bios/Sharp/X1/iplrom.x1") + + def test_another_machines_file_does_not(self): + self._put("bios/NEC/PC-88/font.rom", b"pc-88 font") + self._put("bios/NEC/PC-98/.variants/FONT.ROM.541271ea", b"pc-98 font") + self.assertEqual(self._resolve("FONT.ROM"), + "bios/NEC/PC-98/.variants/FONT.ROM.541271ea") + + def test_an_alias_naming_another_file_does_not(self): + # System.dat pins a font's md5 under IPLROM.X1T. + self._put("bios/Sharp/X1/FNT0808.X1", b"font", indexed_as="iplrom.x1t") + self._put("bios/Sharp/X1/.variants/IPLROM.X1T.44620f57", b"turbo ipl") + self.assertEqual(self._resolve("IPLROM.X1T"), + "bios/Sharp/X1/.variants/IPLROM.X1T.44620f57") + + if __name__ == "__main__": unittest.main()