mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 03:09:08 -05:00
The name step refuses a candidate outside the size an entry validates, but the path step returned any hashless file at the tail as path_exact, a proof slots arbitration trusts. A 32 KB cpc6128.rom at ZEsarUX's tail would have been served for its 48 KB composite. No profile hits it today.
280 lines
11 KiB
Python
280 lines
11 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"))
|
|
|
|
|
|
class TestSizeGateOnThePathStep(unittest.TestCase):
|
|
"""The path step took any file at the tail when no hash was declared,
|
|
while the name step already refused one outside the checked size."""
|
|
|
|
def test_a_file_outside_the_checked_size_is_not_path_proof(self):
|
|
with tempfile.TemporaryDirectory() as root:
|
|
previous = os.getcwd()
|
|
os.chdir(root)
|
|
self.addCleanup(os.chdir, previous)
|
|
rel = "bios/Other/x/cpc/cpc6128.rom"
|
|
Path(rel).parent.mkdir(parents=True)
|
|
Path(rel).write_bytes(b"x" * 32768)
|
|
sha1 = "e" * 40
|
|
db = {
|
|
"files": {sha1: {"path": rel, "name": "cpc6128.rom", "md5": "",
|
|
"size": 32768, "crc32": "", "sha1": sha1}},
|
|
"indexes": {"by_md5": {}, "by_name": {"cpc6128.rom": [sha1]},
|
|
"by_crc32": {}, "by_path_suffix": {"cpc/cpc6128.rom": [sha1]}},
|
|
}
|
|
entry = {"name": "cpc6128.rom", "size": 49152, "validation": ["size"]}
|
|
_path, status = resolve_local_file(entry, db, dest_hint="cpc/cpc6128.rom")
|
|
self.assertNotEqual(status, "path_exact")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|