Files
libretro/tests/test_path_suffix.py
T
Abdessamad Derraz adaa1eba87 fix: a shared tail is proof only in the own tree
PCSX2x6 reads bin/resources/GameIndex.yaml, an arcade database keyed by NM gameid. The shortened tail resources/GameIndex.yaml matched four files, and the resolver set aside the three in other emulators' trees before measuring ambiguity, so PCSX2's console database passed for path proof and shipped in the PCSX2x6 pack. Ambiguity is now read before that filtering. A tail indexed from an identical copy is kept even when the database records the other copy's path.

PCSX2x6's GameIndex.yaml and RedumpDatabase.yaml are collected from its repository at the pinned 583f0e90 (git blobs ed8232e3 and 8669b3af).
2026-10-19 23:14:24 +02:00

256 lines
10 KiB
Python

"""Which tails of a stored path can answer a declared destination."""
from __future__ import annotations
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from common import resolve_local_file # noqa: E402
from generate_db import _path_suffixes # noqa: E402
class TestPathSuffixes(unittest.TestCase):
"""Every tail down to two segments, never the bare filename."""
def test_longest_first_and_never_the_bare_name(self):
self.assertEqual(
_path_suffixes("bios/Nintendo/GameCube/GC/USA/IPL.bin"),
[
"Nintendo/GameCube/GC/USA/IPL.bin",
"GameCube/GC/USA/IPL.bin",
"GC/USA/IPL.bin",
"USA/IPL.bin",
],
)
def test_a_deep_tree_keeps_every_level(self):
# Seven 3DS system archives differ only past the sixth segment, so a
# single key per file merged all seven onto one.
suffixes = _path_suffixes(
"bios/Nintendo/3DS/Citra/nand/0000/title/0004009b/00014002/"
"content/00000000.app.romfs"
)
self.assertIn(
"nand/0000/title/0004009b/00014002/content/00000000.app.romfs",
suffixes,
)
self.assertEqual(suffixes[0].split("/")[0], "Nintendo")
def test_a_file_at_the_root_has_no_tail(self):
self.assertEqual(_path_suffixes("bios/thing.rom"), [])
def test_a_path_without_the_bios_prefix_is_still_split(self):
self.assertEqual(
_path_suffixes("Sony/PlayStation/scph5501.bin"),
["Sony/PlayStation/scph5501.bin", "PlayStation/scph5501.bin"],
)
def test_backslashes_and_empty_segments_are_normalised(self):
self.assertEqual(
_path_suffixes("bios\\\\Sega\\\\Saturn\\\\sat.bin"),
["Sega/Saturn/sat.bin", "Saturn/sat.bin"],
)
class TestResolutionAmbiguity(unittest.TestCase):
"""A tail that could name several files names none of them."""
@classmethod
def setUpClass(cls):
cls.root = Path(tempfile.mkdtemp(prefix="suffix-"))
cls.previous = os.getcwd()
cls.paths = {
"a" * 40: "bios/Other/emu_one/resources/shaders/convert.glsl",
"b" * 40: "bios/Other/emu_two/pcsx2/resources/shaders/convert.glsl",
"c" * 40: "bios/Other/emu_three/assets/sounds/unlock.wav",
"d" * 40: "bios/Vendor/Console/GC/JAP/IPL.bin",
}
for sha1, rel in cls.paths.items():
target = cls.root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(sha1[:4].encode())
files, by_name, by_suffix = {}, {}, {}
for sha1, rel in cls.paths.items():
name = rel.rsplit("/", 1)[-1]
files[sha1] = {"path": rel, "name": name, "md5": "", "size": 4,
"crc32": "", "sha1": sha1}
by_name.setdefault(name, []).append(sha1)
for suffix in _path_suffixes(rel):
if suffix != name:
by_suffix.setdefault(suffix, []).append(sha1)
cls.db = {
"files": files,
"indexes": {"by_md5": {}, "by_name": by_name, "by_crc32": {},
"by_path_suffix": by_suffix},
}
os.chdir(cls.root)
@classmethod
def tearDownClass(cls):
os.chdir(cls.previous)
shutil.rmtree(cls.root, ignore_errors=True)
def _resolve(self, name: str, hint: str):
return resolve_local_file({"name": name}, self.db, dest_hint=hint)
def test_a_full_tail_claimed_twice_goes_to_the_closest_owner(self):
# emu_one stores it right at its destination while emu_two nests it one
# directory deeper. The one that IS the destination wins.
path, status = self._resolve(
"convert.glsl", "resources/shaders/convert.glsl"
)
self.assertEqual(status, "path_exact")
self.assertIn("emu_one", path)
def test_a_tail_two_segments_short_is_not_followed(self):
# "sounds/achievements/unlock.wav" would reach emu_three, which is a
# different emulator's tree, so the name step answers instead.
path, status = self._resolve(
"unlock.wav", "pcsx2/resources/sounds/unlock.wav"
)
self.assertEqual(status, "name_exact")
self.assertIn("emu_three", path)
def test_one_segment_of_descent_is_allowed(self):
path, status = self._resolve("IPL.bin", "system/GC/JAP/IPL.bin")
self.assertEqual(status, "path_exact")
self.assertIn("JAP", path)
def test_an_exact_tail_still_wins(self):
path, status = self._resolve("IPL.bin", "GC/JAP/IPL.bin")
self.assertEqual(status, "path_exact")
self.assertIn("JAP", path)
class TestSizeGateOnTheNameStep(unittest.TestCase):
"""A name answer must still be a plausible size for the entry."""
@classmethod
def setUpClass(cls):
cls.root = Path(tempfile.mkdtemp(prefix="sizegate-"))
cls.previous = os.getcwd()
small = cls.root / "bios/Apple/IIGS/ROM"
large = cls.root / "bios/Apple/Macintosh/ROM"
for path, payload in ((small, b"x" * 8), (large, b"y" * 64)):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
cls.db = {
"files": {
"a" * 40: {"path": "bios/Apple/IIGS/ROM", "name": "ROM",
"md5": "", "size": 8, "crc32": "", "sha1": "a" * 40},
"b" * 40: {"path": "bios/Apple/Macintosh/ROM", "name": "ROM",
"md5": "", "size": 64, "crc32": "", "sha1": "b" * 40},
},
"indexes": {
"by_md5": {}, "by_crc32": {}, "by_path_suffix": {},
"by_name": {"ROM": ["a" * 40, "b" * 40]},
},
}
os.chdir(cls.root)
@classmethod
def tearDownClass(cls):
os.chdir(cls.previous)
shutil.rmtree(cls.root, ignore_errors=True)
def test_a_candidate_outside_the_declared_range_is_refused(self):
entry = {"name": "ROM", "min_size": 32, "max_size": 128,
"validation": ["size"]}
path, _ = resolve_local_file(entry, self.db)
self.assertIn("Macintosh", path)
def test_a_present_file_of_the_wrong_size_is_still_returned(self):
# Reported as present and untested, never as absent: the user has a
# file under that name and needs to be told it is the wrong one.
entry = {"name": "ROM", "size": 999, "validation": ["size"]}
path, status = resolve_local_file(entry, self.db)
self.assertIsNotNone(path)
self.assertEqual(status, "hash_mismatch")
def test_a_list_of_accepted_sizes_gates_the_name_step(self):
# SC-55/ROM2.BIN accepts two sizes. A PlayStation 2 ROM2.BIN of a
# third size answered to the name because the list was ignored.
entry = {"name": "ROM", "size": [8, 999], "validation": ["size"]}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "name_exact")
self.assertIn("IIGS", path)
entry = {"name": "ROM", "size": [999, 1000], "validation": ["size"]}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "hash_mismatch")
def test_a_size_without_validation_rejects_nothing(self):
# Documented behaviour: a size is informative until the emulator
# itself checks it.
entry = {"name": "ROM", "size": 999}
path, _ = resolve_local_file(entry, self.db)
self.assertIsNotNone(path)
class TestForeignTreesAndSharedTails(unittest.TestCase):
"""PCSX2x6's arcade GameIndex.yaml met four copies under a shortened
tail: three in other emulators' trees and PCSX2's console database. The
three were set aside first, the fourth then looked unique and passed for
path proof."""
@classmethod
def setUpClass(cls):
cls.root = Path(tempfile.mkdtemp(prefix="foreign-"))
cls.previous = os.getcwd()
stored = {
"1" * 40: "bios/Other/aethersx2/resources/GameIndex.yaml",
"2" * 40: "bios/Other/armsx2/resources/GameIndex.yaml",
"3" * 40: "bios/Other/emucorex/resources/GameIndex.yaml",
"4" * 40: "bios/Sony/PlayStation 2/resources/GameIndex.yaml",
"5" * 40: "bios/Other/emucorex/resources/RedumpDatabase.yaml",
}
# Same bytes as emucorex's copy: the database keeps one path for it.
duplicate = ("5" * 40, "bios/Other/x6/bin/resources/RedumpDatabase.yaml")
files, by_name, by_suffix = {}, {}, {}
for sha1, rel in [*stored.items(), duplicate]:
target = cls.root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(sha1[:4].encode())
name = rel.rsplit("/", 1)[-1]
if sha1 not in files:
files[sha1] = {"path": rel, "name": name, "md5": "", "size": 4,
"crc32": "", "sha1": sha1}
by_name.setdefault(name, []).append(sha1)
for suffix in _path_suffixes(rel):
if sha1 not in by_suffix.setdefault(suffix, []):
by_suffix[suffix].append(sha1)
cls.db = {
"files": files,
"indexes": {"by_md5": {}, "by_name": by_name, "by_crc32": {},
"by_path_suffix": by_suffix},
}
os.chdir(cls.root)
@classmethod
def tearDownClass(cls):
os.chdir(cls.previous)
shutil.rmtree(cls.root, ignore_errors=True)
def test_a_shared_shortened_tail_proves_nothing(self):
_path, status = resolve_local_file(
{"name": "GameIndex.yaml", "source_profile": "x6"}, self.db,
dest_hint="bin/resources/GameIndex.yaml",
)
self.assertNotEqual(status, "path_exact")
def test_a_tail_indexed_from_a_duplicate_is_the_askers(self):
path, status = resolve_local_file(
{"name": "RedumpDatabase.yaml", "source_profile": "x6"}, self.db,
dest_hint="bin/resources/RedumpDatabase.yaml",
)
self.assertEqual((status, path), ("path_exact", "bios/Other/emucorex/resources/RedumpDatabase.yaml"))
if __name__ == "__main__":
unittest.main()