fix: hash recalbox archives by their members

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 21:06:55 +02:00
1 parent 3e14c9df1f
commit 346950156c
4 files changed
+92 -16

No files matched your search

+14 -14
View File
@@ -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"
+2
View File
@@ -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
+25 -2
View File
@@ -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:
+51
View File
@@ -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()