mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
fix: keep recalbox composite md5 on archives
Bios.cpp:64-68 hashes a .zip with Zip::Md5Composite, the md5 of its members in name order. A profile's md5 on an archive is the container's, so it neither pairs with, corrects nor fills that field, and an archive addition with only a container md5 is left out.
This commit is contained in:
1 parent
f796155e1a
commit
fbf7cb191b
4 files changed
+97
-41
No files matched your search
+3
-35
@@ -7,11 +7,11 @@
|
||||
"files": [
|
||||
"es_bios.xml"
|
||||
],
|
||||
"summary": "357 kept, 88 added, 4 hashes corrected, 129 requirements corrected, 527 additions left out, 102 entries found by content not listed",
|
||||
"summary": "357 kept, 87 added, 2 hashes corrected, 129 requirements corrected, 528 additions left out, 102 entries found by content not listed",
|
||||
"counts": {
|
||||
"kept": 357,
|
||||
"added": 88,
|
||||
"not_added": 527,
|
||||
"added": 87,
|
||||
"not_added": 528,
|
||||
"not_kept": 0
|
||||
},
|
||||
"changes": [
|
||||
@@ -102,18 +102,6 @@
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga600",
|
||||
"name": "boot-data.zip",
|
||||
"destination": "whdboot/boot-data.zip",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "9e638f3d531eb7cd82594f49a4cf03b7",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga600",
|
||||
@@ -1953,15 +1941,6 @@
|
||||
"old": true,
|
||||
"new": false
|
||||
},
|
||||
{
|
||||
"kind": "hash_corrected",
|
||||
"system": "neogeocd",
|
||||
"name": "neocdz.zip",
|
||||
"destination": "neocdz.zip",
|
||||
"field": "md5",
|
||||
"old": "f39572af7584cb5b3f70ae8cc848aba2,389dbeb0cb857c130414747462cf9d71,c85b8132799f1b2ad3754a97947809d2,b9ab07a79997fc044ca164fc72cd5680",
|
||||
"new": "b31a246b69493c35194fe0bac120dc37"
|
||||
},
|
||||
{
|
||||
"kind": "requirement_corrected",
|
||||
"system": "neogeocd",
|
||||
@@ -1971,15 +1950,6 @@
|
||||
"old": true,
|
||||
"new": false
|
||||
},
|
||||
{
|
||||
"kind": "hash_corrected",
|
||||
"system": "neogeocd",
|
||||
"name": "neocd.zip",
|
||||
"destination": "geolith/neocd.zip",
|
||||
"field": "md5",
|
||||
"old": "3711783a59ef875c31076512e3f9f0e5",
|
||||
"new": "006a881532f2caffc3fe9fb5593f2ed5"
|
||||
},
|
||||
{
|
||||
"kind": "requirement_corrected",
|
||||
"system": "neogeocd",
|
||||
@@ -2270,8 +2240,6 @@
|
||||
}
|
||||
],
|
||||
"hashes_corrected": [
|
||||
"neogeocd/neocdz.zip md5",
|
||||
"neogeocd/neocd.zip md5",
|
||||
"ti994a/TI-994A.ctg md5",
|
||||
"ti994a/ti-disk.ctg md5"
|
||||
],
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<bios path="whdboot/WHDLoad" md5="6670a9571f235ec31b8bbd48fc18f428" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/JST" md5="2a3a61bbd7c351751506e22a4354b76a" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/AmiQuit" md5="36ab26d3b855e634fdda062c052cd52b" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/boot-data.zip" md5="9e638f3d531eb7cd82594f49a4cf03b7" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/game-data/whdload_db.json" md5="14d797815aa9bbc79fe284b244ea75b1" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/save-data/Kickstarts/kick33180.A500.RTB" md5="c993feb1eeddec1d2e687aa514231028" core="libretro/amiberry" mandatory="false" />
|
||||
<bios path="whdboot/save-data/Kickstarts/kick34005.A500.RTB" md5="7eb13d974af0992d672b67c317a7e0a1" core="libretro/amiberry" mandatory="false" />
|
||||
@@ -616,7 +615,7 @@
|
||||
b4519df27a352c2a6e42e06d31330d91,bf00272e7150b31156a8995d60bf185d,
|
||||
ca66fd463ef7dcab492a6de8ce5f45eb,f4a125538dfd7a8044e025fc5188fb88,
|
||||
a3b2c9a69c28ad0bd4ea07877b744bbe,00dad01abdbf8ea9e79ad2fe11bdb182" hashMatchMandatory="false" core="libretro/fbneo,libretro/fba2x,libretro/mame2000,libretro/mame2003,libretro/mame2003-plus,libretro/mame2010,libretro/mame2015" />
|
||||
<bios path="neocdz.zip" md5="b31a246b69493c35194fe0bac120dc37" hashMatchMandatory="false" core="libretro/fbneo,libretro/fba2x,libretro/mame2000,libretro/mame2003,libretro/mame2003-plus,libretro/mame2010,libretro/mame2015" />
|
||||
<bios path="neocdz.zip" md5="f39572af7584cb5b3f70ae8cc848aba2,389dbeb0cb857c130414747462cf9d71,c85b8132799f1b2ad3754a97947809d2,b9ab07a79997fc044ca164fc72cd5680" hashMatchMandatory="false" core="libretro/fbneo,libretro/fba2x,libretro/mame2000,libretro/mame2003,libretro/mame2003-plus,libretro/mame2010,libretro/mame2015" />
|
||||
<bios path="neocd/neocd.bin" md5="f39572af7584cb5b3f70ae8cc848aba2" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="CDZ BIOS (MAME)" />
|
||||
<bios path="neocd/uni-bioscd.rom" md5="08ca8b2dba6662e8024f9e789711c6fc" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="Universe BIOS CD 3.3" />
|
||||
<bios path="neocd/top-sp1.bin" md5="122aee210324c72e8a11116e6ef9c0d0" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="Top Loader BIOS (MAME)" />
|
||||
@@ -629,7 +628,7 @@
|
||||
<bios path="neocd/neocd_f.rom" md5="8834880c33164ccbe6476b559f3e37de" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="Front Loader BIOS" />
|
||||
<bios path="neocd/000-lo.lo" md5="fc7599f3f871578fe9a0453662d1c966" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="ZOOM Rom" />
|
||||
<bios path="neocd/ng-lo.rom" md5="e255264d85d5765013b1b2fa8109dd53" core="libretro/neocd" mandatory="false" hashMatchMandatory="false" note="ZOOM Rom" />
|
||||
<bios path="geolith/neocd.zip" md5="006a881532f2caffc3fe9fb5593f2ed5" core="libretro/geolith" mandatory="false" hashMatchMandatory="false" note="Neo-Geo CD Bios" />
|
||||
<bios path="geolith/neocd.zip" md5="3711783a59ef875c31076512e3f9f0e5" core="libretro/geolith" mandatory="false" hashMatchMandatory="false" note="Neo-Geo CD Bios" />
|
||||
<bios path="geolith/neocdz.zip" md5="724bc64e63d073cf81f986adfb1f1b78" core="libretro/geolith" mandatory="true" hashMatchMandatory="false" note="Neo-Geo CDZ Bios" />
|
||||
</system>
|
||||
<system fullname="Texas Instrument TI-99/4A" platform="ti994a">
|
||||
|
||||
@@ -61,6 +61,20 @@ def checked_hashes(entry: dict | None) -> dict | None:
|
||||
return {k: v for k, v in entry.items() if k not in HASH_FIELDS or k in kinds}
|
||||
|
||||
|
||||
def comparable(entry: dict | None) -> dict:
|
||||
"""The entry with only the hashes the truth can be compared with.
|
||||
|
||||
Recalbox hashes a .zip by its members (Bios.cpp:64-68 calls
|
||||
Zip::Md5Composite, which feeds each member's bytes in name order into
|
||||
one md5), never by the container's bytes. Its md5 on an archive is that
|
||||
composite, a digest no profile declares: read as a container md5 it was
|
||||
replaced by geolith's, which Recalbox never computes.
|
||||
"""
|
||||
if not entry or entry.get("zip_digest") != "composite":
|
||||
return entry or {}
|
||||
return {k: v for k, v in entry.items() if k != "md5"}
|
||||
|
||||
|
||||
def is_label(entry: dict) -> bool:
|
||||
"""A name only content-scanning cores give: no core opens that file.
|
||||
|
||||
@@ -104,6 +118,10 @@ class NativeFile:
|
||||
# For an addition, each core that wants it as (runs standalone, name the
|
||||
# platform gives it), empty when no core the platform runs is named.
|
||||
runners: list[tuple[bool, str]] = field(default_factory=list)
|
||||
# The platform reads this file's md5 as a composite of its members
|
||||
# (Recalbox on a .zip): the truth's md5 describes the container and
|
||||
# can be neither compared with it nor written in its place.
|
||||
composite_md5: bool = False
|
||||
|
||||
@property
|
||||
def origin(self) -> str:
|
||||
@@ -129,7 +147,9 @@ class NativeFile:
|
||||
truth's crc32 beside Batocera's md5 for bios7.bin is one dump, and
|
||||
reading it as a contradiction emptied the md5 the frontend checks.
|
||||
"""
|
||||
truth, plat = _field_sets(self._checked_truth), _field_sets(self.platform)
|
||||
truth, plat = _field_sets(self._checked_truth), _field_sets(comparable(self.platform))
|
||||
if self.composite_md5:
|
||||
truth["md5"] = set()
|
||||
if not any(truth.values()):
|
||||
return "platform"
|
||||
if not any(plat.values()):
|
||||
@@ -152,6 +172,8 @@ class NativeFile:
|
||||
the divergence is recorded, never silently merged: an emulator that
|
||||
rejects a file will reject it whatever the platform declares.
|
||||
"""
|
||||
if field_name == "md5" and self.composite_md5:
|
||||
return _hash_values(self.platform or {}, field_name)
|
||||
truth_values = _hash_values(self._checked_truth, field_name)
|
||||
platform_values = _hash_values(self.platform or {}, field_name)
|
||||
source = self._identity_source()
|
||||
@@ -186,6 +208,9 @@ class NativeFile:
|
||||
chosen among those the truth accepts. A field only the truth names
|
||||
is written when it names one value, which is then that dump's.
|
||||
"""
|
||||
if field_name == "md5" and self.composite_md5:
|
||||
values = _hash_values(self.platform or {}, field_name)
|
||||
return values[0] if values else ""
|
||||
source = self._identity_source()
|
||||
truth_values = _hash_values(self._checked_truth, field_name)
|
||||
platform_values = _hash_values(self.platform or {}, field_name)
|
||||
@@ -365,11 +390,13 @@ def by_hash(truth_entry: dict, platform_entry: dict) -> bool:
|
||||
ours = {v for f in HASH_FIELDS for v in _hash_values(truth_entry, f)}
|
||||
if not ours:
|
||||
return False
|
||||
return bool(ours & {v for f in HASH_FIELDS for v in _hash_values(platform_entry, f)})
|
||||
theirs = comparable(platform_entry)
|
||||
return bool(ours & {v for f in HASH_FIELDS for v in _hash_values(theirs, f)})
|
||||
|
||||
|
||||
def _contradicts(truth_entry: dict, platform_entry: dict) -> bool:
|
||||
"""A hash both entries declare, without a value in common."""
|
||||
platform_entry = comparable(platform_entry)
|
||||
return any(
|
||||
(ours := set(_hash_values(truth_entry, f)))
|
||||
and (theirs := set(_hash_values(platform_entry, f)))
|
||||
@@ -445,6 +472,7 @@ def build_native_model(
|
||||
systems: dict[str, NativeSystem] = {}
|
||||
|
||||
scraped_systems = (scraped or {}).get("systems", {})
|
||||
composite_zips = (scraped or {}).get("zip_digest") == "composite"
|
||||
|
||||
# Pass 1: the platform's own file, grouped as the platform groups it.
|
||||
for sys_key, sys_data in scraped_systems.items():
|
||||
@@ -467,6 +495,7 @@ def build_native_model(
|
||||
destination=destination,
|
||||
native_system=native_id,
|
||||
platform=file_entry,
|
||||
composite_md5=file_entry.get("zip_digest") == "composite",
|
||||
)
|
||||
)
|
||||
report.files_kept += 1
|
||||
@@ -545,7 +574,9 @@ def build_native_model(
|
||||
checked = checked_hashes(truth_entry) or {}
|
||||
for field_name in HASH_FIELDS:
|
||||
ours = set(_hash_values(checked, field_name))
|
||||
theirs = set(_hash_values(matched.platform or {}, field_name))
|
||||
if field_name == "md5" and matched.composite_md5:
|
||||
continue
|
||||
theirs = set(_hash_values(comparable(matched.platform), field_name))
|
||||
if ours and theirs and not ours & theirs:
|
||||
matched.corrections.append(field_name)
|
||||
report.hashes_corrected.append(
|
||||
@@ -602,6 +633,7 @@ def build_native_model(
|
||||
native_system=native_id,
|
||||
truth=truth_entry,
|
||||
foreign_system=native_id not in platform_ids,
|
||||
composite_md5=composite_zips and name.lower().endswith(".zip"),
|
||||
)
|
||||
)
|
||||
report.files_added += 1
|
||||
|
||||
@@ -43,6 +43,7 @@ from export_native import ( # noqa: E402
|
||||
SOURCES_INDEX,
|
||||
collect_originals,
|
||||
fetch,
|
||||
landed_changes,
|
||||
pinned_base,
|
||||
source_key,
|
||||
)
|
||||
@@ -1084,6 +1085,62 @@ class RecalboxExport(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(blocks, {pentagon: ["3" * 32], scorpion: [new]})
|
||||
|
||||
COMPOSITE_SCRAPED = {
|
||||
"zip_digest": "composite",
|
||||
"systems": {"snk-neogeo-cd": {"native_id": "neogeocd", "files": [{
|
||||
"name": "neocdz.zip", "destination": "neocdz.zip", "md5": "a" * 32,
|
||||
"native_system": "neogeocd", "core": "libretro/fbneo",
|
||||
"required": True, "zip_digest": "composite",
|
||||
}]}},
|
||||
}
|
||||
|
||||
def test_a_composite_md5_is_not_replaced_by_a_container_md5(self):
|
||||
"""Recalbox hashes a .zip by its members (Bios.cpp:64-68,
|
||||
Zip::Md5Composite): the truth's container md5 is another digest."""
|
||||
truth = {"systems": {"snk-neogeo-cd": {"files": [
|
||||
{"name": "neocdz.zip", "md5": "b" * 32, "required": True, "_cores": ["geolith"]},
|
||||
{"name": "boot-data.zip", "md5": "c" * 32, "required": False, "_cores": ["geolith"]},
|
||||
]}}}
|
||||
systems, report = build_native_model(truth, self.COMPOSITE_SCRAPED)
|
||||
exporter = discover_exporters()["recalbox"]()
|
||||
produced = exporter.render(systems, report, {}, None)["es_bios.xml"]
|
||||
md5_of = {b.get("path"): b.get("md5") for b in ET.fromstring(produced).iter("bios")}
|
||||
self.assertEqual(md5_of, {"neocdz.zip": "a" * 32})
|
||||
self.assertEqual(landed_changes(exporter, systems).records, [])
|
||||
|
||||
def test_a_container_md5_never_pairs_with_a_composite(self):
|
||||
"""neocd.bin's md5 equals the composite of a neocdz.zip: by hash the
|
||||
two would be read as one renamed file."""
|
||||
truth = {"systems": {"snk-neogeo-cd": {"files": [
|
||||
{"name": "neocd.bin", "md5": "a" * 32, "required": True, "_cores": ["neocd"]},
|
||||
]}}}
|
||||
systems, _ = build_native_model(truth, self.COMPOSITE_SCRAPED)
|
||||
declared = next(fe for s in systems.values() for fe in s.files if fe.platform)
|
||||
self.assertIsNone(declared.truth)
|
||||
|
||||
def test_the_live_export_keeps_every_zip_md5(self):
|
||||
"""No element of es_bios.xml names a .zip under a new md5: none can
|
||||
be stated as the composite Recalbox computes."""
|
||||
path = REPO_ROOT / "exports" / "recalbox" / "es_bios.xml"
|
||||
original = UPSTREAM_CACHE / "recalbox" / "es_bios.xml"
|
||||
if not original.is_file():
|
||||
self.skipTest("no cached original for recalbox")
|
||||
|
||||
def zips(text: str) -> dict[tuple[str, str], str]:
|
||||
return {
|
||||
(s.get("platform"), b.get("path")): ",".join(sorted(
|
||||
v.strip().lower() for v in b.get("md5", "").split(",")
|
||||
))
|
||||
for s in ET.fromstring(text).iter("system")
|
||||
for b in s.iter("bios")
|
||||
if b.get("path", "").lower().endswith(".zip")
|
||||
}
|
||||
|
||||
before = zips(original.read_text(encoding="utf-8"))
|
||||
after = zips(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual({k: v for k, v in after.items() if k in before}, before)
|
||||
self.assertEqual(set(after) - set(before), set())
|
||||
|
||||
def test_a_truncated_export_is_reported(self):
|
||||
exporter, systems, _ = self._render()
|
||||
empty = {
|
||||
|
||||
Reference in new issue
Block a user