mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
fix: credit readers the file their pack resolves
The name step tried the destination's basename alongside the entry's own names. With dsi_nand.bin absent from a checkout, nand.bin from dsi/nand.bin let the Wii NAND outrank the entry's own DSi variant, and the site named melonDS Nightly a reader of it. The basename is now tried only when no own name answers. FileConsumers also resolved profile entries without the archive members the emulator pack passes, so gngeopi and neo-emu were credited with the MAME neogeo.zip instead of the archive their pack carries.
This commit is contained in:
1 parent
c2875a3851
commit
aeb68fe753
3 files changed
+139
-7
No files matched your search
+14
-6
@@ -1247,6 +1247,7 @@ def resolve_local_file(
|
||||
# When dest_hint contains a path, also try its basename as a name
|
||||
# (handles emulator profiles where name: is descriptive and path: is
|
||||
# the actual filename, e.g. name: "MDA font ROM", path: "mda.rom")
|
||||
own_names = list(names_to_try)
|
||||
if dest_hint:
|
||||
hint_base = dest_hint.rsplit("/", 1)[-1] if "/" in dest_hint else dest_hint
|
||||
if hint_base and hint_base not in names_to_try:
|
||||
@@ -1491,12 +1492,19 @@ def resolve_local_file(
|
||||
|
||||
if not has_strong_hash and not unsourceable:
|
||||
candidates = []
|
||||
for try_name in names_to_try:
|
||||
for match_sha1 in by_name.get(try_name, []):
|
||||
if match_sha1 in files_db and _size_ok(match_sha1):
|
||||
path = files_db[match_sha1]["path"]
|
||||
if os.path.exists(path) and path not in candidates:
|
||||
candidates.append(path)
|
||||
# The entry's own names first, the destination's basename only when
|
||||
# none of them answers. melonDS Nightly's dsi_nand.bin is read at
|
||||
# dsi/nand.bin: with the DSi image absent from the checkout, the
|
||||
# basename let the Wii nand.bin outrank the entry's own DSi variant.
|
||||
for tier in (own_names, names_to_try[len(own_names):]):
|
||||
for try_name in tier:
|
||||
for match_sha1 in by_name.get(try_name, []):
|
||||
if match_sha1 in files_db and _size_ok(match_sha1):
|
||||
path = files_db[match_sha1]["path"]
|
||||
if os.path.exists(path) and path not in candidates:
|
||||
candidates.append(path)
|
||||
if candidates:
|
||||
break
|
||||
if candidates and all("/.variants/" in p for p in candidates):
|
||||
# Only variants answer the exact casing, but the primary they
|
||||
# stand in for may be stored under another one: x1 asked for
|
||||
|
||||
@@ -53,6 +53,7 @@ from validation import (
|
||||
outside_gap_scope,
|
||||
read_from_save_dir,
|
||||
unread_on,
|
||||
with_archive_members,
|
||||
)
|
||||
|
||||
yaml = require_yaml()
|
||||
@@ -563,7 +564,12 @@ class FileConsumers:
|
||||
for dest, sha1 in members.items()
|
||||
]
|
||||
else:
|
||||
found = [self.sha1_of(entry, entry.get("path") or "", owner=name)]
|
||||
# The archive members the profile reads decide between
|
||||
# same-named copies, as in the emulator pack and verify.
|
||||
found = [self.sha1_of(
|
||||
with_archive_members(entry, files), entry.get("path") or "",
|
||||
owner=name,
|
||||
)]
|
||||
for sha1 in found:
|
||||
if sha1:
|
||||
self.emulators.setdefault(sha1, set()).add(name)
|
||||
|
||||
@@ -173,6 +173,124 @@ class ReadersFollowTheDataTrees(unittest.TestCase):
|
||||
self.assertIn("dolphin", self._readers({}))
|
||||
|
||||
|
||||
class TheDestinationBasenameComesLast(unittest.TestCase):
|
||||
"""melonDS Nightly reads dsi_nand.bin at dsi/nand.bin. With the DSi image
|
||||
absent from the checkout and only its variant left, the destination's
|
||||
basename let the Wii nand.bin outrank the entry's own name, and the site
|
||||
named melonDS Nightly a reader of the Wii NAND."""
|
||||
|
||||
def setUp(self):
|
||||
self._tree = tempfile.TemporaryDirectory()
|
||||
root = Path(self._tree.name)
|
||||
files = {}
|
||||
by_name: dict[str, list[str]] = {}
|
||||
for rel, payload in (
|
||||
("bios/Nintendo/Wii/nand.bin", b"wii nand"),
|
||||
("bios/Nintendo/Nintendo DSi/.variants/dsi_nand.bin.8b79a73c", b"dsi nand"),
|
||||
):
|
||||
path = root / rel
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(payload)
|
||||
sha1 = hashlib.sha1(payload).hexdigest()
|
||||
name = "dsi_nand.bin" if "dsi_nand" in rel else path.name
|
||||
files[sha1] = {"name": name, "sha1": sha1, "path": str(path),
|
||||
"md5": hashlib.md5(payload).hexdigest(), "size": len(payload)}
|
||||
by_name.setdefault(name, []).append(sha1)
|
||||
self.wii = hashlib.sha1(b"wii nand").hexdigest()
|
||||
self.dsi = hashlib.sha1(b"dsi nand").hexdigest()
|
||||
self.db = {"files": files, "indexes": {
|
||||
"by_name": by_name, "by_md5": {}, "by_path_suffix": {}}}
|
||||
self.entry = {"name": "dsi_nand.bin", "path": "dsi/nand.bin", "agnostic": True}
|
||||
|
||||
def tearDown(self):
|
||||
self._tree.cleanup()
|
||||
|
||||
def test_the_resolver_keeps_the_entry_name(self):
|
||||
from common import resolve_local_file
|
||||
|
||||
path, status = resolve_local_file(
|
||||
{**self.entry, "source_profile": "melonds-nightly"}, self.db,
|
||||
dest_hint=self.entry["path"],
|
||||
)
|
||||
self.assertEqual((path, status), (self.db["files"][self.dsi]["path"], "name_exact"))
|
||||
|
||||
def test_the_wii_nand_has_no_ds_reader(self):
|
||||
consumers = FileConsumers(self.db, data_dir_registry={})
|
||||
consumers.add_emulators({"melonds-nightly": {"type": "standalone", "files": [self.entry]}})
|
||||
self.assertEqual(consumers.emulators.get(self.wii), None)
|
||||
self.assertEqual(consumers.emulators.get(self.dsi), {"melonds-nightly"})
|
||||
|
||||
def test_a_descriptive_name_still_reaches_the_basename(self):
|
||||
"""name: "MDA font ROM", path: "mda.rom" is the case the basename step serves."""
|
||||
from common import resolve_local_file
|
||||
|
||||
path, _ = resolve_local_file(
|
||||
{"name": "Wii NAND image", "path": "nand.bin"}, self.db, dest_hint="nand.bin",
|
||||
)
|
||||
self.assertEqual(path, self.db["files"][self.wii]["path"])
|
||||
|
||||
|
||||
class EveryReaderIsTheEmulatorPackFile(unittest.TestCase):
|
||||
"""Each emulator FileConsumers credits with a file is one whose entry the
|
||||
emulator pack resolves to that file: the same entry fields (archive
|
||||
members, owner), destination hint, ZIP index and data trees. The pairing
|
||||
omitted the archive members, and gngeopi and neo-emu were credited with
|
||||
the MAME neogeo.zip while their pack carries the one holding the ROMs
|
||||
they open."""
|
||||
|
||||
def test_pairings_agree_with_the_resolver(self):
|
||||
import json
|
||||
import os
|
||||
|
||||
from common import (
|
||||
build_zip_contents_index, load_data_dir_registry,
|
||||
load_emulator_profiles, resolve_local_file,
|
||||
)
|
||||
from validation import with_archive_members
|
||||
|
||||
db_path = REPO_ROOT / "database.json"
|
||||
if not db_path.exists():
|
||||
self.skipTest("database.json absent")
|
||||
previous = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
db = json.loads(db_path.read_text(encoding="utf-8"))
|
||||
zips = build_zip_contents_index(db)
|
||||
registry = load_data_dir_registry(str(REPO_ROOT / "platforms"))
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
consumers = FileConsumers(db, zips, registry)
|
||||
consumers.add_emulators(profiles)
|
||||
sha1_by_path = {e["path"]: s for s, e in db["files"].items()}
|
||||
|
||||
expected: set[tuple[str, str]] = set()
|
||||
from_trees: set[tuple[str, str]] = set()
|
||||
for name, profile in profiles.items():
|
||||
if profile.get("type") == "alias":
|
||||
continue
|
||||
files = profile.get("files") or []
|
||||
for entry in files:
|
||||
if not isinstance(entry, dict) or not entry.get("name"):
|
||||
continue
|
||||
members = consumers.directory_members(entry)
|
||||
if members:
|
||||
from_trees |= {(sha1, name) for sha1 in members.values()}
|
||||
continue
|
||||
path, status = resolve_local_file(
|
||||
{**with_archive_members(entry, files), "source_profile": name},
|
||||
db, zips, dest_hint=entry.get("path") or "",
|
||||
data_dir_registry=registry,
|
||||
)
|
||||
if path and status not in ("not_found", "hash_mismatch"):
|
||||
sha1 = sha1_by_path.get(path)
|
||||
if sha1:
|
||||
expected.add((sha1, name))
|
||||
credited = {
|
||||
(sha1, name) for sha1, names in consumers.emulators.items() for name in names
|
||||
}
|
||||
self.assertEqual(sorted(expected - credited), [])
|
||||
self.assertEqual(sorted(credited - expected - from_trees), [])
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
_TMP.cleanup()
|
||||
|
||||
|
||||
Reference in new issue
Block a user