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()