From a2108c6a1c6913479273401110763c16b01eacb2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:00:17 +0200 Subject: [PATCH] fix: check geargrafx cards by size, as its loader --- emulators/geargrafx.yml | 31 +++++++++++++++++-------------- scripts/common.py | 21 ++++++++++++++------- tests/test_profile_checks.py | 15 +++++++++++++++ tests/test_resolution_evidence.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/emulators/geargrafx.yml b/emulators/geargrafx.yml index a89ae88e..a45cd1b5 100644 --- a/emulators/geargrafx.yml +++ b/emulators/geargrafx.yml @@ -82,9 +82,10 @@ notes: | files: - name: "syscard3.pce" description: "Super CD-ROM System Card 3.0" - size: 262144 + size: [262144, 262656] required: true - validation: [size, crc32] + crc32: ["6d9a73ef", "0ecea1f3", "2b5b75fe"] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:714, platforms/libretro/libretro.cpp:726-731, platforms/libretro/libretro.cpp:736-746, src/media.cpp:662-705, src/defines.h:53, src/game_db.h:51-52, src/game_db.h:54" note: > Loaded for the default option value System Card 3 and for Force Game @@ -95,9 +96,10 @@ files: - name: "syscard2.pce" description: "CD-ROM System Card 2.0 / 2.1" - size: 262144 + size: [262144, 262656] required: false - validation: [size, crc32] + crc32: ["52520bc6", "283b74e0", "ff2a5ec3"] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:713, platforms/libretro/libretro.cpp:723-724, src/media.cpp:662-705, src/defines.h:53, src/game_db.h:49-50, src/game_db.h:53" note: > Loaded when geargrafx_cdrom_bios is System Card 2. game_db.h accepts @@ -106,18 +108,19 @@ files: - name: "syscard1.pce" description: "CD-ROM System Card 1.0 (Japan)" - size: 262144 + size: [262144, 262656] crc32: "3F9F95A4" required: false - validation: [size, crc32] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:712, platforms/libretro/libretro.cpp:720-721, src/media.cpp:662-705, src/defines.h:53, src/game_db.h:48" note: "Loaded when geargrafx_cdrom_bios is System Card 1." - name: "gexpress.pce" description: "Games Express CD Card" - size: 32768 + size: [32768, 33280] required: false - validation: [size, crc32] + crc32: ["51a12d90", "16aaf05a"] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:715, platforms/libretro/libretro.cpp:748-749, src/media.cpp:662-705, src/media.cpp:821-823, src/media.cpp:835-839, src/media.cpp:993-997, src/defines.h:54, src/game_db.h:55-56" note: > Always attempted after the System Card, used for discs flagged @@ -130,11 +133,11 @@ files: - name: "pac-n1.bin" description: "LaserActive NEC PAC-N1 v1.02 (Japan)" - size: 524288 + size: [524288, 524800] crc32: "A8CB694C" region: [japan] required: true - validation: [size, crc32] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:780-784, platforms/libretro/libretro.cpp:534-549, src/media.cpp:570-632, src/defines.h:55" note: > Japanese PAC firmware, read for MMI content when @@ -143,21 +146,21 @@ files: - name: "pce-lp1.bin" description: "LaserActive NEC PCE-LP1 v1.02 (Japan)" - size: 524288 + size: [524288, 524800] crc32: "76116A02" region: [japan] required: false - validation: [size, crc32] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:785-789, src/media.cpp:570-632, src/defines.h:55" note: "Read only when pac-n1.bin did not load." - name: "pac-n10.bin" description: "LaserActive NEC PAC-N10 v1.02 (US)" - size: 524288 + size: [524288, 524800] crc32: "01223DD5" region: [north-america] required: true - validation: [size, crc32] + validation: [size] source_ref: "platforms/libretro/libretro.cpp:792-796, platforms/libretro/libretro.cpp:534-549, src/media.cpp:570-632, src/defines.h:55" note: > US PAC firmware, read for MMI content when diff --git a/scripts/common.py b/scripts/common.py index d3ea0180..0085cb43 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -666,12 +666,13 @@ def resolve_local_file( ] # One value, a list, or a comma-separated string, like the other hashes. crc_values = file_entry.get("crc32") or [] - crc_candidates = { + # Kept in the order declared, which is the order a lookup tries them. + crc_candidates = dict.fromkeys( candidate.strip().lower().removeprefix("0x") for value in (crc_values if isinstance(crc_values, list) else [crc_values]) for candidate in str(value).split(",") if candidate.strip() - } + ) declared_size = file_entry.get("size") has_strong_hash = bool( sha1_candidates or sha256_candidates or md5_list or crc_candidates @@ -744,14 +745,20 @@ def resolve_local_file( and not sha256_candidates ): by_crc32 = db.get("indexes", {}).get("by_crc32", {}) - for crc in sorted(crc_candidates): + found: list[tuple[str, str]] = [] + for crc in crc_candidates: match = by_crc32.get(crc) if match and match in files_db: - entry = files_db[match] - path = entry["path"] + path = files_db[match]["path"] status = _record_match_status(match) if status and os.path.exists(path): - return path, status + found.append((path, status)) + # A primary copy over a .variants/ one, as everywhere else. + for path, status in found: + if "/.variants/" not in path: + return path, status + if found: + return found[0] # 2. MD5 direct lookup (skip for zipped_file: md5 is inner ROM, not container) # Guard: only accept if the found file's name matches the requested name @@ -986,7 +993,7 @@ def resolve_local_file( sha1_candidates=sha1_candidates, sha256_candidates=sha256_candidates, md5_list=md5_list, - crc_candidates=crc_candidates, + crc_candidates=set(crc_candidates), zipped_file=zipped_file, declared_size=declared_size, ) diff --git a/tests/test_profile_checks.py b/tests/test_profile_checks.py index 8fb7aae6..713c19d5 100644 --- a/tests/test_profile_checks.py +++ b/tests/test_profile_checks.py @@ -263,5 +263,20 @@ class GambatteBootRoms(_Probe): self.assertTrue(self.accepts("gambatte", name, b"\0" * (size + 512))) +class GeargrafxCards(_Probe): + """LoadBiosData strips a 512-byte header and refuses any other size; the + CRC32 only names the card in the log, and an unknown LaserActive CRC32 + is logged and loaded (src/media.cpp:570-632, 662-705, 1028-1063 at + d0307bf). The profiles declared crc32 checks with no value.""" + + def test_size_decides_and_an_unknown_card_loads(self): + for name, size in (("syscard3.pce", 262144), ("gexpress.pce", 32768), + ("pac-n10.bin", 524288)): + with self.subTest(name=name): + self.assertTrue(self.accepts("geargrafx", name, b"\1" * size, strict=True)) + self.assertTrue(self.accepts("geargrafx", name, b"\1" * (size + 512))) + self.assertFalse(self.accepts("geargrafx", name, b"\1" * (size - 1))) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_resolution_evidence.py b/tests/test_resolution_evidence.py index d7f542db..80e2855b 100644 --- a/tests/test_resolution_evidence.py +++ b/tests/test_resolution_evidence.py @@ -246,5 +246,34 @@ class OneSpellingForAcceptedSizes(unittest.TestCase): self.assertEqual(file_entry["size"]["type"], ["integer", "array", "null"]) +class SeveralAcceptedCrc32(unittest.TestCase): + """A profile can list every CRC32 its code accepts (geargrafx's System + Card 3 table holds the Japanese card, a font hack and the US card). + Walking them in sorted order served the font hack, held in .variants/, + in place of the primary Japanese card.""" + + def test_declared_order_and_the_primary_first(self): + with tempfile.TemporaryDirectory() as tmp: + primary = Path(tmp) / "syscard3.pce" + variant = Path(tmp) / ".variants" / "syscard3.pce.f82f5056" + variant.parent.mkdir() + primary.write_bytes(b"japan") + variant.write_bytes(b"hack") + crc = {p: format(zlib.crc32(p.read_bytes()), "08x") for p in (primary, variant)} + db = { + "files": { + "s1": {"path": str(primary), "name": primary.name, "crc32": crc[primary], "size": 5}, + "s2": {"path": str(variant), "name": primary.name, "crc32": crc[variant], "size": 4}, + }, + "indexes": {"by_crc32": {crc[primary]: "s1", crc[variant]: "s2"}, + "by_name": {primary.name: ["s1", "s2"]}}, + } + hack_first = sorted([crc[variant], crc[primary]]) + entry = {"name": "syscard3.pce", "crc32": hack_first} + self.assertEqual( + common.resolve_local_file(entry, db), (str(primary), "crc32_exact") + ) + + if __name__ == "__main__": unittest.main()