mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
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.
144 lines
6.1 KiB
Python
144 lines
6.1 KiB
Python
"""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()
|