diff --git a/install/recalbox.json b/install/recalbox.json index 4ec559c0..7ed532d2 100644 --- a/install/recalbox.json +++ b/install/recalbox.json @@ -238,9 +238,9 @@ } ], "total_files": 3378, - "total_size": 4262186697, + "total_size": 4263070113, "pack_files": 3380, - "pack_size": 4262186697, + "pack_size": 4263070113, "total_omitted": 1, "omitted_files": [ { @@ -5318,10 +5318,10 @@ }, { "dest": "dc/hod2bios.zip", - "sha1": "0b5cc968644c936d4819c1eb5f623f5f3f83eda1", - "sha256": "8ae5a99e4ffc15b92e4d590c9e14558743e8f2068947a23e942dfc038ca326c6", - "size": 1465991, - "repo_path": "bios/Arcade/Arcade/.variants/hod2bios.zip.0b5cc968", + "sha1": "9b9c9f0af3de914de677ba8e19f4190673142c1b", + "sha256": "c90c754f36d4ab0c927da1f79883cb322a4f94b7cebe9cc7f401151a1b8f1a5a", + "size": 1057881, + "repo_path": "bios/Arcade/Arcade/hod2bios.zip", "cores": null, "systems": [ "sega-dreamcast-arcade" @@ -5340,10 +5340,10 @@ }, { "dest": "dc/naomigd.zip", - "sha1": "f9ad4a4c6b0bbbe39ba358690a48f763ecbd98f0", - "sha256": "1280f4185ac4f7352ab85e49d71db1ca0b3e679c6430bd2c8545088042117eb1", - "size": 3371765, - "repo_path": "bios/Arcade/Arcade/.variants/naomigd.zip", + "sha1": "4b92936f396c8199f745d3ed0ff588a324b8dd70", + "sha256": "793db2a859be2d814d334ab511811c42c6727a16841e7627fa7c33171c3de441", + "size": 4972898, + "repo_path": "bios/Arcade/Arcade/naomigd.zip", "cores": null, "systems": [ "sega-dreamcast-arcade" @@ -5892,10 +5892,10 @@ }, { "dest": "neocdz.zip", - "sha1": "7bb26d1e5d1e930515219cb18bcde5b7b23e2eda", - "sha256": "2e93af5848080ea04d17a7841b742f009330d30e4ff40c3410547581d921c892", - "size": 524288, - "repo_path": "bios/SNK/Neo Geo CD/neocd.bin", + "sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc", + "sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667", + "size": 214681, + "repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31", "cores": null, "systems": [ "snk-neogeo-cd" diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 95dbfd57..4b484ae5 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -415,6 +415,8 @@ platforms: source_url: https://gitlab.com/recalbox/recalbox/-/raw/master/board/recalbox/fsoverlay/recalbox/share_init/system/.emulationstation/es_bios.xml source_format: xml hash_type: md5 + # Bios.cpp:64-68 hashes a .zip with Zip::Md5Composite, never its bytes + zip_digest: composite schedule: monthly target_scraper: null target_source: null diff --git a/scripts/common.py b/scripts/common.py index bc819fd8..fd215746 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -197,6 +197,22 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - if key != "cores" and key not in config: config[key] = val + # Recalbox hashes a .zip by its members, so a plain md5 never answers one + # of its archive declarations: the resolver reads the mark per entry. + if config.get("zip_digest") == "composite": + config["systems"] = { + sys_id: { + **system, + "files": [ + {**f, "zip_digest": "composite"} + if str(f.get("name", "")).lower().endswith(".zip") + else f + for f in system.get("files", []) + ], + } + for sys_id, system in config.get("systems", {}).items() + } + _platform_config_cache[cache_key] = config return config @@ -848,7 +864,12 @@ def resolve_local_file( # .variants/ pattern: filename like "neogeo.zip.fc398ab4" return any(bn.startswith(n + ".") for n in _name_set) - if md5_list and not zipped_file: + # A frontend that hashes an archive by its members (Recalbox) never reads + # the container's own md5: neocd.bin's md5 is also the composite of a + # one-member neocdz.zip, and the raw ROM shipped under the archive's name. + composite_only = file_entry.get("zip_digest") == "composite" + + if md5_list and not zipped_file and not composite_only: for md5_candidate in md5_list: sha1_match = by_md5.get(md5_candidate) if sha1_match and sha1_match in files_db: @@ -1014,6 +1035,8 @@ def resolve_local_file( if candidates: if md5_set and not (sha1_candidates or sha256_candidates or crc_candidates): + if composite_only: + candidates.sort(key=lambda c: "/.variants/" in c[0]) for path, db_md5, _size in candidates: if ".zip" in os.path.basename(path): try: @@ -1022,7 +1045,7 @@ def resolve_local_file( return path, "md5_composite_exact" except (zipfile.BadZipFile, OSError): pass - if db_md5.lower() in md5_set: + if db_md5.lower() in md5_set and not composite_only: return path, "md5_exact" # When zipped_file is set, only accept candidates that contain it if zipped_file: diff --git a/tests/test_archive_choice.py b/tests/test_archive_choice.py index 9375f7fd..3c6a6774 100644 --- a/tests/test_archive_choice.py +++ b/tests/test_archive_choice.py @@ -218,5 +218,56 @@ class TheArchiveHoldingTheCoreMembers(unittest.TestCase): self.assertNotIn('{"name": archive', source) +class AnArchiveHashedByItsMembers(unittest.TestCase): + """Recalbox hashes a .zip with Zip::Md5Composite (Bios.cpp:64-68). Its + neocdz.zip pin is neocd.bin's md5, which is also the composite of a + one-member archive, and the md5 step served the raw ROM under the + archive's name: Recalbox cannot open it and reports the BIOS red.""" + + def setUp(self): + directory = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, directory, True) + rom = b"bios" * 8 + raw = directory / "bios/neocd.bin" + archive = directory / "bios/.variants/neocdz.zip.4f28af31" + archive.parent.mkdir(parents=True) + raw.write_bytes(rom) + with zipfile.ZipFile(archive, "w") as zf: + zf.writestr("neocd.bin", rom) + files, by_md5, by_name = {}, {}, {} + for path, name in ((raw, "neocd.bin"), (archive, "neocdz.zip")): + data = path.read_bytes() + sha1, md5 = hashlib.sha1(data).hexdigest(), hashlib.md5(data).hexdigest() + files[sha1] = {"path": str(path), "name": name, "sha1": sha1, "md5": md5} + by_md5[md5] = sha1 + by_name.setdefault(name, []).append(sha1) + self.db = {"files": files, "indexes": {"by_name": by_name, "by_md5": by_md5, + "by_path_suffix": {}, "by_crc32": {}}} + self.entry = {"name": "neocdz.zip", "md5": hashlib.md5(rom).hexdigest()} + self.archive = str(archive) + + def test_the_archive_answers_a_composite_frontend(self): + from common import resolve_local_file + + path, status = resolve_local_file({**self.entry, "zip_digest": "composite"}, self.db) + self.assertEqual((path, status), (self.archive, "md5_composite_exact")) + + def test_a_plain_digest_still_takes_the_md5_match(self): + from common import resolve_local_file + + path, _status = resolve_local_file(self.entry, self.db) + self.assertTrue(path.endswith("neocd.bin")) + + def test_the_registry_marks_recalbox_archives(self): + from common import load_platform_config + + config = load_platform_config("recalbox", str(REPO_ROOT / "platforms")) + marks = { + (f["name"].lower().endswith(".zip"), f.get("zip_digest")) + for system in config["systems"].values() for f in system["files"] + } + self.assertEqual(marks, {(True, "composite"), (False, None)}) + + if __name__ == "__main__": unittest.main()