From d9548e3522e2e47f2ee2495090dfc875dd145b9b Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Wed, 21 Oct 2026 05:14:44 +0200
Subject: [PATCH] fix: count a recalbox element kept as written
An element whose md5 the truth contradicts without naming another is
written back whole. It was counted as an entry the format cannot keep.
It now counts as kept, and the md5 it keeps is named as withheld.
---
scripts/exporter/recalbox_exporter.py | 28 +++++++++++++++++++++++++++
tests/test_exporters.py | 26 +++++++++++++++++++++++++
2 files changed, 54 insertions(+)
diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py
index 0647bcb8..490ae90b 100644
--- a/scripts/exporter/recalbox_exporter.py
+++ b/scripts/exporter/recalbox_exporter.py
@@ -222,6 +222,19 @@ class Exporter(BaseExporter):
return False
return super().states(fe, field_name)
+ def lands(self, fe: NativeFile) -> bool:
+ """An element of theirs the render cannot rewrite is still in the
+ file, written back as Recalbox wrote it."""
+ return self.writable(fe) or id(fe) in getattr(self, "_held_whole", {})
+
+ def withheld(self) -> list[str]:
+ """Each element kept as written because the truth names no md5."""
+ return [
+ f"{label} keeps its md5: the truth accepts another dump and "
+ "names no md5 for it, which es_bios.xsd requires"
+ for label in sorted(getattr(self, "_held_whole", {}).values())
+ ]
+
@classmethod
def writable(cls, fe: NativeFile, require: str = "") -> bool:
"""es_bios.xsd makes md5 and core required: without them, no element.
@@ -272,6 +285,21 @@ class Exporter(BaseExporter):
for s in root.iter("system")
]
+ # Elements of theirs written back as they stand, by (platform, path).
+ held = {
+ (platform, " ".join(child.get("path", "").split()))
+ for platform, _, children in theirs
+ for child in children
+ if isinstance(child, dict)
+ }
+ self._held_whole = {
+ id(fe): f"{system.native_id}/{self._path(fe)}"
+ for system in systems.values()
+ for fe in system.files
+ if fe.platform is not None
+ and not self.writable(fe)
+ and (system.native_id, " ".join(self._path(fe).split())) in held
+ }
ours: dict[str, tuple[NativeSystem, dict[str, tuple[NativeFile, str]]]] = {}
for system in systems.values():
writable = [fe for fe in system.files if self.writable(fe)]
diff --git a/tests/test_exporters.py b/tests/test_exporters.py
index 3c7f4dfc..72025477 100644
--- a/tests/test_exporters.py
+++ b/tests/test_exporters.py
@@ -1169,6 +1169,32 @@ class RecalboxExport(unittest.TestCase):
[("cdimono1.zip", False, True)],
)
+ def test_an_element_kept_as_written_is_not_counted_lost(self):
+ """The truth takes the entry on sha1 and size and names no md5: the
+ element is written back whole, and the run counted it a platform
+ entry the format cannot keep."""
+ scraped = {"systems": {"x": {"native_id": "x", "files": [{
+ "name": "a.bin", "destination": "x/a.bin", "md5": "a" * 32, "size": 100,
+ "native_system": "x", "core": "libretro/foo", "required": True,
+ }]}}}
+ truth = {"systems": {"x": {"files": [{
+ "name": "a.bin", "path": "x/a.bin", "sha1": "b" * 40, "size": 200,
+ "required": True, "_cores": ["foo"],
+ }]}}}
+ element = f' '
+ original = (
+ '\n\n'
+ f' \n{element}\n \n\n'
+ )
+ systems, report = build_native_model(truth, scraped)
+ exporter = discover_exporters()["recalbox"]()
+ produced = exporter.render(systems, report, {"es_bios.xml": original}, None)
+ self.assertIn(element, produced["es_bios.xml"])
+ fe = next(fe for s in systems.values() for fe in s.files)
+ self.assertTrue(exporter.lands(fe))
+ self.assertEqual(landed_changes(exporter, systems).records, [])
+ self.assertEqual(len(exporter.withheld()), 1)
+
def test_a_truncated_export_is_reported(self):
exporter, systems, _ = self._render()
empty = {