feat: let a single check warn while the others refuse

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 07:10:22 +02:00
1 parent ee377d2f57
commit 18af962cb3
4 files changed
+80 -22

No files matched your search

+3
View File
@@ -73,6 +73,7 @@ files:
required: false
size: [288, 320]
validation: [size, signature]
on_mismatch: {signature: warn}
note: "console-unique key seed; 288 bytes without the AES MAC block, 320 with it"
source_ref: "src/core/hw/unique_data.cpp:170-200, src/core/hw/unique_data.cpp:217, src/core/hw/unique_data.h:103-133"
@@ -82,6 +83,7 @@ files:
required: false
size: 273
validation: [size, signature]
on_mismatch: {signature: warn}
note: "console serial and region data"
source_ref: "src/core/hw/unique_data.cpp:43-92, src/core/hw/unique_data.cpp:205"
@@ -91,5 +93,6 @@ files:
required: false
size: 272
validation: [size, signature]
on_mismatch: {signature: warn}
note: "friend code generation seed"
source_ref: "src/core/hw/unique_data.cpp:94-123, src/core/hw/unique_data.cpp:209, src/core/hw/unique_data.h:76"
+21 -4
View File
@@ -657,10 +657,27 @@
"type": "string"
},
"on_mismatch": {
"description": "What the code does when a declared check fails: reject the file (default) or warn and run with it.",
"enum": [
"reject",
"warn"
"description": "What the code does when a declared check fails: reject the file (default) or warn and run with it, for every check or check by check.",
"oneOf": [
{
"enum": [
"reject",
"warn"
]
},
{
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"patternProperties": {
"^(size|crc32|md5|sha1|sha256|adler32|signature|crypto)$": {
"enum": [
"reject",
"warn"
]
}
}
}
]
}
},
+41 -18
View File
@@ -95,31 +95,52 @@ def _entry_rule(emu_name: str, f: dict, checks: set[str]) -> dict:
},
"adler32_byteswap": bool(f.get("adler32_byteswap")),
"crypto": {c for c in checks if c in _CRYPTO_CHECKS},
# A failed check the code only warns about: the file still runs.
"warn": f.get("on_mismatch") == "warn",
# The checks whose failure the code only warns about: the file runs.
"warn": warned_checks(f, checks),
}
def warned_checks(f: dict, checks: set[str]) -> set[str]:
"""The checks of an entry the code warns about instead of refusing.
`on_mismatch: warn` covers every check; a map names them one by one, as
Azahar refuses a SecureInfo_A of the wrong size and only logs a bad
signature.
"""
level = f.get("on_mismatch")
if level == "warn":
return set(checks)
if isinstance(level, dict):
return {check for check, value in level.items() if value == "warn"} & set(checks)
return set()
def _rule_failure(
rule: dict, local_path: str, filename: str, bios_dir: str, hashes: dict,
strict: bool = False,
) -> str | None:
"""Why one entry's checks reject a file, or None when they accept it."""
actual_size = os.path.getsize(local_path)
if rule["sizes"] and actual_size not in rule["sizes"]:
accepted = ",".join(str(s) for s in sorted(rule["sizes"]))
return f"size mismatch: got {actual_size}, accepted [{accepted}]"
if rule["min_size"] is not None and actual_size < rule["min_size"]:
return f"size too small: min {rule['min_size']}, got {actual_size}"
if rule["max_size"] is not None and actual_size > rule["max_size"]:
return f"size too large: max {rule['max_size']}, got {actual_size}"
"""Why one entry's checks reject a file, or None when they accept it.
A check the code only warns about counts under ``strict`` alone.
"""
skip = set() if strict else rule["warn"]
if "size" not in skip:
actual_size = os.path.getsize(local_path)
if rule["sizes"] and actual_size not in rule["sizes"]:
accepted = ",".join(str(s) for s in sorted(rule["sizes"]))
return f"size mismatch: got {actual_size}, accepted [{accepted}]"
if rule["min_size"] is not None and actual_size < rule["min_size"]:
return f"size too small: min {rule['min_size']}, got {actual_size}"
if rule["max_size"] is not None and actual_size > rule["max_size"]:
return f"size too large: max {rule['max_size']}, got {actual_size}"
for hash_type in ("crc32", "md5", "sha1", "sha256"):
if rule[hash_type]:
if rule[hash_type] and hash_type not in skip:
if not hashes:
hashes.update(compute_hashes(local_path))
if hashes[hash_type].lower() not in rule[hash_type]:
accepted = ",".join(sorted(rule[hash_type]))
return f"{hash_type} mismatch: got {hashes[hash_type]}, accepted [{accepted}]"
if rule["adler32"]:
if rule["adler32"] and "adler32" not in skip:
if rule["adler32_byteswap"]:
actual = _adler32_byteswapped(local_path)
else:
@@ -129,7 +150,7 @@ def _rule_failure(
if actual not in rule["adler32"]:
accepted = ",".join(sorted(rule["adler32"]))
return f"adler32 mismatch: got 0x{actual}, accepted [{accepted}]"
if rule["crypto"]:
if rule["crypto"] - skip:
from crypto_verify import check_crypto_validation
return check_crypto_validation(local_path, filename, bios_dir)
@@ -182,8 +203,11 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
}
index[fname]["rules"].append(_entry_rule(emu_name, f, checks))
index[fname]["emulators"].add(emu_name)
if f.get("on_mismatch") == "warn":
# Never a rejection, so not part of the merged rejection rules.
# A warned check is never a rejection, so it stays out of the
# merged rejection rules.
warned = warned_checks(f, set(checks))
checks = [check for check in checks if check not in warned]
if not checks:
continue
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
@@ -348,8 +372,7 @@ def check_file_validation(
if not rules:
continue
reasons = [
None if rule["warn"] and not strict
else _rule_failure(rule, local_path, filename, bios_dir, hashes)
_rule_failure(rule, local_path, filename, bios_dir, hashes, strict)
for rule in rules
]
if all(reasons):
+15
View File
@@ -329,5 +329,20 @@ class AltirraHappy810(_Probe):
self.assertTrue(self.accepts("altirra", "happy-810-prev7-4k.rom", b"\5" * 5000))
class AzaharConsoleUniqueData(_Probe):
"""LoadSecureInfoA, LoadLocalFriendCodeSeedB and LoadMovable refuse a
file of the wrong size, and only log a failed signature: the data is
loaded and used (src/core/hw/unique_data.cpp:43-123, 165-202,
src/core/hle/service/cfg/cfg.cpp:405-410 at 9dd12da)."""
def test_size_refuses_and_signature_only_warns(self):
for name, size in (("SecureInfo_A", 273), ("LocalFriendCodeSeed_B", 272),
("movable.sed", 288)):
with self.subTest(name=name):
self.assertTrue(self.accepts("azahar", name, b"\0" * size))
self.assertFalse(self.accepts("azahar", name, b"\0" * size, strict=True))
self.assertFalse(self.accepts("azahar", name, b"\0" * (size + 7)))
if __name__ == "__main__":
unittest.main()