fix: keep one-value md5 blank when dumps differ

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 15:40:44 +02:00
1 parent 7cb98f7c92
commit 3a0ae494d3
4 files changed
+74 -16

No files matched your search

+13
View File
@@ -133,6 +133,19 @@ class BaseExporter(ABC):
require = require or cls.requires()
return not require or bool(fe.hash(require))
@staticmethod
def single_value(fe: NativeFile, field_name: str) -> str:
"""The one hash a single-value field is checked against.
A platform that leaves the field empty checks the file by existence.
Filling it turns that into a content check on one value: right when
the truth accepts exactly one image, wrong when the core takes several
and the field can hold only the first. Such a fill stays empty.
"""
if field_name in fe.filled and len(set(fe.hashes(field_name))) != 1:
return ""
return fe.hash(field_name)
def states(self, fe: NativeFile, field_name: str) -> bool:
"""Whether the written file carries this entry's corrected field.
+6 -1
View File
@@ -53,6 +53,11 @@ class Exporter(BaseExporter):
return False
return super().writable(fe, require)
def states(self, fe: NativeFile, field_name: str) -> bool:
if field_name == "md5" and "md5" in fe.filled:
return bool(self.single_value(fe, "md5"))
return super().states(fe, field_name)
@staticmethod
def native_sources() -> dict[str, str]:
return {"batocera-systems": SOURCE_URL}
@@ -89,7 +94,7 @@ class Exporter(BaseExporter):
for fe in files:
# The platform states an unhashed file as an empty md5 rather
# than leaving it out, and so do we.
item = [f'"md5": "{fe.hash("md5")}"']
item = [f'"md5": "{self.single_value(fe, "md5")}"']
alt = fe.native("alt_md5", "")
if alt:
item.append(f'"altmd5": "{alt}"')
+2 -15
View File
@@ -41,22 +41,9 @@ class Exporter(BaseExporter):
def native_sources() -> dict[str, str]:
return {"batocera-systems.json": SOURCE_URL}
@staticmethod
def _md5(fe: NativeFile) -> str:
"""The one md5 the entry is checked against, empty for existence.
RetroBat writes an empty md5 to check a file by existence. Filling it
turns that into a content check on a single value: right when the
truth accepts exactly one image, wrong when the core takes several
and the field can hold only the first.
"""
if "md5" in fe.filled and len(fe.hashes("md5")) != 1:
return ""
return fe.hash("md5")
def states(self, fe: NativeFile, field_name: str) -> bool:
if field_name == "md5" and "md5" in fe.filled:
return bool(self._md5(fe))
return bool(self.single_value(fe, "md5"))
return super().states(fe, field_name)
def render(
@@ -94,7 +81,7 @@ class Exporter(BaseExporter):
ours: OrderedDict[str, OrderedDict[str, str]] = OrderedDict()
for fe in files:
entry: OrderedDict[str, str] = OrderedDict()
entry["md5"] = self._md5(fe)
entry["md5"] = self.single_value(fe, "md5")
declared = fe.native("native_path", "")
entry["file"] = str(declared) if declared else f"bios/{fe.destination}"
ours.setdefault(entry["file"], entry)
+53
View File
@@ -663,6 +663,59 @@ class BatoceraExport(unittest.TestCase):
)
self.assertIn('"md5": ""', produced["batocera-systems"])
def test_a_blank_md5_is_not_filled_with_one_of_several_dumps(self):
"""checkBios compares one md5: the first of two refuses the other.
The empty field checks by existence, so a user holding the second
dump the core accepts would turn from OK to UNTESTED.
"""
first, second = "1" * 32, "2" * 32
truth = {
"systems": {
"nintendo-ds": {
"files": [
{
"name": "firmware.bin",
"path": "firmware.bin",
"md5": [first, second],
"required": True,
"_cores": ["melonds"],
}
]
}
}
}
scraped = {
"systems": {
"nintendo-ds": {
"native_id": "nds",
"files": [
{
"name": "firmware.bin",
"destination": "firmware.bin",
"native_system": "nds",
"md5": "",
}
],
}
}
}
for platform, filename in (
("batocera", "batocera-systems"),
("rocknix", "rocknix-systems"),
):
with self.subTest(platform=platform):
systems, report = build_native_model(truth, scraped)
(entry,) = systems["nds"].files
self.assertIn("md5", entry.filled)
exporter = discover_exporters()[platform]()
produced = exporter.render(
systems, report, {filename: self.SKELETON}, None
)
self.assertIn('"md5": ""', produced[filename])
self.assertNotIn(first, produced[filename])
self.assertFalse(exporter.states(entry, "md5"))
def test_writing_without_the_platform_file_is_refused(self):
"""A systems dict alone is not the script Batocera runs."""
systems, report = model()