diff --git a/scripts/common.py b/scripts/common.py index 2c408de1..909af157 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1364,11 +1364,14 @@ def resolve_local_file( and path not in candidates ): candidates.append(path) - if not candidates: + owners = _owner_tokens(file_entry) + if not candidates or all(_foreign_tree(p, owners) for p in candidates): # Case-insensitive fallback: upstream platforms disagree on # 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. + # runs when no casing matches exactly, or when only another + # emulator's tree does: xm6typeg got XM6 Utility's synthesized + # SCSIINROM.DAT while the X68000 dump sat as scsiinrom.dat. folded = _casefold_name_index(by_name) for try_name in names_to_try: for match_sha1 in folded.get(try_name.casefold(), []): @@ -1385,21 +1388,23 @@ def resolve_local_file( elif len(candidates) > 1 and name.casefold().endswith(".zip"): candidates = _covering_archives(candidates) candidates = _by_affinity(candidates, file_entry, dest_hint) + best = candidates[0] if candidates else "" # A copy outside another emulator's tree comes first: the # affinity read pcsx2 in bios/Other/armsx2/pcsx2/ and served # upstream PCSX2 the patches.zip of ARMSX2. - owners = _owner_tokens(file_entry) candidates = [ p for p in candidates if not _foreign_tree(p, owners) ] or candidates primary = [p for p in candidates if "/.variants/" not in p] if primary or candidates: chosen = primary[0] if primary else candidates[0] - # Even the best of them is another emulator's own copy: the - # name says nothing about this profile's file, and a data - # directory it reads may hold it. Dolphin got Dolphin-MMJR's - # codehandler.bin while data/dolphin-sys held its own. - if data_dir_registry and _foreign_tree(chosen, _owner_tokens(file_entry)): + # The best of them is another emulator's own copy, served or + # set aside: the name says nothing about this profile's file, + # and a data directory it reads may hold it. Dolphin got + # Dolphin-MMJR's codehandler.bin while data/dolphin-sys held + # its own; blueMSX got the 16 KB RS232.ROM once msx-emu's + # 8 KB copy was set aside, while data/bluemsx held its own. + if data_dir_registry and _foreign_tree(best, owners): hit, _mismatch = _resolve_in_data_dirs( names_to_try, data_dir_registry, functools.partial( diff --git a/tests/test_foreign_tree_names.py b/tests/test_foreign_tree_names.py new file mode 100644 index 00000000..a4aea6f5 --- /dev/null +++ b/tests/test_foreign_tree_names.py @@ -0,0 +1,78 @@ +"""The name step does not settle on another emulator's copy. + +xm6typeg asked for SCSIINROM.DAT: only XM6 Utility's synthesized image +matched the casing, under bios/Other/xm6pro68k/, and the X68000 dump stored +as scsiinrom.dat was never a candidate. blueMSX asked for RS232.ROM: its own +8 KB copy was msx-emu's, set aside as foreign, and the 16 KB file left was +served while data/bluemsx held the 8 KB one. +""" + +from __future__ import annotations + +import hashlib +import os +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from common import resolve_local_file # noqa: E402 + + +class ForeignTreeNames(unittest.TestCase): + def setUp(self): + (REPO_ROOT / "tmp").mkdir(exist_ok=True) + self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, self.root, True) + # The collection's paths are relative to the repository root. + self.addCleanup(os.chdir, os.getcwd()) + os.chdir(self.root) + self.db = {"files": {}, "indexes": { + "by_md5": {}, "by_name": {}, "by_crc32": {}, "by_path_suffix": {}, + }} + + def _hold(self, rel: str, data: bytes) -> str: + target = self.root / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + sha1 = hashlib.sha1(data).hexdigest() + name = rel.rsplit("/", 1)[-1] + if "/.variants/" in rel: + name = name.rsplit(".", 1)[0] + self.db["files"][sha1] = { + "path": rel, "name": name, "size": len(data), + "md5": hashlib.md5(data).hexdigest(), "crc32": "", + } + self.db["indexes"]["by_name"].setdefault(name, []).append(sha1) + return rel + + def test_a_casing_only_another_tree_holds(self): + self._hold("bios/Other/xm6pro68k/.variants/SCSIINROM.DAT.47404fbb", b"X" * 64) + own = self._hold("bios/Sharp/X68000/scsiinrom.dat", b"D" * 64) + entry = {"name": "SCSIINROM.DAT", "source_emulator": "xm6typeg"} + self.assertEqual(resolve_local_file(entry, self.db)[0], own) + + def test_a_set_aside_copy_sends_the_name_to_the_data_directory(self): + self._hold("bios/Microsoft/MSX/RS232.ROM", b"L" * 128) + self._hold("bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM", b"S" * 64) + cache = self.root / "data" / "bluemsx" + (cache / "Machines" / "Shared Roms").mkdir(parents=True) + (cache / "Machines" / "Shared Roms" / "RS232.ROM").write_bytes(b"S" * 64) + entry = { + "name": "RS232.ROM", "path": "Machines/Shared Roms/RS232.ROM", + "size": 64, "source_emulator": "bluemsx", + } + path, status = resolve_local_file( + entry, self.db, + data_dir_registry={"bluemsx": {"local_cache": str(cache)}}, + ) + self.assertEqual(status, "data_dir") + self.assertEqual(Path(path).stat().st_size, 64) + + +if __name__ == "__main__": + unittest.main()