fix: claim the archives a core reads by members

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 19:50:27 +02:00
1 parent 9faa3a3f79
commit 598c930771
6 files changed
+176 -31

No files matched your search

+9 -9
View File
@@ -17,9 +17,9 @@
],
"standalone_copies": [],
"total_files": 5869,
"total_size": 6160101174,
"total_size": 6161070712,
"pack_files": 8512,
"pack_size": 6200633701,
"pack_size": 6201603239,
"total_omitted": 0,
"omitted_files": [],
"core_names": {
@@ -1528,10 +1528,10 @@
},
{
"dest": "fbneo/nmk004.zip",
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": null,
"systems": [
"arcade"
@@ -1841,10 +1841,10 @@
},
{
"dest": "fbneo/neocdz.zip",
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": null,
"systems": [
"arcade",
+9 -9
View File
@@ -256,9 +256,9 @@
}
],
"total_files": 5869,
"total_size": 6160101174,
"total_size": 6161070712,
"pack_files": 8512,
"pack_size": 6200633701,
"pack_size": 6201603239,
"total_omitted": 0,
"omitted_files": [],
"core_names": {
@@ -1767,10 +1767,10 @@
},
{
"dest": "fbneo/nmk004.zip",
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": null,
"systems": [
"arcade"
@@ -2080,10 +2080,10 @@
},
{
"dest": "fbneo/neocdz.zip",
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": null,
"systems": [
"arcade",
+9 -9
View File
@@ -16,9 +16,9 @@
],
"standalone_copies": [],
"total_files": 9906,
"total_size": 8494372405,
"total_size": 8495341943,
"pack_files": 12549,
"pack_size": 8534904932,
"pack_size": 8535874470,
"total_omitted": 0,
"omitted_files": [],
"core_names": {
@@ -1545,10 +1545,10 @@
},
{
"dest": "fbneo/nmk004.zip",
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": null,
"systems": [
"arcade"
@@ -1858,10 +1858,10 @@
},
{
"dest": "fbneo/neocdz.zip",
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": null,
"systems": [
"arcade",
+6
View File
@@ -623,6 +623,12 @@ def archive_entry(archive: str, files: list[dict]) -> dict:
return {**own, "members": members} if members else own
def holds_required_members(path: str, members: list[dict]) -> bool:
"""Whether an archive holds every member a profile requires inside it."""
crcs = zip_member_crcs(path)
return all(m["crc32"] in crcs for m in members if m["required"])
def _holding_members(paths: list[str], members: list[dict]) -> list[str]:
"""The same-named archives holding the most required members, then the most members.
+81 -4
View File
@@ -20,8 +20,10 @@ from dataclasses import dataclass, field
import nativemode
from common import (
archive_entry,
build_zip_contents_index,
check_inside_zip,
holds_required_members,
load_data_dir_registry,
parse_md5_list,
resolution_is_hash_exact,
@@ -34,7 +36,11 @@ from validation import outside_gap_scope
# A profile entry can prove a slot without declaring a hash: Dolphin names no
# checksum for the GameCube boot ROM because its source names none, and the
# proof is then the path the repository stores the file at.
PROVEN_STATUSES = frozenset({"path_exact"})
PROVEN_STATUSES = frozenset({"path_exact", "members_held"})
# A core that reads ROMs inside an archive proves the archive by its members:
# FBNeo's neocdz.zip is any copy holding neocd.bin and 000-lo.lo by crc.
MEMBERS_HELD = "members_held"
@dataclass
@@ -169,6 +175,12 @@ def profile_claims(
if profile.get("type") in ("launcher", "alias"):
continue
is_standalone = runs_standalone(emu_name, profile, standalone_cores)
claims.extend(
_archive_claims(
emu_name, profile, is_standalone, db, base_dest, placement,
zip_contents, data_dir_registry,
)
)
for entry in profile.get("files") or []:
if not isinstance(entry, dict):
continue
@@ -178,9 +190,8 @@ def profile_claims(
if outside_gap_scope(entry, is_standalone):
continue
# A member of a romset is not a file at the BIOS root: the archive
# holding it is what occupies a destination, and it is declared
# separately. Claiming the member put FBNeo's msx.zip contents
# loose at the platform's root.
# holding it occupies the destination (_archive_claims). Claiming
# the member put FBNeo's msx.zip contents loose at the root.
if entry.get("archive"):
continue
dest = (
@@ -221,6 +232,69 @@ def profile_claims(
return claims
def _archive_claims(
emu_name: str,
profile: dict,
is_standalone: bool,
db: dict,
base_dest: str,
placement,
zip_contents: dict | None,
data_dir_registry: dict | None,
) -> list[Claim]:
"""The archives a core reads its ROMs from, where the builder places them.
The archive sits at the root and, for a core that reads a directory of
its own (archive_prefix), under it as well. The claim is proven when the
copy holds every member the core requires.
"""
files = [
entry for entry in profile.get("files") or []
if isinstance(entry, dict) and not outside_gap_scope(entry, is_standalone)
]
prefix = profile.get("archive_prefix", "")
claims: list[Claim] = []
for archive in sorted({entry["archive"] for entry in files if entry.get("archive")}):
entry = archive_entry(archive, files)
members = entry.get("members")
if not members:
continue
system = next(f.get("system") for f in files if f.get("archive") == archive)
for dest in (archive, f"{prefix}/{archive}") if prefix else (archive,):
if placement is None:
full = f"{base_dest}/{dest}" if base_dest else dest
else:
placed = placement.place({"profile": emu_name, "system": system}, dest)
if placed is None:
continue
full = placed[1]
local, status = resolve_local_file(
{**entry, "source_profile": emu_name},
db,
zip_contents,
dest_hint=dest,
data_dir_registry=data_dir_registry,
)
if (
local
and not resolution_is_hash_exact(status)
and holds_required_members(local, members)
):
status = MEMBERS_HELD
claims.append(
Claim(
origin="profile",
destination=full,
name=archive,
emulator=emu_name,
entry=entry,
local_path=local,
status=status,
)
)
return claims
def _claim_rank(claim: Claim) -> int:
"""How strongly a platform claim speaks for what the pack ships."""
if resolution_is_hash_exact(claim.status):
@@ -526,6 +600,9 @@ def _satisfies(profile: Claim, platform: Claim, db: dict) -> bool:
"""
if profile.local_path == platform.local_path:
return True
members = profile.entry.get("members")
if members and platform.local_path:
return holds_required_members(platform.local_path, members)
if (
profile.local_path
and _lists_hashes(platform.entry)
+62
View File
@@ -646,5 +646,67 @@ class StrongestProfileClaimWins(unittest.TestCase):
)
class AnArchiveIsClaimedByItsMembers(unittest.TestCase):
"""System.dat pins fbneo/neocdz.zip to a copy holding neocd.bin alone;
FBNeo also requires 000-lo.lo inside it. Members were skipped by the
claims, so nothing contested the pin and RetroArch shipped an archive
the core cannot boot from."""
def setUp(self):
import hashlib
import zipfile
import zlib
roms = {"neocd.bin": b"bios" * 8, "000-lo.lo": b"zoom" * 8}
entries = {}
self.paths = {}
for label, members in (("short", ["neocd.bin"]), ("full", list(roms))):
path = Path(f"bios/Arc/{label}/neocdz.zip")
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as zf:
for member in members:
zf.writestr(member, roms[member])
data = path.read_bytes()
entries[hashlib.sha1(data).hexdigest()] = {
"path": str(path), "md5": hashlib.md5(data).hexdigest(), "size": len(data),
}
self.paths[label] = (str(path), hashlib.md5(data).hexdigest())
self.addCleanup(shutil.rmtree, "bios/Arc", True)
self.db = _db(entries)
self.profiles = {"fbneo": {
"emulator": "FBNeo", "type": "libretro", "systems": ["snk-neogeo-cd"],
"archive_prefix": "fbneo",
"files": [
{"name": name, "archive": "neocdz.zip", "system": "snk-neogeo-cd",
"required": True, "crc32": format(zlib.crc32(data), "08x")}
for name, data in roms.items()
],
}}
def _config(self, mode: str, pinned: str) -> dict:
return {
"platform": "P", "verification_mode": mode, "base_destination": "system",
"cores": ["fbneo"],
"systems": {"snk-neogeo-cd": {"files": [{
"name": "neocdz.zip", "destination": "fbneo/neocdz.zip",
"md5": self.paths[pinned][1],
}]}},
}
def test_existence_serves_the_copy_the_core_boots_from(self):
overrides = slots.pack_overrides(self._config("existence", "short"), self.profiles, self.db)
self.assertEqual(overrides, {"system/fbneo/neocdz.zip": self.paths["full"][0]})
def test_a_content_check_keeps_the_pin_and_reports_it(self):
config = self._config("md5", "short")
self.assertEqual(slots.pack_overrides(config, self.profiles, self.db), {})
conflicts = slots.find_conflicts(config, self.profiles, self.db, "system")
self.assertEqual([c.destination for c in conflicts], ["system/fbneo/neocdz.zip"])
def test_a_pin_holding_the_members_is_agreement(self):
config = self._config("md5", "full")
self.assertEqual(slots.find_conflicts(config, self.profiles, self.db, "system"), [])
if __name__ == "__main__":
unittest.main()