From 587aadde0898d1f0f1abf08a47826cfc1573cd2a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:30:37 +0200 Subject: [PATCH] fix: coerce numeric hashes to strings --- emulators/vice.yml | 2 +- scripts/common.py | 24 ++++++++++++++ scripts/exporter/systemdat_exporter.py | 2 +- tests/test_e2e.py | 44 ++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/emulators/vice.yml b/emulators/vice.yml index 3f008602..2906c041 100644 --- a/emulators/vice.yml +++ b/emulators/vice.yml @@ -602,7 +602,7 @@ files: size: 16384 sha1: a3d9e5be091b98de39a046ab167fb7632d053682 md5: b3c450c36a88b491281d18fdf3e94ab0 - crc32: 70295038 + crc32: "70295038" note: "Plus/4 Kernal NTSC Rev 5 (default NTSC). Embedded in core." source_ref: "vice/src/plus4/plus4rom.h:37" diff --git a/scripts/common.py b/scripts/common.py index e4cfc93d..cb43d41d 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -768,11 +768,35 @@ def load_emulator_profiles( continue if skip_aliases and profile.get("type") == "alias": continue + _normalize_hash_fields(profile) profiles[f.stem] = profile _emulator_profiles_cache[cache_key] = profiles return profiles +_HASH_FIELDS = ("crc32", "md5", "sha1", "sha256") + + +def _normalize_hash_fields(node: object) -> None: + """Coerce hash values to str in place. + + YAML types unquoted digit-only hashes (e.g. crc32: 70295038) as int, + which breaks string comparisons and formatting downstream. + """ + if isinstance(node, dict): + for key, val in node.items(): + if key in _HASH_FIELDS: + if isinstance(val, int): + node[key] = str(val) + elif isinstance(val, list): + node[key] = [str(v) if isinstance(v, int) else v for v in val] + else: + _normalize_hash_fields(val) + elif isinstance(node, list): + for item in node: + _normalize_hash_fields(item) + + def unique_emulator_profiles(profiles: dict[str, dict]) -> dict[str, dict]: """Profiles counting as distinct emulators (aliases and tests excluded).""" return { diff --git a/scripts/exporter/systemdat_exporter.py b/scripts/exporter/systemdat_exporter.py index 63c94228..dc9a07e2 100644 --- a/scripts/exporter/systemdat_exporter.py +++ b/scripts/exporter/systemdat_exporter.py @@ -96,7 +96,7 @@ class Exporter(BaseExporter): rom_parts.append(f"size {size}") crc = fe.get("crc32", "") if crc: - rom_parts.append(f"crc {crc.upper()}") + rom_parts.append(f"crc {str(crc).upper()}") md5 = fe.get("md5", "") if isinstance(md5, list): md5 = md5[0] if md5 else "" diff --git a/tests/test_e2e.py b/tests/test_e2e.py index a8f2588d..9c91bea8 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -935,6 +935,22 @@ class TestE2E(unittest.TestCase): profiles = load_emulator_profiles(self.emulators_dir) self.assertNotIn("test_alias", profiles) + def test_42b_profile_hash_fields_load_as_str(self): + """Unquoted digit-only hashes must load as strings, not YAML ints.""" + with tempfile.TemporaryDirectory() as tmp: + Path(tmp, "numcrc.yml").write_text( + "emulator: NumCRC\n" + "type: libretro\n" + "files:\n" + " - name: kernal.bin\n" + " crc32: 70295038\n" + " md5: [12345678901234567890123456789012]\n" + ) + profiles = load_emulator_profiles(tmp) + entry = profiles["numcrc"]["files"][0] + self.assertEqual(entry["crc32"], "70295038") + self.assertEqual(entry["md5"], ["12345678901234567890123456789012"]) + def test_43_cross_ref_data_dir_does_not_suppress_files(self): config = load_platform_config("test_md5", self.platforms_dir) profiles = load_emulator_profiles(self.emulators_dir) @@ -3842,6 +3858,34 @@ class TestE2E(unittest.TestCase): self.assertIn("retroarch", exporters) self.assertIs(exporters["retroarch"], SystemDatExporter) + def test_systemdat_exporter_numeric_crc(self): + """Digit-only crc32 typed as int by YAML must not break the export.""" + from exporter.systemdat_exporter import Exporter as SystemDatExporter + + truth = { + "platform": "retroarch", + "systems": { + "commodore-plus4": { + "files": [ + { + "name": "kernal-318005-05.bin", + "path": "kernal-318005-05.bin", + "size": 16384, + "crc32": 70295038, + "required": False, + }, + ], + }, + }, + } + + out_path = os.path.join(self.root, "System_numeric.dat") + SystemDatExporter().export(truth, out_path, scraped_data={"systems": {}}) + + with open(out_path) as fh: + content = fh.read() + self.assertIn("crc 70295038", content) + # --------------------------------------------------------------- # Full truth + diff integration test # ---------------------------------------------------------------