From 402e688b01a440a2427fc3d3c53f1d95306513e1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 03:21:07 +0100 Subject: [PATCH] fix: platform entries skip other emulators' trees A platform entry names no profile, so bios/Other// was never foreign to it. EmuELEC's ports/VVVVVV/data.zip resolved to xrick's data.zip, then to xrick's data directory copy. A system that lists its cores now takes no name match only another emulator's tree answers. --- install/emuelec.json | 34 ++++++++-------- scripts/common.py | 33 +++++++++++++++- scripts/generate_site.py | 9 ++++- tests/test_game_data_homonyms.py | 66 ++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/install/emuelec.json b/install/emuelec.json index 1be551132..8135201bd 100644 --- a/install/emuelec.json +++ b/install/emuelec.json @@ -20,11 +20,11 @@ } ], "standalone_copies": [], - "total_files": 5297, - "total_size": 4143074421, - "pack_files": 9169, - "pack_size": 4257242836, - "total_omitted": 197, + "total_files": 5296, + "total_size": 4141616981, + "pack_files": 9168, + "pack_size": 4255785396, + "total_omitted": 198, "omitted_files": [ { "dest": "FB Alpha 2012/hiscore.dat", @@ -2070,6 +2070,18 @@ ], "category": "game_data" }, + { + "dest": "ports/VVVVVV/data.zip", + "name": "data.zip", + "system": "vvvvvv", + "required": true, + "reason": "not_found", + "cores": null, + "systems": [ + "vvvvvv" + ], + "category": "game_data" + }, { "dest": "ports/diablo/diabdat.mpq", "name": "diabdat.mpq", @@ -8911,18 +8923,6 @@ ], "category": "game_data" }, - { - "dest": "ports/VVVVVV/data.zip", - "sha1": "ecfc092fe6371dbf38e238a8ba5f90785b5db52d", - "sha256": "2a86458696e83eb924fc6c6fda3ca5d320ca90885bd6c2f32d121757ade389bb", - "size": 1457440, - "repo_path": "bios/Other/xrick/data.zip", - "cores": null, - "systems": [ - "vvvvvv" - ], - "category": "game_data" - }, { "dest": "ports/xrick/data.zip", "sha1": "ecfc092fe6371dbf38e238a8ba5f90785b5db52d", diff --git a/scripts/common.py b/scripts/common.py index a1f057f23..0244c09fb 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -424,11 +424,24 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - # Each entry carries the system that declares it: a hashless name match is # narrowed to the maker folder that system names (resolve_local_file). + # A system that names its own cores (EmuELEC, AmberELEC, KNULLI) also + # passes them on, so another emulator's tree is foreign to its entries. # Fresh dicts, since a YAML anchor can share one entry between systems. config["systems"] = { sys_id: { **system, - "files": [{**f, "platform_system": sys_id} for f in system["files"]], + "files": [ + { + **f, + "platform_system": sys_id, + **( + {"platform_cores": list(system["cores"])} + if system.get("cores") + else {} + ), + } + for f in system["files"] + ], } if isinstance(system, dict) and system.get("files") else system @@ -1735,6 +1748,24 @@ def resolve_local_file( continue if os.path.exists(path) and path not in candidates: candidates.append(path) + # A platform entry names no profile, but a system that lists its + # cores says whose files it asks for. A name only another emulator's + # tree answers is no evidence, and the data directories hold those + # emulators' trees too: EmuELEC's VVVVVV data.zip was served xrick's + # archive of the same name, then xrick's data directory copy. + core_owners = { + token + for core in file_entry.get("platform_cores") or () + for token in _affinity_tokens(str(core)) + } + if ( + not owners + and core_owners + and candidates + and all(_foreign_tree(p, core_owners) for p in candidates) + ): + candidates = [] + unsourceable = True if makers and candidates: candidates = [p for p in candidates if not other_maker(p, makers, db)] if not candidates: diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 6fe7b38ca..02a852ea1 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1035,13 +1035,18 @@ def _split_contract(config: dict) -> tuple[dict, dict]: """ allowed = _contract_keys() contract = {key: value for key, value in config.items() if key in allowed} - # platform_system is the resolver's copy of the key each entry sits under. + # platform_system and platform_cores are the resolver's copies of the key + # each entry sits under and of that system's cores. if isinstance(contract.get("systems"), dict): contract["systems"] = { sys_id: { **system, "files": [ - {k: v for k, v in f.items() if k != "platform_system"} + { + k: v + for k, v in f.items() + if k not in ("platform_system", "platform_cores") + } for f in system["files"] ], } diff --git a/tests/test_game_data_homonyms.py b/tests/test_game_data_homonyms.py index 5724f49ec..6e5cc144f 100644 --- a/tests/test_game_data_homonyms.py +++ b/tests/test_game_data_homonyms.py @@ -148,6 +148,72 @@ class AbsentFileIsNotReplacedByAHomonym(unittest.TestCase): self.assertEqual(self._resolve(), (None, "not_found")) +class APlatformEntryTakesNoOtherEmulatorsTree(unittest.TestCase): + """A platform declaration names no profile, so another emulator's tree + was never foreign to it: EmuELEC's ports/VVVVVV/data.zip was served + xrick's data.zip, Rick Dangerous sounds, then xrick's data directory + copy. A system that lists its cores says whose files it asks for.""" + + def setUp(self): + import tempfile + + import generate_db + + self._tmp = tempfile.TemporaryDirectory() + self._cwd = os.getcwd() + os.chdir(self._tmp.name) + payload = b"rick dangerous" + target = Path("bios/Other/xrick/data.zip") + target.parent.mkdir(parents=True) + target.write_bytes(payload) + cache = Path("data/xrick/data.zip") + cache.parent.mkdir(parents=True) + cache.write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files = {sha1: { + "path": str(target), "name": "data.zip", "size": len(payload), + "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "0", + }} + self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + self.registry = {"xrick": {"local_cache": "data/xrick"}} + + def tearDown(self): + os.chdir(self._cwd) + self._tmp.cleanup() + + def _resolve(self, **fields): + entry = {"name": "data.zip", "destination": "ports/VVVVVV/data.zip", + "platform_system": "vvvvvv", **fields} + return resolve_local_file( + entry, self.db, {}, dest_hint=entry["destination"], + data_dir_registry=self.registry, + ) + + def test_a_system_naming_other_cores_gets_nothing(self): + self.assertEqual(self._resolve(platform_cores=["VVVVVV"]), (None, "not_found")) + + def test_the_owners_own_system_still_gets_it(self): + self.assertEqual( + self._resolve(platform_cores=["xrick"]), + ("bios/Other/xrick/data.zip", "name_exact"), + ) + + def test_the_emuelec_entry_is_not_served_xrick(self): + os.chdir(self._cwd) + from common import load_platform_config + + config = load_platform_config("emuelec", str(REPO_ROOT / "platforms")) + entry = config["systems"]["vvvvvv"]["files"][0] + self.assertEqual(entry["platform_cores"], ["VVVVVV"]) + db = load_database(str(REPO_ROOT / "database.json")) + path, _status = resolve_local_file( + entry, db, {}, dest_hint=entry["destination"], + data_dir_registry=load_data_dir_registry(str(REPO_ROOT / "platforms")), + ) + self.assertFalse(path and "xrick" in path, path) + + class SecondPassKeepsIdentity(unittest.TestCase): """A copy of a covered file at another core's path keeps the entry's proof.