fix: check dolphin fork ipl dumps as a warning

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 06:52:41 +02:00
1 parent 4027604bf9
commit e100e50095
7 files changed
+77 -28

No files matched your search

+6
View File
@@ -85,7 +85,9 @@ files:
hle_fallback: true
region: [north-america]
size: 2097152
crc32: ["6dac1f2a", "d5e6feea", "86573808", "667d0b64"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, NTSC-U directory"
note: "Read whole into a 2 MiB buffer with no size check, then descrambled from 0x100 for 0x1afe00 bytes. The crc32 gate runs when the IPL itself is booted, and the four non-PAL dumps pass here without complaint."
source_ref: "Source/Core/Core/ConfigManager.cpp:861-868, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:99-112, Source/Core/Core/HW/EXI/EXI_DeviceIPL.h:43, Source/Core/Core/Boot/Boot.cpp:296-330"
@@ -97,7 +99,9 @@ files:
hle_fallback: true
region: [europe]
size: 2097152
crc32: ["4f319f43", "ad1b7f16"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, PAL directory"
note: "The two PAL dumps are the ones this directory takes without a mismatch warning."
source_ref: "Source/Core/Core/ConfigManager.cpp:835-849, Source/Core/Core/Boot/Boot.cpp:298-337"
@@ -109,7 +113,9 @@ files:
hle_fallback: true
region: [japan]
size: 2097152
crc32: ["6dac1f2a", "d5e6feea", "86573808", "667d0b64"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, NTSC-J directory"
note: "JAP is the only Japanese directory name this fork knows. FindIPLDump, which serves the font path, scans USA then EUR then JAP and stops at the first hit."
source_ref: "Source/Core/Common/CommonPaths.h:32-34, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:174-186"
+6
View File
@@ -121,7 +121,9 @@ files:
hle_fallback: true
region: [north-america]
size: 2097152
crc32: ["6dac1f2a", "d5e6feea", "86573808", "667d0b64"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, NTSC-U directory"
note: "Read whole into a 2 MiB buffer with no size check, then descrambled from 0x100 for 0x1afe00 bytes. The crc32 gate runs when the IPL itself is booted and only warns; the four non-PAL dumps, the Brazilian MPAL one included, pass here without complaint."
source_ref: "Source/Core/Core/Config/MainSettings.cpp:594-600, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:105-113, Source/Core/Core/HW/EXI/EXI_DeviceIPL.h:43, Source/Core/Core/Boot/Boot.cpp:408-442, Source/Core/Core/Boot/Boot.cpp:444-449"
@@ -133,7 +135,9 @@ files:
hle_fallback: true
region: [europe]
size: 2097152
crc32: ["4f319f43", "ad1b7f16"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, PAL directory"
note: "The two PAL dumps are the ones this directory takes without a mismatch warning."
source_ref: "Source/Core/Core/Config/MainSettings.cpp:580-581, Source/Core/Core/Boot/Boot.cpp:444-449"
@@ -145,7 +149,9 @@ files:
hle_fallback: true
region: [japan]
size: 2097152
crc32: ["6dac1f2a", "d5e6feea", "86573808", "667d0b64"]
validation: [crc32]
on_mismatch: warn
description: "GameCube boot ROM, NTSC-J directory"
note: "JPN is defined alongside JAP but the boot ROM callers use the legacy default, so JAP is the directory read. FindIPLDump, which serves the font path, scans USA then EUR then JAP and stops at the first hit."
source_ref: "Source/Core/Common/CommonPaths.h:34-35, Source/Core/Core/Config/MainSettings.cpp:574-575, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:175-187"
+15 -2
View File
@@ -511,8 +511,21 @@
"pattern": "^[a-fA-F0-9]{64}(,[a-fA-F0-9]{64})*$"
},
"crc32": {
"type": "string",
"pattern": "^[a-fA-F0-9]{8}$"
"description": "CRC32 the code accepts; a list when several dumps pass.",
"oneOf": [
{
"type": "string",
"pattern": "^[a-fA-F0-9]{8}$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-fA-F0-9]{8}$"
},
"minItems": 1
}
]
},
"known_hash_adler32": {
"description": "Adler-32 values the code accepts; a list when several revisions pass.",
+27 -19
View File
@@ -408,7 +408,7 @@ def declared_hash_verdict(
sha1_candidates: set,
sha256_candidates: set,
md5_list: list,
crc_raw: str,
crc_candidates: set,
zipped_file: str | None,
declared_size,
) -> str:
@@ -427,7 +427,7 @@ def declared_hash_verdict(
algorithms.add("sha256")
if md5_list and not zipped_file:
algorithms.add("md5")
if crc_raw:
if crc_candidates:
algorithms.add("crc32")
actual = compute_hashes(candidate, frozenset(algorithms)) if algorithms else {}
if sha1_candidates and actual.get("sha1", "").lower() not in sha1_candidates:
@@ -438,9 +438,9 @@ def declared_hash_verdict(
actual.get("md5", "").lower().startswith(expected) for expected in md5_list
):
return "hash_mismatch"
if crc_raw and actual.get("crc32", "").lower() != crc_raw:
if crc_candidates and actual.get("crc32", "").lower() not in crc_candidates:
return "hash_mismatch"
if crc_raw and declared_size is not None:
if crc_candidates and declared_size is not None:
allowed_sizes = declared_size if isinstance(declared_size, list) else [declared_size]
if os.path.getsize(candidate) not in allowed_sizes:
return "hash_mismatch"
@@ -664,10 +664,17 @@ def resolve_local_file(
for candidate in str(value).split(",")
if len(candidate.strip()) == 64
]
crc_raw = str(file_entry.get("crc32", "") or "").strip().lower()
# One value, a list, or a comma-separated string, like the other hashes.
crc_values = file_entry.get("crc32") or []
crc_candidates = {
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_raw
sha1_candidates or sha256_candidates or md5_list or crc_candidates
)
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
@@ -694,8 +701,8 @@ def resolve_local_file(
if not any(actual_md5.startswith(expected) for expected in md5_list):
return None
statuses.append("md5_exact")
if crc_raw:
if str(entry.get("crc32", "")).lower() != crc_raw:
if crc_candidates:
if str(entry.get("crc32", "")).lower() not in crc_candidates:
return None
if declared_size is not None:
allowed_sizes = (
@@ -730,20 +737,21 @@ def resolve_local_file(
# size confirms it when available; a handful of emulators validate CRC32
# alone, so those entries retain the same (weaker) evidence as the core.
if (
crc_raw
crc_candidates
and not zipped_file
and not md5_list
and not sha1_candidates
and not sha256_candidates
):
by_crc32 = db.get("indexes", {}).get("by_crc32", {})
match = by_crc32.get(crc_raw)
if match and match in files_db:
entry = files_db[match]
path = entry["path"]
status = _record_match_status(match)
if status and os.path.exists(path):
return path, status
for crc in sorted(crc_candidates):
match = by_crc32.get(crc)
if match and match in files_db:
entry = files_db[match]
path = entry["path"]
status = _record_match_status(match)
if status and os.path.exists(path):
return path, status
# 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
@@ -900,7 +908,7 @@ def resolve_local_file(
candidates = sized
if candidates:
if md5_set and not (sha1_candidates or sha256_candidates or crc_raw):
if md5_set and not (sha1_candidates or sha256_candidates or crc_candidates):
for path, db_md5, _size in candidates:
if ".zip" in os.path.basename(path):
try:
@@ -938,7 +946,7 @@ def resolve_local_file(
zipped_file
and md5_list
and zip_contents
and not (sha1_candidates or sha256_candidates or crc_raw)
and not (sha1_candidates or sha256_candidates or crc_candidates)
):
for md5_candidate in md5_list:
if md5_candidate in zip_contents:
@@ -978,7 +986,7 @@ def resolve_local_file(
sha1_candidates=sha1_candidates,
sha256_candidates=sha256_candidates,
md5_list=md5_list,
crc_raw=crc_raw,
crc_candidates=crc_candidates,
zipped_file=zipped_file,
declared_size=declared_size,
)
+2 -1
View File
@@ -2028,7 +2028,8 @@ def _render_emulator_file(
if fmd5:
details.append(f"MD5: {_hash_spans(fmd5)}")
if fcrc32:
details.append(f"CRC32: `{fcrc32}`")
crcs = fcrc32 if isinstance(fcrc32, list) else [fcrc32]
details.append("CRC32: " + ", ".join(f"`{crc}`" for crc in crcs))
if fsha256:
details.append(f"SHA256: {_hash_spans(fsha256)}")
if fadler32:
+15
View File
@@ -235,5 +235,20 @@ class HypseusArchives(unittest.TestCase):
self.assertFalse(entry.get("validation"))
class DolphinForksIplDumps(_Probe):
"""Load_BS2 compares the IPL's CRC32 with six Redump dumps and only
raises "The IPL file is not a known good dump" before booting it
(Source/Core/Core/Boot/Boot.cpp:296-337 at Dolphin-MMJR 1b095b6,
408-449 at Dolphin-MMJR2 4d0fdb8). The profiles declared the check with
no value, so nothing told a known dump from any other."""
def test_an_unknown_ipl_only_warns(self):
for emulator in ("dolphin-mmjr", "dolphin-mmjr2"):
with self.subTest(emulator=emulator):
data = b"\0" * 2097152
self.assertTrue(self.accepts(emulator, "IPL.bin", data))
self.assertFalse(self.accepts(emulator, "IPL.bin", data, strict=True))
if __name__ == "__main__":
unittest.main()
+6 -6
View File
@@ -52,7 +52,7 @@ class DeclaredHashVerdict(unittest.TestCase):
def verdict(self, path, **declared):
base = dict(
has_strong_hash=True, sha1_candidates=set(), sha256_candidates=set(),
md5_list=[], crc_raw="", zipped_file=None, declared_size=None,
md5_list=[], crc_candidates=set(), zipped_file=None, declared_size=None,
)
base.update(declared)
return common.declared_hash_verdict(str(path), **base)
@@ -82,7 +82,7 @@ class DeclaredHashVerdict(unittest.TestCase):
def test_crc32_is_checked(self):
self.assertEqual(
self.verdict(self.wrong, crc_raw=self.d["crc32"]), "hash_mismatch"
self.verdict(self.wrong, crc_candidates={self.d["crc32"]}), "hash_mismatch"
)
def test_a_truncated_md5_still_decides(self):
@@ -102,7 +102,7 @@ class DeclaredHashVerdict(unittest.TestCase):
self.verdict(
self.right,
sha1_candidates={self.d["sha1"]},
crc_raw="deadbeef",
crc_candidates={"deadbeef"},
),
"hash_mismatch",
)
@@ -110,13 +110,13 @@ class DeclaredHashVerdict(unittest.TestCase):
def test_a_size_beside_a_crc32_is_enforced(self):
self.assertEqual(
self.verdict(
self.right, crc_raw=self.d["crc32"], declared_size=len(PAYLOAD)
self.right, crc_candidates={self.d["crc32"]}, declared_size=len(PAYLOAD)
),
"data_dir_hash_exact",
)
self.assertEqual(
self.verdict(
self.right, crc_raw=self.d["crc32"], declared_size=len(PAYLOAD) + 1
self.right, crc_candidates={self.d["crc32"]}, declared_size=len(PAYLOAD) + 1
),
"hash_mismatch",
)
@@ -125,7 +125,7 @@ class DeclaredHashVerdict(unittest.TestCase):
self.assertEqual(
self.verdict(
self.right,
crc_raw=self.d["crc32"],
crc_candidates={self.d["crc32"]},
declared_size=[1, len(PAYLOAD)],
),
"data_dir_hash_exact",