diff --git a/scripts/common.py b/scripts/common.py index 6d6b1a95..80413e4c 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -642,6 +642,7 @@ def resolve_local_file( # 2b. Path suffix lookup is useful for same-named regional files, but it # is identity evidence only when no content hash was declared. A stale # or incorrect destination can therefore never mask a hash mismatch. + own_file_absent = False if dest_hint and by_path_suffix: # A destination is written from the emulator's point of view # ("pcsx2/resources/GameIndex.yaml") and the index from the repo's @@ -665,6 +666,14 @@ def resolve_local_file( candidates = by_path_suffix.get(tail, []) if start and len(candidates) > 1: continue + # The index names the file this destination designates. Not on + # disk, it is a release asset the checkout does not hold, and no + # other file of that name is it. + if not start and candidates and not any( + os.path.exists(files_db.get(h, {}).get("path", "")) + for h in candidates + ): + own_file_absent = True if len(candidates) > 1: depth = len(tail.split("/")) candidates = sorted( @@ -699,7 +708,7 @@ def resolve_local_file( # an FPseNG font, an openMSX one and a 3DO ROM. Content still decides: the # hash and path steps above run first, so collecting the real bytes makes # the entry resolve. - unsourceable = bool(file_entry.get("unsourceable")) + unsourceable = bool(file_entry.get("unsourceable")) or own_file_absent if not has_strong_hash and not unsourceable: candidates = [] diff --git a/tests/test_game_data_homonyms.py b/tests/test_game_data_homonyms.py index fa2a7bed..a9bb595a 100644 --- a/tests/test_game_data_homonyms.py +++ b/tests/test_game_data_homonyms.py @@ -81,6 +81,56 @@ class HomonymRule(unittest.TestCase): self.assertEqual(homonyms(claims), []) +class AbsentFileIsNotReplacedByAHomonym(unittest.TestCase): + """The database names the file a destination designates. When that file + is a release asset the checkout does not hold, the name step answered + with whatever else carried the name: Enemy Territory's `etmain/pak0.pk3` + resolved to the Quake III demo pak on a clone without the large files.""" + + def setUp(self): + import hashlib + import tempfile + + import generate_db + + self._tmp = tempfile.TemporaryDirectory() + root = Path(self._tmp.name) + files = {} + for relative, payload in ( + ("bios/ET/etmain/pak0.pk3", b"enemy territory"), + ("bios/Q3/demoq3/pak0.pk3", b"quake iii demo"), + ): + target = root / relative + target.parent.mkdir(parents=True) + target.write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files[sha1] = { + "path": str(target), "name": "pak0.pk3", "size": len(payload), + "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "0", + } + indexes = generate_db.build_indexes( + {s: {**r, "path": r["path"][len(str(root)) + 1:]} for s, r in files.items()}, + {}, + ) + self.db = {"files": files, "indexes": indexes} + self.own = root / "bios/ET/etmain/pak0.pk3" + self.entry = {"name": "pak0.pk3", "path": "etmain/pak0.pk3"} + + def tearDown(self): + self._tmp.cleanup() + + def _resolve(self): + return resolve_local_file(self.entry, self.db, {}, dest_hint="etmain/pak0.pk3") + + def test_present_it_resolves_by_its_path(self): + self.assertEqual(self._resolve(), (str(self.own), "path_exact")) + + def test_absent_it_is_not_found_rather_than_another_game(self): + self.own.unlink() + self.assertEqual(self._resolve(), (None, "not_found")) + + class CollectionCarriesNoGameDataHomonym(unittest.TestCase): def test_the_profiles_resolve_no_game_data_to_another_game(self): database = REPO_ROOT / "database.json"