feat: let a core check warn instead of reject

This commit is contained in:
Abdessamad Derraz committed 2026-10-15 21:53:13 +02:00
1 parent 33ae27ebcf
commit 7cb4b926a3
5 files changed
+120 -52

No files matched your search

+7
View File
@@ -642,6 +642,13 @@
},
"unsourceable": {
"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"
]
}
},
"allOf": [
+8 -12
View File
@@ -85,9 +85,7 @@ from validation import (
agnostic_substitute,
frontend_digest_matches,
_build_validation_index,
check_file_validation,
filter_files_by_mode,
find_validated_variant,
)
yaml = require_yaml()
@@ -1531,17 +1529,15 @@ def generate_emulator_pack(
# The file verify --emulator credits is the one shipped:
# a dump the core's own check rejects gives way to a held
# one it accepts (azahar's otp.bin, dolphin's dsp_rom.bin).
refused = check_file_validation(
local_path, fe["name"], validation_index, bios_dir, {emu_name}
better, disagreement = validated_choice(
fe, local_path, db, validation_index, bios_dir, None,
emulators={emu_name},
)
if refused:
better = find_validated_variant(
fe, db, local_path, validation_index, bios_dir,
emulators={emu_name},
)
if not better:
refusal = refusal or f"{fe['name']} -{refused[0]}"
continue
if disagreement:
reason = disagreement.split(" says ", 1)[-1]
refusal = refusal or f"{fe['name']} -{reason}"
continue
if better != local_path:
chosen = (fe, better, None)
break
if status == "hash_mismatch":
+38 -15
View File
@@ -95,6 +95,8 @@ 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",
}
@@ -180,6 +182,9 @@ 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.
continue
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
index[fname]["crypto_only"].update(c for c in checks if c in _CRYPTO_CHECKS)
@@ -314,6 +319,7 @@ def check_file_validation(
validation_index: dict[str, dict],
bios_dir: str = "bios",
emulators: set[str] | None = None,
strict: bool = False,
) -> tuple[str, list[str]] | None:
"""Check emulator-level validation on a resolved file.
@@ -323,7 +329,9 @@ def check_file_validation(
With ``emulators``, the cores that read the file's destination, each of
them must accept it through one of its own entries for the name, the
way each runs its own check. Without, the checks of every core naming
the file are merged.
the file are merged. A check declared `on_mismatch: warn` rejects
nothing, since the core runs with the file; ``strict`` counts it, to
prefer a file that passes it too.
Returns None if all checks pass or no validation applies.
Returns (reason, emulators) tuple on failure, where *emulators*
@@ -340,7 +348,8 @@ def check_file_validation(
if not rules:
continue
reasons = [
_rule_failure(rule, local_path, filename, bios_dir, hashes)
None if rule["warn"] and not strict
else _rule_failure(rule, local_path, filename, bios_dir, hashes)
for rule in rules
]
if all(reasons):
@@ -470,6 +479,7 @@ def find_validated_variant(
bios_dir: str = "bios",
platform_digest: str | None = None,
emulators: set[str] | None = None,
strict: bool = False,
) -> str | None:
"""A held file the emulator's own checks accept, in place of current_path.
@@ -529,7 +539,7 @@ def find_validated_variant(
if accepted and str(entry.get(platform_digest, "")).lower() not in accepted:
continue
if check_file_validation(
path, fname, validation_index, bios_dir, emulators
path, fname, validation_index, bios_dir, emulators, strict
) is None:
return path
return None
@@ -668,6 +678,7 @@ def validated_choice(
platform_digest: str | None,
destination: str = "",
owners: dict | None = None,
emulators: set[str] | None = None,
) -> tuple[str | None, str | None]:
"""The file a platform destination ships, and the disagreement if any.
@@ -681,8 +692,8 @@ def validated_choice(
return local_path, None
name = file_entry.get("name", "")
rules = validation_index.get(name)
judges: set[str] | None = None
if rules and owners and destination:
judges: set[str] | None = emulators
if rules and judges is None and owners and destination:
owning = _owners_of(destination, name, owners)
if not owning & set(rules["emulators"]):
# No core that checks this name reads this destination: PC-98's
@@ -691,19 +702,31 @@ def validated_choice(
# The cores that read this destination judge it, each by its own
# entries.
judges = owning & set(rules["emulators"])
check = check_file_validation(
local_path, file_entry.get("name", ""), validation_index, bios_dir, judges
)
if not check:
def check(path: str, strict: bool):
return check_file_validation(path, name, validation_index, bios_dir, judges, strict)
def variant(strict: bool):
return find_validated_variant(
file_entry, db, local_path, validation_index, bios_dir,
platform_digest=platform_digest, emulators=judges, strict=strict,
)
if not check(local_path, True):
return local_path, None
better = find_validated_variant(
file_entry, db, local_path, validation_index, bios_dir,
platform_digest=platform_digest, emulators=judges,
)
# A held file every check passes, warn-level ones included, comes first;
# then the file itself when only a warning fails (the core runs it);
# then a held file the core at least runs.
better = variant(True)
if better:
return better, None
reason, emulators = check
return local_path, f"{', '.join(emulators)} says {reason}"
failed = check(local_path, False)
if not failed:
return local_path, None
better = variant(False)
if better:
return better, None
reason, failing = failed
return local_path, f"{', '.join(failing)} says {reason}"
def existence_discrepancy(file_entry: dict, local_path: str, platform_display: str) -> str:
+20 -25
View File
@@ -81,7 +81,6 @@ from validation import (
build_ground_truth,
check_file_validation,
filter_files_by_mode,
find_validated_variant,
outside_gap_scope,
)
@@ -1585,31 +1584,27 @@ def verify_emulator(
)
else:
# Apply emulator validation
# Judged by this emulator's own entries, as it runs its check.
check = check_file_validation(
local_path, name, validation_index, emulators={emu_name}
# Judged by this emulator's own entries, as it runs its check,
# through the selection the emulator pack makes.
chosen, disagreement = validated_choice(
file_entry, local_path, db, validation_index, DEFAULT_BIOS_DIR,
None, emulators={emu_name},
)
if check:
reason, _emus = check
better = find_validated_variant(
file_entry, db, local_path, validation_index,
emulators={emu_name},
)
if better:
result = {
"name": name,
"status": Status.OK,
"required": required,
"path": better,
}
else:
result = {
"name": name,
"status": Status.UNTESTED,
"required": required,
"path": local_path,
"reason": reason,
}
if disagreement:
result = {
"name": name,
"status": Status.UNTESTED,
"required": required,
"path": local_path,
"reason": disagreement.split(" says ", 1)[-1],
}
elif chosen != local_path:
result = {
"name": name,
"status": Status.OK,
"required": required,
"path": chosen,
}
elif resolve_status == "hash_mismatch":
# Nothing in validation: caught it, but the name matched
# while the bytes contradict the hash the profile
+47
View File
@@ -215,5 +215,52 @@ class DolphinForksJudgeTheirOwnDspTable(unittest.TestCase):
)
self.assertEqual(verdict is None, label == accepted, label)
class AWarningRejectsNothing(unittest.TestCase):
"""BeebEm warns about a short ROM and runs it; Azahar warns about a bad
signature and runs the file. A warn-level check never refuses a file, yet
a held file that passes it is preferred."""
def _setup(self, tmp, held_good: bool):
short = Path(tmp) / "a" / "OS12.rom"
good = Path(tmp) / "b" / "OS12.rom"
short.parent.mkdir()
good.parent.mkdir()
short.write_bytes(b"s" * 8192)
good.write_bytes(b"g" * 16384)
files = [short] + ([good] if held_good else [])
db = {"files": {}, "indexes": {"by_name": {"OS12.rom": []}, "by_md5": {}}}
for f in files:
sha1 = hashlib.sha1(f.read_bytes()).hexdigest()
db["files"][sha1] = {"path": str(f), "name": "OS12.rom", "size": f.stat().st_size}
db["indexes"]["by_name"]["OS12.rom"].append(sha1)
index = _build_validation_index({"beebem": {"files": [
{"name": "OS12.rom", "size": 16384, "validation": ["size"], "on_mismatch": "warn"},
]}})
return short, good, db, index
def test_the_core_runs_a_file_that_only_warns(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
short, _good, db, index = self._setup(tmp, held_good=False)
chosen, disagreement = validated_choice(
{"name": "OS12.rom"}, str(short), db, index, tmp, None, emulators={"beebem"}
)
self.assertEqual((chosen, disagreement), (str(short), None))
def test_a_file_passing_the_warned_check_is_preferred(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
short, good, db, index = self._setup(tmp, held_good=True)
chosen, disagreement = validated_choice(
{"name": "OS12.rom"}, str(short), db, index, tmp, None, emulators={"beebem"}
)
self.assertEqual((chosen, disagreement), (str(good), None))
def test_the_merged_view_rejects_nothing_for_it(self):
from validation import check_file_validation # noqa: PLC0415
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
short, _good, _db, index = self._setup(tmp, held_good=False)
self.assertIsNone(check_file_validation(str(short), "OS12.rom", index, tmp))
if __name__ == "__main__":
unittest.main()