From e9f926030143799dbdff572102f88633322b859c Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:26:46 +0200 Subject: [PATCH] fix: keep one dump per exported hash record --- scripts/exporter/baseline.py | 23 ++++++++++++++++ scripts/exporter/romm_exporter.py | 6 ++--- scripts/exporter/systemdat_exporter.py | 6 ++--- tests/test_exporters.py | 37 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/scripts/exporter/baseline.py b/scripts/exporter/baseline.py index 0ca0218d..775adafd 100644 --- a/scripts/exporter/baseline.py +++ b/scripts/exporter/baseline.py @@ -123,6 +123,29 @@ class NativeFile: values = self.hashes(field_name) return values[0] if values else "" + def record_hash(self, field_name: str) -> str: + """One value of a record whose fields all describe one dump. + + A clrmamepro rom line or a RomM entry carries a single crc, md5 and + sha1: taking each field's first accepted value wrote syscard2.pce + with the v2.0 crc (the truth's first) beside the v2.1 md5 and sha1 + (System.dat's), a record no file satisfies. When the two sides + agree on the dump, the platform's record is kept whole, its value + chosen among those the truth accepts; a field only the truth names + is written when it names one value, which is then that dump's. + """ + source = self._identity_source() + truth_values = _hash_values(self.truth or {}, field_name) + platform_values = _hash_values(self.platform or {}, field_name) + if source == "truth": + return truth_values[0] if truth_values else "" + if source == "platform" or not truth_values: + return platform_values[0] if platform_values else "" + if platform_values: + accepted = [v for v in platform_values if v in truth_values] + return (accepted or truth_values)[0] + return truth_values[0] if len(truth_values) == 1 else "" + @property def required(self) -> bool: if self.truth is not None and self.truth.get("required") is not None: diff --git a/scripts/exporter/romm_exporter.py b/scripts/exporter/romm_exporter.py index 0cac671f..89b87af2 100644 --- a/scripts/exporter/romm_exporter.py +++ b/scripts/exporter/romm_exporter.py @@ -128,13 +128,13 @@ class Exporter(BaseExporter): entry: OrderedDict[str, str] = OrderedDict() # The fixture states every value as a string, size included. entry["size"] = str(fe.size()) - crc = fe.hash("crc32") + crc = fe.record_hash("crc32") if crc: entry["crc"] = crc - md5 = fe.hash("md5") + md5 = fe.record_hash("md5") if md5: entry["md5"] = md5 - sha1 = fe.hash("sha1") + sha1 = fe.record_hash("sha1") if sha1: entry["sha1"] = sha1 output[f"{system.native_id}:{fe.name}"] = entry diff --git a/scripts/exporter/systemdat_exporter.py b/scripts/exporter/systemdat_exporter.py index 016c9d01..8a918dfb 100644 --- a/scripts/exporter/systemdat_exporter.py +++ b/scripts/exporter/systemdat_exporter.py @@ -153,13 +153,13 @@ class Exporter(BaseExporter): size = fe.size() if size: parts.append(f"size {size}") - crc = fe.hash("crc32") + crc = fe.record_hash("crc32") if crc: parts.append(f"crc {crc.upper()}") - md5 = fe.hash("md5") + md5 = fe.record_hash("md5") if md5: parts.append(f"md5 {md5}") - sha1 = fe.hash("sha1") + sha1 = fe.record_hash("sha1") if sha1: parts.append(f"sha1 {sha1}") rendered.setdefault(name, f"\trom ( {' '.join(parts)} )") diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 80bd22ce..b0c25428 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1352,3 +1352,40 @@ class UpstreamFidelity(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class ARecordDescribesOneDump(unittest.TestCase): + """syscard2.pce: System.dat states v2.1 (crc 283b74e0, md5, sha1), + geargrafx accepts three crcs with v2.0's first. Field by field, the + export wrote v2.0's crc beside v2.1's md5 and sha1.""" + + def test_the_platform_record_stays_whole_when_the_truth_accepts_it(self): + from exporter.baseline import NativeFile + + fe = NativeFile( + name="syscard2.pce", destination="syscard2.pce", native_system="pce", + platform={"crc32": "283b74e0", "md5": "3cdd6614" + "0" * 24, + "sha1": "88da02e2" + "0" * 32}, + truth={"crc32": ["52520bc6", "283b74e0", "ff2a5ec3"]}, + ) + self.assertEqual(fe.hash("crc32"), "52520bc6") + self.assertEqual(fe.record_hash("crc32"), "283b74e0") + self.assertEqual(fe.record_hash("md5"), "3cdd6614" + "0" * 24) + + def test_a_field_only_the_truth_names_is_written_when_unambiguous(self): + from exporter.baseline import NativeFile + + one = NativeFile(name="a", destination="a", native_system="s", + platform={"crc32": "11111111"}, + truth={"crc32": "11111111", "md5": "a" * 32}) + two = NativeFile(name="a", destination="a", native_system="s", + platform={"crc32": "11111111"}, + truth={"crc32": "11111111", "md5": ["a" * 32, "b" * 32]}) + self.assertEqual(one.record_hash("md5"), "a" * 32) + self.assertEqual(two.record_hash("md5"), "") + + def test_single_record_writers_use_it(self): + for module in ("systemdat_exporter.py", "romm_exporter.py"): + source = (Path(__file__).resolve().parent.parent / "scripts" / "exporter" / module).read_text() + with self.subTest(module=module): + self.assertNotRegex(source, r'= fe\.hash\("(crc32|md5|sha1)"\)')