From 4377cbb2af0d3159202761214305b3d79ffaca13 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 08:36:42 +0100 Subject: [PATCH] fix: another tree's casing does not hold a file dosbox-x's et4000.bin matched only pcem's et4000.BIN. resolve_local_file, asked as dosbox-x, refuses another emulator's tree in its case-insensitive step, so the pack and installer leave it out, while the gap report and verify's core files resolved without the asker and counted it held. Both now resolve as the profile, and the manifests of Batocera and KNULLI no longer flag it held. --- install/batocera.json | 3 +- install/knulli.json | 3 +- scripts/cross_reference.py | 26 ++++++++++++++--- scripts/generate_site.py | 6 ++-- scripts/verify.py | 15 ++++++---- tests/test_foreign_tree_names.py | 48 ++++++++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/install/batocera.json b/install/batocera.json index 8c24297c0..114a811ab 100644 --- a/install/batocera.json +++ b/install/batocera.json @@ -1841,8 +1841,7 @@ "reason": "not_found", "cores": [ "dosbox-x" - ], - "held": true + ] }, { "dest": "extras.wad", diff --git a/install/knulli.json b/install/knulli.json index 25b3ccce4..f06163712 100644 --- a/install/knulli.json +++ b/install/knulli.json @@ -1598,8 +1598,7 @@ "reason": "not_found", "cores": [ "dosbox-x" - ], - "held": true + ] }, { "dest": "extras.wad", diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 71ac29ed9..08e1b74be 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -26,7 +26,9 @@ sys.path.insert(0, os.path.dirname(__file__)) from common import ( platform_arg, ProfileSelectionError, + _foreign_tree, _norm_system_id, + _owner_tokens, archive_entry, directory_members, expand_directory_entries, @@ -215,8 +217,14 @@ def _resolve_source( none of the hashes the entry declares: a name never wins over a declared hash, and zquestclassic's ending.mid read as held because Hydra Castle Labyrinth has another file of that name. + + Another casing of the name in another emulator's tree proves nothing, as + in resolve_local_file's case-insensitive step: dosbox-x's et4000.bin read + as held through pcem's et4000.BIN, which the pack and the installer + refuse it. """ declared = _declared_content(file_entry or {}) + owners = _owner_tokens(file_entry or {}) def _can_be(sha1s: list | str) -> bool: if not file_entry or db_files is None: @@ -233,6 +241,14 @@ def _resolve_source( """Whether the files indexed under *name* can be this entry.""" return _can_be(by_name.get(name) or []) + def _folded_hit(name: str) -> bool: + """_name_hit for a name matched in another casing.""" + return _can_be([ + sha1 for sha1 in by_name.get(name) or [] + if db_files is None + or not _foreign_tree(db_files.get(sha1, {}).get("path", ""), owners) + ]) + # bios/ via database.json by_name if fname in by_name and _name_hit(fname): return "bios" @@ -241,10 +257,10 @@ def _resolve_source( if basename and basename in by_name and _name_hit(basename): return "bios" key = fname.lower() - if key in by_name_lower and _name_hit(by_name_lower[key]): + if key in by_name_lower and _folded_hit(by_name_lower[key]): return "bios" if basename: - if basename.lower() in by_name_lower and _name_hit( + if basename.lower() in by_name_lower and _folded_hit( by_name_lower[basename.lower()] ): return "bios" @@ -257,7 +273,7 @@ def _resolve_source( if canonical and canonical != fname: if canonical in by_name and _name_hit(canonical): return "bios" - if canonical.lower() in by_name_lower and _name_hit( + if canonical.lower() in by_name_lower and _folded_hit( by_name_lower[canonical.lower()] ): return "bios" @@ -901,7 +917,9 @@ def _cross_reference_profile( source = next( ( found for version in versions[slot] - if (found := entry_source(version, index)) is not None + if (found := entry_source( + {**version, "source_profile": emu_name}, index, + )) is not None ), "missing", ) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 32f926da0..4e206261e 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -9416,7 +9416,7 @@ def _hash_spans(value: str | list, copy: bool = False) -> str: def _availability_check( - db: dict, data_names: set[str] | None + db: dict, data_names: set[str] | None, owner: str = "", ) -> Callable[[dict], bool]: """A predicate answering whether the collection holds a file. @@ -9447,7 +9447,7 @@ def _availability_check( # under the name belongs to another game or release. if not f.get("name") or f.get("unsourceable"): return False - return entry_source(f, index) is not None + return entry_source({**f, "source_profile": owner} if owner else f, index) is not None return _file_available @@ -10180,7 +10180,7 @@ def emulator_pages( lines.append("") lines.extend(markup.aside("info", "Why no files", [_escape_tags(str(exclusion))])[:-1]) else: - _file_available = _availability_check(db, data_names) + _file_available = _availability_check(db, data_names, name) lines.extend(["## Files", ""]) lines.extend(_emulator_file_summary(files)) diff --git a/scripts/verify.py b/scripts/verify.py index 8689a4dcf..b465ab4bc 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -667,13 +667,18 @@ def find_undeclared_files( # Resolution: name, then path basename # storage: release names where the bytes are served from, not # that the collection holds them: resolved like any file. + # Resolved as the asker, as the builder's ExtraResolver does: + # another casing of the name in another emulator's tree is + # not this emulator's file (dosbox-x's et4000.bin and pcem's + # et4000.BIN). + owned = {**f, "source_profile": emu_name} if shipped_on_mismatch and (f.get("md5") or f.get("sha1")): # The entry states what its content should be and the builder # drops a copy that contradicts it, so content decides here # too. A name match would not do: generic names collide across # systems, and answering yes on one describes a pack that will # not contain the file. - _lp, _st = resolve_local_file(f, db, dest_hint=dest) + _lp, _st = resolve_local_file(owned, db, dest_hint=dest) in_repo = _lp is not None and _st != "hash_mismatch" else: in_repo = _name_in_index( @@ -693,8 +698,8 @@ def find_undeclared_files( # rejects them all. NanoBoyAdvance's 16 KB bios.bin met # only PCem's and QEMU's PC BIOS images of that name. # The fallback below then decides as for any mismatch. - _lp, _st = resolve_local_file(f, db, dest_hint=dest) - if _st == "hash_mismatch": + _lp, _st = resolve_local_file(owned, db, dest_hint=dest) + if _st == "hash_mismatch" or _lp is None: in_repo = False if not in_repo: # Hash fallback: the repo may hold the content under a @@ -707,7 +712,7 @@ def find_undeclared_files( # frontend would reject it, the builder leaves it out, and # calling it available here would describe a pack that # does not contain it. - _lp, _st = resolve_local_file(f, db, dest_hint=dest) + _lp, _st = resolve_local_file(owned, db, dest_hint=dest) in_repo = _st != "not_found" and _lp is not None if in_repo and _st == "hash_mismatch" and shipped_on_mismatch: in_repo = False @@ -717,7 +722,7 @@ def find_undeclared_files( # held for it, the rule the builder's lookup applies # (packextras.ExtraResolver). The Apple IIGS' 128 KB # ROM is not Basilisk II's ROM. - _lp, _st = resolve_local_file(f, db, dest_hint=dest) + _lp, _st = resolve_local_file(owned, db, dest_hint=dest) if core_size_refusal( f, _lp, same_slot_entries(profile.get("files"), fname, dest) ): diff --git a/tests/test_foreign_tree_names.py b/tests/test_foreign_tree_names.py index 2be75d71b..360060add 100644 --- a/tests/test_foreign_tree_names.py +++ b/tests/test_foreign_tree_names.py @@ -185,5 +185,53 @@ class RetailDashboardTree(unittest.TestCase): self.assertEqual(status, "path_exact", path) +class TheGapReportAsksAsTheEmulator(unittest.TestCase): + """dosbox-x's et4000.bin, no hash: pcem's et4000.BIN was its only + casing. The resolver, asked as dosbox-x, refuses it and the pack and the + installer leave the file out, while the gap report, verify's core files + and the manifest's held flag counted it available.""" + + _hold = ForeignTreeNames._hold + + def setUp(self): + ForeignTreeNames.setUp(self) + self._hold("bios/Other/pcem/roms/et4000.BIN", b"E" * 64) + self.entry = {"name": "et4000.bin", "required": True} + + def _source(self, owner: str) -> str | None: + from cross_reference import entry_source + + by_name = self.db["indexes"]["by_name"] + index = { + "by_name": by_name, "by_name_lower": {k.lower(): k for k in by_name}, + "by_path_suffix": {}, "by_md5": {}, "by_sha256": {}, "by_crc32": {}, + "db_files": self.db["files"], "data_names": None, + } + return entry_source({**self.entry, "source_profile": owner}, index) + + def test_another_tree_casing_is_not_held(self): + self.assertIsNone(self._source("dosbox-x")) + entry = {**self.entry, "source_profile": "dosbox-x"} + self.assertEqual(resolve_local_file(entry, self.db), (None, "not_found")) + + def test_the_tree_owner_holds_it(self): + self.assertEqual(self._source("pcem"), "bios") + + def test_core_files_follow_the_resolver(self): + from verify import find_undeclared_files + + profiles = {"dosbox-x": { + "emulator": "DOSBox-X", "type": "libretro", "systems": ["dos"], + "files": [self.entry], + }} + config = {"platform": "P", "verification_mode": "md5", "cores": ["dosbox-x"], + "systems": {"dos": {"files": []}}} + (found,) = [ + u for u in find_undeclared_files(config, "emulators", self.db, profiles) + if u["name"] == "et4000.bin" + ] + self.assertFalse(found["in_repo"]) + + if __name__ == "__main__": unittest.main()