fix: never prove a file by another emulator's tree

lrps2 names no hash for its GameIndex.yaml, and the only full-tail
copy was ARMSX2's: it counted as path proof and replaced the file
RetroArch pins on desktop targets, which have no ARMSX2. Upstream
PCSX2 took ARMSX2's patches.zip by name the same way. A copy in
another emulator's tree now proves nothing at any tail length, and
LRPS2's own GameIndex comes from the tree the buildbot publishes.
This commit is contained in:
Abdessamad Derraz committed 2026-10-17 08:12:24 +02:00
1 parent b9a65cd622
commit 6810e8df9a
3 files changed
+100 -13

No files matched your search

+9 -9
View File
@@ -19,6 +19,10 @@ bios_directory: "pcsx2/bios/"
resources_directory: "pcsx2/resources/"
notes: |
pcsx2/resources/GameIndex.yaml, the per-game patch, settings override and
compatibility database, is the one the buildbot publishes in LRPS2.zip (the
lrps2 data directory). An OSD warning is shown if it is missing; some games
do not boot without it.
Hard fork of PCSX2 (pre-Qt, 1.7-dev era) ported to libretro. x86_64 only.
BIOS detection is filename-agnostic: scans pcsx2/bios/ for any file between 4-8 MB
with a valid romdir structure (RESET + ROMVER entries). No hash validation.
@@ -28,6 +32,11 @@ notes: |
.info firmware_count=2 counts the bios folder and GameIndex.yaml.
.info description mentions EROM.BIN but no EROM loading path exists in the code.
data_directories:
- ref: lrps2
destination: ""
source_ref: "pcsx2/GameDatabase.cpp:49, pcsx2/GameDatabase.cpp:979"
files:
- name: "<bios>.bin"
path: "pcsx2/bios/<bios>.bin"
@@ -85,15 +94,6 @@ files:
Mechacon (disc drive controller) version as u32.
Auto-created with default 0x00020603 if missing.
- name: "GameIndex.yaml"
path: "pcsx2/resources/GameIndex.yaml"
required: false
description: "game compatibility database"
source_ref: "pcsx2/GameDatabase.cpp:49, pcsx2/GameDatabase.cpp:979"
note: >
YAML database of per-game patches, settings overrides, and compatibility fixes.
OSD warning shown if missing. Some games may not boot or have issues without it.
- name: "cheats_ws.zip"
path: "pcsx2/resources/cheats_ws.zip"
required: false
+14 -4
View File
@@ -644,8 +644,8 @@ def _rank_tail_candidates(
) -> list[str]:
"""Order the files a destination tail designates, the likeliest first.
A shortened tail found only in another emulator's tree is that
emulator's asset, not evidence about this entry. A full tail claimed
A tail found in another emulator's tree is that emulator's asset, not
evidence about this entry, whatever its length. A full tail claimed
twice goes to the file whose path carries the least on top of the
destination, then to the owner's own copy (super3's Games.xml was served
to Supermodel-Dojo by index order alone), then to any copy outside
@@ -655,8 +655,11 @@ def _rank_tail_candidates(
def path_of(sha1: str) -> str:
return files_db.get(sha1, {}).get("path", "")
if start:
candidates = [h for h in candidates if not _foreign_tree(path_of(h), owners)]
# Another emulator's tree answers that emulator's entry, at any tail
# length: lrps2 names no hash for pcsx2/resources/GameIndex.yaml, and the
# only full-tail copy was ARMSX2's, which then counted as path proof and
# replaced the GameIndex RetroArch pins on every desktop target.
candidates = [h for h in candidates if not _foreign_tree(path_of(h), owners)]
if len(candidates) < 2:
return candidates
depth = len(tail.split("/"))
@@ -1040,6 +1043,13 @@ def resolve_local_file(
if file_entry.get("members"):
candidates = _holding_members(candidates, file_entry["members"])
candidates = _by_affinity(candidates, file_entry, dest_hint)
# 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]
+77
View File
@@ -2287,6 +2287,83 @@ class ARegionDroppedExtraBlocksNothing(unittest.TestCase):
self.assertEqual(report({"foo.rom"}), {})
class AnotherEmulatorsTreeProvesNothing(unittest.TestCase):
"""lrps2 names no hash for pcsx2/resources/GameIndex.yaml; the only
full-tail copy was ARMSX2's, which counted as path proof and replaced
the GameIndex RetroArch pins on desktop targets, where ARMSX2 does not
exist. Upstream PCSX2 took ARMSX2's patches.zip by name the same way."""
def _db(self, root, files):
import hashlib
db = {"files": {}, "indexes": {"by_name": {}, "by_md5": {}, "by_crc32": {},
"by_path_suffix": {}}}
for relative, payload in files.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
db["files"][sha1] = {"path": relative, "name": path.name, "sha1": sha1,
"size": len(payload)}
db["indexes"]["by_name"].setdefault(path.name, []).append(sha1)
parts = relative.split("/")
for start in range(1, len(parts) - 1):
db["indexes"]["by_path_suffix"].setdefault(
"/".join(parts[start:]), []).append(sha1)
return db
def test_neither_the_full_tail_nor_the_name_lands_in_a_foreign_tree(self):
from scripts.common import resolve_local_file
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
root = Path(directory)
# The collection's layout, read relative to its root.
previous = os.getcwd()
os.chdir(root)
self.addCleanup(os.chdir, previous)
db = self._db(root, {
"bios/Other/armsx2/pcsx2/resources/GameIndex.yaml": b"fork",
"bios/Other/armsx2/pcsx2/resources/patches.zip": b"fork patches",
"bios/Sony/PlayStation 2/patches.zip": b"upstream patches",
})
index, status = resolve_local_file(
{"name": "GameIndex.yaml", "path": "pcsx2/resources/GameIndex.yaml",
"source_profile": "lrps2"},
db, dest_hint="pcsx2/resources/GameIndex.yaml",
)
# Held only in the fork's tree, the file answers by name and
# proves nothing about this entry.
self.assertEqual(status, "name_exact")
patches, _status = resolve_local_file(
{"name": "patches.zip", "path": "resources/patches.zip",
"source_profile": "pcsx2"},
db, dest_hint="resources/patches.zip",
)
self.assertTrue(patches.endswith("PlayStation 2/patches.zip"))
def test_no_desktop_retroarch_pack_serves_armsx2s_tree(self):
from scripts import slots
from scripts.common import (
load_data_dir_registry,
load_database,
load_emulator_profiles,
load_platform_config,
load_target_config,
)
if not (ROOT / "database.json").exists():
self.skipTest("database.json is not generated")
db = load_database(str(ROOT / "database.json"))
config = load_platform_config("retroarch", str(ROOT / "platforms"))
profiles = load_emulator_profiles(str(ROOT / "emulators"))
cores = set(load_target_config("retroarch", "x86_64", str(ROOT / "platforms")))
overrides = slots.pack_overrides(
config, profiles, db, None, load_data_dir_registry(str(ROOT / "platforms")),
target_cores=cores,
)
self.assertFalse([d for d, p in overrides.items() if "/armsx2/" in p])
class TheDatabaseCacheIsWrittenWhole(unittest.TestCase):
"""A crash while serializing left a truncated db_cache.json behind, and
the next run rehashed the whole collection from scratch."""