mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-28 14:47:21 -05:00
feat: check archives against their set recipes
A held archive whose name has a recipe fails when it carries three or more members outside every recipe of that name. Exempt: containers a platform pins by md5, members a platform pins by zipped_file, merged clones in their own folder, NAOMI device_ref ROMs, samples and artwork.
This commit is contained in:
1 parent
6c00caeaca
commit
1abb8f678f
1 file changed
+143
@@ -0,0 +1,143 @@
|
||||
"""A held romset archive carries the members of its own set and nothing else.
|
||||
|
||||
The MAME device sets added in March 2026 each held the ROMs of every slot
|
||||
option reachable from the device: sys68k_cpu30.zip carried 33 members of the
|
||||
H19 terminal and other cards next to its five CPU-30 ROMs, slvrball.zip 371
|
||||
members of ISA cards and other slot devices. MAME reads a slot card from the card's own archive, so
|
||||
those members were never read there, and the archive matched no recipe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
import unittest
|
||||
import zipfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
# Sets whose archive also carries the ROMs of devices the machine references
|
||||
# (listxml device_ref): the NAOMI boards pull the JVS I/O board and the Maple
|
||||
# interface, and MAME finds those ROMs in the machine's own archive too.
|
||||
DEVICE_ROMS = {
|
||||
"airlbios.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"f355bios.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"f355dlx.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"hod2bios.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"naomi.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"naomi2.zip": ("jvs13551.zip", "mie.zip"),
|
||||
"naomigd.zip": ("jvs13551.zip", "mie.zip"),
|
||||
}
|
||||
|
||||
# Archives that share a set name without being that set.
|
||||
NOT_ROMSETS = {"samples", "artwork"}
|
||||
|
||||
# The defect added whole slot-option trees, dozens of members at a time.
|
||||
MIN_EXTRA = 3
|
||||
|
||||
# Kept as they are: rewriting them would drop bytes the collection holds
|
||||
# nowhere else, and no recipe names the set those bytes belong to.
|
||||
KEPT = {
|
||||
"bios/Acorn/BBC Micro/.variants/bbcb.zip": "zdfs-0.90.rom is held only here",
|
||||
}
|
||||
|
||||
|
||||
def _hashes(values: object) -> set[str]:
|
||||
raw = values if isinstance(values, list) else str(values or "").split(",")
|
||||
return {str(value).strip().lower() for value in raw if str(value).strip()}
|
||||
|
||||
|
||||
class HeldArchivesMatchTheirRecipes(unittest.TestCase):
|
||||
def test_no_member_outside_every_recipe_of_the_name(self):
|
||||
bios = REPO_ROOT / "bios"
|
||||
if not bios.is_dir() or not (REPO_ROOT / "recipes").is_dir():
|
||||
self.skipTest("bios/ or recipes/ is not present")
|
||||
from common import ( # noqa: PLC0415
|
||||
list_registered_platforms,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
variant_base_name,
|
||||
)
|
||||
from hashing import md5_composite_of # noqa: PLC0415
|
||||
from romset_recipes import load_dat_recipes # noqa: PLC0415
|
||||
|
||||
allowed: dict[str, set[str]] = defaultdict(set)
|
||||
for archive, per_label in load_dat_recipes(REPO_ROOT / "recipes").items():
|
||||
for recipe in per_label.values():
|
||||
allowed[archive.lower()] |= {crc for _name, crc in recipe}
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
for profile in profiles.values():
|
||||
for entry in profile.get("files", []) or []:
|
||||
crcs = {
|
||||
str(member.get("crc32") or "").lower()
|
||||
for member in entry.get("contents") or []
|
||||
}
|
||||
crcs.discard("")
|
||||
if not crcs:
|
||||
continue
|
||||
for name in [entry.get("name", "")] + list(entry.get("aliases") or []):
|
||||
if name:
|
||||
allowed[str(name).lower()] |= crcs
|
||||
for name, devices in DEVICE_ROMS.items():
|
||||
for device in devices:
|
||||
allowed[name] |= allowed.get(device, set())
|
||||
|
||||
pinned: set[str] = set()
|
||||
zipped: dict[str, set[tuple[str, str]]] = defaultdict(set)
|
||||
platforms_dir = str(REPO_ROOT / "platforms")
|
||||
for platform in list_registered_platforms(platforms_dir, include_archived=True):
|
||||
config = load_platform_config(platform, platforms_dir)
|
||||
for system in config.get("systems", {}).values():
|
||||
for entry in system.get("files", []) or []:
|
||||
if entry.get("zipped_file"):
|
||||
for md5 in _hashes(entry.get("md5")):
|
||||
zipped[str(entry.get("name", "")).lower()].add(
|
||||
(str(entry["zipped_file"]).casefold(), md5)
|
||||
)
|
||||
continue
|
||||
for key in ("md5", "sha1", "alt_md5"):
|
||||
pinned |= _hashes(entry.get(key))
|
||||
|
||||
offenders = []
|
||||
for path in sorted(bios.rglob("*")):
|
||||
name = variant_base_name(path.name).lower()
|
||||
if not path.is_file() or name not in allowed or not name.endswith(".zip"):
|
||||
continue
|
||||
if path.parent.name in NOT_ROMSETS:
|
||||
continue
|
||||
stem = name[:-4]
|
||||
try:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
extra = [
|
||||
info for info in archive.infolist()
|
||||
if not info.is_dir()
|
||||
and f"{info.CRC:08x}" not in allowed[name]
|
||||
# a merged set keeps each clone in a folder of its name
|
||||
and not ("/" in info.filename and info.filename.split("/")[0] != stem)
|
||||
]
|
||||
extra = [
|
||||
info for info in extra
|
||||
if (info.filename.casefold(),
|
||||
hashlib.md5(archive.read(info)).hexdigest()) not in zipped[name]
|
||||
]
|
||||
except zipfile.BadZipFile:
|
||||
continue
|
||||
if len(extra) < MIN_EXTRA or str(path.relative_to(REPO_ROOT)) in KEPT:
|
||||
continue
|
||||
blob = path.read_bytes()
|
||||
container = {hashlib.md5(blob).hexdigest(), hashlib.sha1(blob).hexdigest(),
|
||||
md5_composite_of(path)}
|
||||
if container & pinned:
|
||||
continue
|
||||
offenders.append(
|
||||
f"{path.relative_to(REPO_ROOT)}: {len(extra)} members outside every "
|
||||
f"{name} recipe ({', '.join(info.filename for info in extra[:4])})"
|
||||
)
|
||||
self.assertEqual(offenders, [], "\n".join(offenders))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user