diff --git a/exports/lakka/System.dat b/exports/lakka/System.dat index be4a0756..a3a14e4c 100644 --- a/exports/lakka/System.dat +++ b/exports/lakka/System.dat @@ -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 ) diff --git a/exports/retroarch/System.dat b/exports/retroarch/System.dat index 115f2434..afb37f6e 100644 --- a/exports/retroarch/System.dat +++ b/exports/retroarch/System.dat @@ -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 ) diff --git a/scripts/exporter/systemdat_exporter.py b/scripts/exporter/systemdat_exporter.py index 6e403873..042a6357 100644 --- a/scripts/exporter/systemdat_exporter.py +++ b/scripts/exporter/systemdat_exporter.py @@ -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()) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index d0608c2c..47585e76 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -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