fix: keep System.dat lines whose values agree

The rom line renderer upper-cased every crc, so Dinothawr.zip's
683ed4ad, the one lower-case crc libretro writes, changed with no
correction behind it. A line whose values match ours now keeps its
bytes, a corrected crc follows the case of its own line, and an added
line keeps the file's upper case. An empty truth hands System.dat back
byte for byte for RetroArch and Lakka.
This commit is contained in:
Abdessamad Derraz committed 2026-10-20 19:39:36 +02:00
1 parent 546c1ac871
commit 7e0b3a08b9
4 files changed
+84 -4

No files matched your search

+1 -1
View File
@@ -167,7 +167,7 @@ game (
rom ( name vice/DRIVES/dos1571cr-318047-01.bin size 32768 crc F24EFCC4 md5 f9158328494b3e269f86ef419792c2e5 sha1 14ee7a0fb7e1c59c51fbf781f944387037daa3ee )
comment "Dinothawr"
rom ( name Dinothawr.zip size 5763199 crc 683ED4AD md5 a2e891e330d146c4046c2b622fc31462 sha1 693d8bb4d992c645e6413a57195acf4eca2f5a2e )
rom ( name Dinothawr.zip size 5763199 crc 683ed4ad md5 a2e891e330d146c4046c2b622fc31462 sha1 693d8bb4d992c645e6413a57195acf4eca2f5a2e )
comment "DOS"
rom ( name MT32_CONTROL.ROM size 65536 crc A7C1531B md5 5626206284b22c2734f3e9efefcd2675 sha1 b083518fffb7f66b03c23b7eb4f868e62dc5a987 )
+1 -1
View File
@@ -168,7 +168,7 @@ game (
rom ( name vice/DRIVES/dos1571cr-318047-01.bin size 32768 crc F24EFCC4 md5 f9158328494b3e269f86ef419792c2e5 sha1 14ee7a0fb7e1c59c51fbf781f944387037daa3ee )
comment "Dinothawr"
rom ( name Dinothawr.zip size 5763199 crc 683ED4AD md5 a2e891e330d146c4046c2b622fc31462 sha1 693d8bb4d992c645e6413a57195acf4eca2f5a2e )
rom ( name Dinothawr.zip size 5763199 crc 683ed4ad md5 a2e891e330d146c4046c2b622fc31462 sha1 693d8bb4d992c645e6413a57195acf4eca2f5a2e )
comment "DOS"
rom ( name MT32_CONTROL.ROM size 65536 crc A7C1531B md5 5626206284b22c2734f3e9efefcd2675 sha1 b083518fffb7f66b03c23b7eb4f868e62dc5a987 )
+21 -2
View File
@@ -42,6 +42,26 @@ def _quote(name: str, quoted: frozenset[str] = frozenset()) -> str:
_ROM_NAME = re.compile(r'^(\s+)rom \( name ("[^"]+"|\S+)')
_CRC = re.compile(r" crc (\S+)")
def _patch(line: str, indent: str, ours: str) -> str:
"""The original line where our values agree with it, else ours in its form.
libretro writes one crc in lower case (Dinothawr.zip's 683ed4ad) and the
others in upper case. A line whose values did not change keeps its bytes,
and a corrected crc takes the case of the line it lands on. The
indentation stays libretro's.
"""
ours = indent + ours.lstrip("\t")
if line.lower() == ours.lower():
return line
theirs = _CRC.search(line)
if theirs and theirs.group(1) != theirs.group(1).upper():
ours = _CRC.sub(lambda m: f" crc {m.group(1).lower()}", ours, count=1)
return ours
def _sections(original: str) -> list[tuple[str, list[str]]]:
"""The system sections of a System.dat, each comment with its rom lines."""
_, sep, body = original.partition("\ngame (")
@@ -191,8 +211,7 @@ class Exporter(BaseExporter):
for line in roms:
match = _ROM_NAME.match(line)
if match and match.group(2) in rendered:
# The values are ours, the indentation stays libretro's.
body.append(match.group(1) + rendered.pop(match.group(2)).lstrip("\t"))
body.append(_patch(line, match.group(1), rendered.pop(match.group(2))))
else:
body.append(line)
body.extend(rendered.values())
+61
View File
@@ -1343,6 +1343,67 @@ class SystemDatKeepsSpaceIndentedLines(unittest.TestCase):
self.assertEqual(produced.count("kick33180.A500"), 1)
class SystemDatComesBackAsWritten(unittest.TestCase):
"""libretro writes one crc in lower case, Dinothawr.zip's 683ed4ad, and
the 513 others in upper case. Rendered upper case without condition,
that line changed with no correction behind it."""
SECTION = "Dinothawr"
ORIGINAL = (
'clrmamepro (\n\tname "System"\n)\n\ngame (\n\tname "System"\n'
'\tcomment "System"\n\n'
'\tcomment "Dinothawr"\n'
f"\trom ( name lower.zip size 10 crc 683ed4ad md5 {'1' * 32} sha1 {'1' * 40} )\n"
f"\trom ( name upper.zip size 10 crc 0BCDEF12 md5 {'2' * 32} sha1 {'2' * 40} )\n"
")\n"
)
def _render(self, truth_files: list[dict]) -> str:
scraped = {"systems": {"dinothawr": {"native_id": self.SECTION, "files": [
{"name": "lower.zip", "destination": "lower.zip", "native_system": self.SECTION,
"size": 10, "crc32": "683ed4ad", "md5": "1" * 32, "sha1": "1" * 40},
{"name": "upper.zip", "destination": "upper.zip", "native_system": self.SECTION,
"size": 10, "crc32": "0BCDEF12", "md5": "2" * 32, "sha1": "2" * 40},
]}}}
truth = {"systems": {"dinothawr": {"files": truth_files}}} if truth_files else {"systems": {}}
systems, report = build_native_model(truth, scraped)
return discover_exporters()["retroarch"]().render(
systems, report, {"System.dat": self.ORIGINAL}, None
)["System.dat"]
def test_an_empty_truth_hands_back_the_lines(self):
self.assertEqual(self._render([]), self.ORIGINAL)
def test_a_corrected_crc_follows_the_case_of_its_line(self):
produced = self._render([
{"name": "lower.zip", "size": 10, "crc32": "abcdef01", "md5": "3" * 32, "sha1": "3" * 40},
{"name": "upper.zip", "size": 10, "crc32": "fedcba98", "md5": "4" * 32, "sha1": "4" * 40},
])
self.assertIn(" crc abcdef01 ", produced)
self.assertIn(" crc FEDCBA98 ", produced)
def test_an_added_line_takes_the_case_of_the_file(self):
produced = self._render([
{"name": "new.zip", "size": 10, "crc32": "abcdef01", "md5": "5" * 32, "sha1": "5" * 40},
])
self.assertIn("name new.zip size 10 crc ABCDEF01 ", produced)
def test_an_empty_truth_hands_back_system_dat(self):
for platform in ("retroarch", "lakka"):
original_path = UPSTREAM_CACHE / platform / "System.dat"
if not original_path.exists():
self.skipTest("upstream cache not populated")
original = original_path.read_bytes()
exporter = discover_exporters()[platform]()
config = native_layer(load_platform_config(platform, str(REPO_ROOT / "platforms")))
systems, report = build_native_model({"systems": {}}, config)
produced = exporter.render(
systems, report, {"System.dat": original.decode("utf-8")}, config
)
with self.subTest(platform=platform):
self.assertEqual(produced["System.dat"].encode("utf-8"), original)
class RommSizeIsNeverNone(unittest.TestCase):
"""size() is None where the truth contradicts RomM's hash without a size
or names several sizes. str(None) went out as "None", validate() passed