From c5fd079b78d371d5e2f7aa7ad76f0af945153e2c Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:23:29 +0200 Subject: [PATCH] fix: keep the text of unchanged Recalbox elements Every element the model held was rendered again, so attribute order, alignment, a line break inside md5 and the case of a digest changed in 222 elements that stated the same thing. The diff deleted 451 lines for 136 corrections. An element whose attributes say what Recalbox wrote now keeps Recalbox's own text: 175 lines, the corrections. --- scripts/exporter/recalbox_exporter.py | 45 ++++++++++++++++++++++++--- tests/test_exporters.py | 29 +++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py index 47d53768..9f1d9ca1 100644 --- a/scripts/exporter/recalbox_exporter.py +++ b/scripts/exporter/recalbox_exporter.py @@ -11,6 +11,8 @@ when the platform stated it explicitly. from __future__ import annotations +import html +import re import sys from pathlib import Path from xml.etree.ElementTree import Comment, ParseError @@ -33,6 +35,32 @@ SCHEMA_URL = ( ) +_ATTRIBUTE = re.compile(r'([A-Za-z]+)\s*=\s*"([^"]*)"') +_SYSTEM_BLOCK = re.compile(r"", re.S) +_BIOS_ELEMENT = re.compile(r"[ \t]*", re.S) + + +def _stated(text: str) -> dict[str, str]: + """What an element says, without its layout: attribute order, spacing, + line breaks inside a value and the case of a digest are not content.""" + attrs = {k: " ".join(html.unescape(v).split()) for k, v in _ATTRIBUTE.findall(text)} + if "md5" in attrs: + attrs["md5"] = ",".join(h.strip().lower() for h in attrs["md5"].split(",")) + return attrs + + +def _original_elements(original: str) -> list[dict[str, str]]: + """Each system block's elements as written, by path, in order.""" + blocks = [] + for block in _SYSTEM_BLOCK.finditer(original): + elements: dict[str, str] = {} + for match in _BIOS_ELEMENT.finditer(block.group(0)): + raw = match.group(0) + elements.setdefault(_stated(raw).get("path", ""), raw) + blocks.append(elements) + return blocks + + class Exporter(BaseExporter): """Write Recalbox's es_bios.xml, corrected.""" @@ -179,17 +207,26 @@ class Exporter(BaseExporter): lines.extend(body) lines.append(" ") - # Each block of theirs corrects the entries it holds. + # Each block of theirs corrects the entries it holds. An element that + # states what Recalbox wrote keeps Recalbox's text: re-rendered, 222 + # unchanged elements made 180 lines of diff the report never named. + written = _original_elements(original) if original else [] blocks: list[tuple[str, str, list[str]]] = [] - for platform, fullname, children in theirs: + for index, (platform, fullname, children) in enumerate(theirs): elements = ours.get(platform, (None, {}))[1] + raw = written[index] if index < len(written) else {} body: list[str] = [] for child in children: if isinstance(child, str): body.append(f" {child}") continue - mine = elements.pop(child.get("path", ""), None) - body.append(mine[1] if mine else element(child)) + path = child.get("path", "") + mine = elements.pop(path, None) + kept = raw.get(" ".join(path.split())) + if kept is not None and (mine is None or _stated(mine[1]) == _stated(kept)): + body.append(kept) + else: + body.append(mine[1] if mine else element(child)) blocks.append((platform, fullname, body)) # An addition joins the block named like its file, else the first diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 9f496447..520f45bc 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1764,5 +1764,34 @@ class BatoceraNotesStayWithTheirItem(unittest.TestCase): self.assertEqual(trailing, {("bios/bk0010.zip", "two.rom"): "# tail"}) +class RecalboxKeepsUnchangedText(unittest.TestCase): + """Every element the model held was re-rendered: attribute order, + alignment, line breaks inside md5 and digest case changed in 222 + elements that said the same thing, 180 lines of diff nobody was told of.""" + + def test_an_element_that_says_the_same_keeps_its_text(self): + scraped = {"systems": {"sinclair-zxspectrum": { + "native_id": "zxspectrum", + "files": [{ + "name": "128p-0.rom", "destination": "fuse/128p-0.rom", + "native_system": "zxspectrum", "native_name": "Pentagon", + "native_path": "fuse/128p-0.rom", "md5": "a" * 32, + "core": "libretro/fuse", "required": True, + }], + }}} + written = f' ' + original = ( + '\n\n' + ' \n' + f"{written}\n" + " \n\n" + ) + systems, report = build_native_model({"systems": {}}, scraped) + produced = discover_exporters()["recalbox"]().render( + systems, report, {"es_bios.xml": original}, None + )["es_bios.xml"] + self.assertIn(written, produced) + + if __name__ == "__main__": unittest.main()