"""An archive declared once per member ships as the copy holding them all. Batocera checks adam_fdc.zip through eight zipped_file declarations. The first hash-exact answer was a one-member MAME set, so the pack shipped it and seven of the eight members Batocera checks were missing. """ from __future__ import annotations import hashlib import os import shutil import sys import tempfile import unittest import zipfile import zlib from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) class PreferredArchive(unittest.TestCase): def test_the_archive_most_declarations_accept_wins(self): import generate_pack roms = {f"r{i}.bin": bytes([i]) * 32 for i in range(3)} with tempfile.TemporaryDirectory() as tmp: previous = os.getcwd() os.chdir(tmp) self.addCleanup(os.chdir, previous) Path("bios/a").mkdir(parents=True) Path("bios/b").mkdir(parents=True) with zipfile.ZipFile("bios/a/set.zip", "w") as zf: zf.writestr("r0.bin", roms["r0.bin"]) with zipfile.ZipFile("bios/b/set.zip", "w") as zf: for name, data in roms.items(): zf.writestr(name, data) files, by_md5, by_name = {}, {}, {} for path in ("bios/a/set.zip", "bios/b/set.zip"): data = Path(path).read_bytes() sha1, md5 = hashlib.sha1(data).hexdigest(), hashlib.md5(data).hexdigest() files[sha1] = {"path": path, "name": "set.zip", "sha1": sha1, "md5": md5, "size": len(data)} by_md5[md5] = sha1 by_name.setdefault("set.zip", []).append(sha1) db = {"files": files, "indexes": {"by_name": by_name, "by_md5": by_md5, "by_path_suffix": {}, "by_crc32": {}}} member_md5 = {n: hashlib.md5(d).hexdigest() for n, d in roms.items()} one_member = files[by_name["set.zip"][0]]["md5"] declarations = [{"name": "set.zip", "destination": "set.zip", "md5": one_member}] declarations += [ {"name": "set.zip", "destination": "set.zip", "zipped_file": name, "md5": md5} for name, md5 in member_md5.items() ] systems = {"s": {"files": declarations}} preferred = generate_pack._preferred_entries( systems, db, "bios", "", False, {}, True) chosen = next(fe for fe in declarations if id(fe) == preferred["set.zip"]) path, _status = generate_pack.resolve_file(chosen, db, "bios", {}, offline=True) self.assertEqual(path, "bios/b/set.zip") class OneWinnerRuleForEveryReader(unittest.TestCase): """verify passed the data-directory registry to the winner rule, the pack and the manifest passed none: a declaration only a data/ cache satisfies won in the report and lost in the pack. The rule takes no registry.""" def test_the_rule_reads_no_data_directory(self): import inspect # noqa: PLC0415 import generate_pack # noqa: PLC0415 parameters = inspect.signature(generate_pack._preferred_entries).parameters self.assertFalse([name for name in parameters if "registry" in name]) class IntegrityChecksEveryMember(unittest.TestCase): """A pack holding one ROM of an archive declared per ROM fails its check. The check accepted the destination as soon as one declaration passed, so a Batocera pack missing seven of adam_fdc.zip's eight ROMs verified. """ def test_a_missing_member_is_an_error_when_the_repository_holds_it(self): import yaml import packverify roms = {f"r{i}.bin": bytes([i]) * 32 for i in range(3)} with tempfile.TemporaryDirectory() as tmp: previous = os.getcwd() os.chdir(tmp) self.addCleanup(os.chdir, previous) Path("bios/b").mkdir(parents=True) with zipfile.ZipFile("bios/b/set.zip", "w") as zf: for name, data in roms.items(): zf.writestr(name, data) data = Path("bios/b/set.zip").read_bytes() sha1 = hashlib.sha1(data).hexdigest() db = {"files": {sha1: {"path": "bios/b/set.zip", "name": "set.zip", "sha1": sha1, "md5": hashlib.md5(data).hexdigest(), "size": len(data)}}, "indexes": {"by_name": {"set.zip": [sha1]}, "by_md5": {}, "by_path_suffix": {}, "by_crc32": {}}} Path("platforms").mkdir() config = {"platform": "P", "verification_mode": "md5", "base_destination": "bios", "cores": [], "systems": {"s": {"files": [ {"name": "set.zip", "destination": "set.zip", "zipped_file": n, "md5": hashlib.md5(d).hexdigest()} for n, d in roms.items()]}}} Path("platforms/p.yml").write_text(yaml.safe_dump(config), encoding="utf-8") Path("emulators").mkdir() import io inner = io.BytesIO() with zipfile.ZipFile(inner, "w") as zf: zf.writestr("r0.bin", roms["r0.bin"]) with zipfile.ZipFile("pack.zip", "w") as zf: zf.writestr("bios/set.zip", inner.getvalue()) ok, *_rest, = packverify.verify_pack_against_platform( "pack.zip", "p", "platforms", db, emulators_dir="emulators", emu_profiles={}) errors = _rest[2] self.assertFalse(ok) self.assertEqual(sorted(e.split(": ")[1] for e in errors), ["r1.bin not found inside ZIP", "r2.bin not found inside ZIP"]) class TheArchiveHoldingTheCoreMembers(unittest.TestCase): """FBNeo reads neocd.bin and 000-lo.lo inside neocdz.zip. The primary copy has no 000-lo.lo; a variant holds both. Resolved by name, every pack shipped the primary at fbneo/neocdz.zip and the core could not start a Neo Geo CD game.""" def setUp(self): directory = tempfile.mkdtemp(dir=REPO_ROOT / "tmp") self.addCleanup(shutil.rmtree, directory, True) root = Path(directory) self.roms = {"neocd.bin": b"bios" * 8, "000-lo.lo": b"zoom" * 8} files, by_name = {}, {} for path, members in ( (root / "bios/neocdz.zip", ["neocd.bin"]), (root / "bios/.variants/neocdz.zip.ba20a7be", ["neocd.bin", "000-lo.lo"]), ): path.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(path, "w") as zf: for member in members: zf.writestr(member, self.roms[member]) sha1 = hashlib.sha1(path.read_bytes()).hexdigest() files[sha1] = {"path": str(path), "name": "neocdz.zip", "sha1": sha1} by_name.setdefault("neocdz.zip", []).append(sha1) self.db = {"files": files, "indexes": {"by_name": by_name, "by_md5": {}, "by_path_suffix": {}, "by_crc32": {}}} self.profile_files = [ {"name": name, "archive": "neocdz.zip", "required": True, "crc32": format(zlib.crc32(data), "08x")} for name, data in self.roms.items() ] def test_the_copy_with_every_required_member_wins(self): from common import archive_entry, resolve_local_file path, status = resolve_local_file( archive_entry("neocdz.zip", self.profile_files), self.db ) self.assertTrue(path.endswith(".variants/neocdz.zip.ba20a7be")) self.assertEqual(status, "name_exact") def test_without_members_the_primary_stays(self): from common import resolve_local_file path, _status = resolve_local_file({"name": "neocdz.zip"}, self.db) self.assertTrue(path.endswith("bios/neocdz.zip")) def test_the_extras_carry_the_members(self): from packextras import _report_extra members = [{"crc32": "5a86cff2", "required": True}] extra = _report_extra( {"name": "neocdz.zip", "archive": "neocdz.zip", "members": members}, "neocdz.zip", "fbneo/neocdz.zip", ) self.assertEqual(extra["members"], members) def test_the_collection_holds_each_archive_a_core_requires(self): """FBNeo's nmk004.zip wants nmk004.bin crc 8ae61a09; both copies held MAME's 83b6f611 dump while the FBNeo dump sat loose in bios/.""" import json from common import archive_members, load_emulator_profiles, zip_member_crcs database = REPO_ROOT / "database.json" if not database.exists(): self.skipTest("database.json is not generated") db = json.loads(database.read_text()) by_name = db["indexes"]["by_name"] short = [] for emu, profile in load_emulator_profiles(str(REPO_ROOT / "emulators")).items(): entries = [f for f in profile.get("files") or [] if not f.get("unsourceable")] for archive in sorted({f["archive"] for f in entries if f.get("archive")}): required = { m["crc32"] for m in archive_members(archive, entries) if m["required"] } copies = [ REPO_ROOT / db["files"][sha1]["path"] for sha1 in by_name.get(archive, []) if sha1 in db["files"] ] if required and copies and not any( required <= zip_member_crcs(str(path)) for path in copies ): short.append(f"{emu}: {archive}") self.assertEqual(short, []) def test_verify_and_the_pack_read_one_entry(self): for module in ("verify.py", "generate_pack.py"): source = (REPO_ROOT / "scripts" / module).read_text() with self.subTest(module=module): self.assertIn("archive_entry(", source) self.assertNotIn('{"name": archive', source) if __name__ == "__main__": unittest.main()