mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 20:43:50 -06:00
feat: run content checks profiles declare
validation: [romdir] walks a PS2 ROM directory the way the profile's fork reads it, masked_sha1 hashes a file with a byte range cleared, and size_multiple bounds a size to a granule. The resolver name steps, the pack and manifest scans, the emulator expansion and verify call the same content_fits. A PCSX2 companion is taken only beside an image that passes, and a companion placeholder no longer seeds a scan of its own.
This commit is contained in:
1 parent
51e5fd0dbd
commit
ec9fe03673
12 files changed
+777
-29
No files matched your search
+44
-3
@@ -15,10 +15,14 @@ from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||
from common import (
|
||||
adler32_of,
|
||||
archive_members,
|
||||
CONTENT_CHECKS,
|
||||
compute_hashes,
|
||||
content_fits,
|
||||
declared_content_checks,
|
||||
entry_system_ids,
|
||||
name_match_size_ok,
|
||||
sanitize_pack_path,
|
||||
shape_fits,
|
||||
size_fits,
|
||||
)
|
||||
from hashing import md5_composite, parse_hash_list, parse_md5_list
|
||||
@@ -35,6 +39,11 @@ def _adler32_byteswapped(path: str) -> str:
|
||||
return adler32_of(path, byteswap=True)
|
||||
|
||||
|
||||
_CONTENT_REASONS = {
|
||||
"romdir": "romdir mismatch: no RESET and ROMVER the code accepts",
|
||||
"masked_sha1": "masked_sha1 mismatch: the cleared image is not the one the code names",
|
||||
}
|
||||
|
||||
# All reproducible validation types.
|
||||
_HASH_CHECKS = frozenset({"crc32", "md5", "sha1", "adler32"})
|
||||
|
||||
@@ -75,6 +84,7 @@ def _entry_rule(emu_name: str, f: dict, checks: set[str]) -> dict:
|
||||
- {None},
|
||||
"min_size": f.get("min_size") if "size" in checks else None,
|
||||
"max_size": f.get("max_size") if "size" in checks else None,
|
||||
"size_multiple": f.get("size_multiple") if "size" in checks else None,
|
||||
"crc32": values("crc32"),
|
||||
"md5": values("md5"),
|
||||
"sha1": values("sha1"),
|
||||
@@ -84,6 +94,9 @@ 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},
|
||||
"content": {
|
||||
check: f.get(check) or {} for check in sorted(CONTENT_CHECKS & set(checks))
|
||||
},
|
||||
# The checks whose failure the code only warns about: the file runs.
|
||||
"warn": warned_checks(f, checks),
|
||||
}
|
||||
@@ -122,6 +135,8 @@ def _rule_failure(
|
||||
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}"
|
||||
if rule.get("size_multiple") and actual_size % int(rule["size_multiple"]):
|
||||
return f"size mismatch: got {actual_size}, accepted multiples of {rule['size_multiple']}"
|
||||
for hash_type in ("crc32", "md5", "sha1", "sha256"):
|
||||
if rule[hash_type] and hash_type not in skip:
|
||||
if not hashes:
|
||||
@@ -139,6 +154,9 @@ def _rule_failure(
|
||||
if actual not in rule["adler32"]:
|
||||
accepted = ",".join(sorted(rule["adler32"]))
|
||||
return f"adler32 mismatch: got 0x{actual}, accepted [{accepted}]"
|
||||
for check, spec in rule.get("content", {}).items():
|
||||
if check not in skip and not content_fits({"validation": [check], check: spec}, local_path):
|
||||
return _CONTENT_REASONS[check]
|
||||
if rule["crypto"] - skip:
|
||||
from crypto_verify import check_crypto_validation
|
||||
|
||||
@@ -253,6 +271,7 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
"sizes": set(),
|
||||
"min_size": None,
|
||||
"max_size": None,
|
||||
"size_multiples": set(),
|
||||
"crc32": set(),
|
||||
"md5": set(),
|
||||
"sha1": set(),
|
||||
@@ -288,6 +307,8 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
index[fname]["min_size"] = (
|
||||
min(cur, f["min_size"]) if cur is not None else f["min_size"]
|
||||
)
|
||||
if f.get("size_multiple"):
|
||||
index[fname]["size_multiples"].add(int(f["size_multiple"]))
|
||||
if f.get("max_size") is not None:
|
||||
cur = index[fname]["max_size"]
|
||||
index[fname]["max_size"] = (
|
||||
@@ -325,7 +346,7 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
# Per-emulator ground truth detail
|
||||
expected: dict = {}
|
||||
if "size" in checks:
|
||||
for key in ("size", "min_size", "max_size"):
|
||||
for key in ("size", "min_size", "max_size", "size_multiple"):
|
||||
if f.get(key) is not None:
|
||||
expected[key] = f[key]
|
||||
for hash_type in ("crc32", "md5", "sha1", "sha256"):
|
||||
@@ -334,6 +355,8 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
adler_val_pe = f.get("known_hash_adler32") or f.get("adler32")
|
||||
if adler_val_pe:
|
||||
expected["adler32"] = adler_val_pe
|
||||
for check in sorted(CONTENT_CHECKS & set(checks)):
|
||||
expected[check] = f.get(check) or {}
|
||||
pe_entry = {
|
||||
"checks": sorted(checks),
|
||||
"source_ref": f.get("source_ref"),
|
||||
@@ -479,6 +502,20 @@ def check_file_validation(
|
||||
if entry["max_size"] is not None and actual_size > entry["max_size"]:
|
||||
emus = _emulators_for_check("size", pe)
|
||||
return f"size too large: max {entry['max_size']}, got {actual_size}", emus
|
||||
multiples = entry.get("size_multiples") or set()
|
||||
if multiples and all(actual_size % m for m in multiples):
|
||||
emus = _emulators_for_check("size", pe)
|
||||
accepted = ",".join(str(m) for m in sorted(multiples))
|
||||
return f"size mismatch: got {actual_size}, accepted multiples of [{accepted}]", emus
|
||||
|
||||
# A content test passes when one of the entries declaring it accepts
|
||||
# the file, each read as its own code reads it.
|
||||
for check in sorted(CONTENT_CHECKS & set(checks)):
|
||||
specs = [r["content"][check] for r in entry.get("rules", []) if check in r.get("content", {})]
|
||||
if specs and not any(
|
||||
content_fits({"validation": [check], check: spec}, local_path) for spec in specs
|
||||
):
|
||||
return _CONTENT_REASONS[check], _emulators_for_check(check, pe)
|
||||
|
||||
# Hash checks: compute once, reuse for all hash types.
|
||||
# Each hash field is a set of accepted values (multiple valid ROM versions).
|
||||
@@ -855,8 +892,8 @@ def agnostic_substitute(
|
||||
held = candidate.get("path", "")
|
||||
if (
|
||||
held.startswith(prefix)
|
||||
and size_fits(entry, candidate.get("size", 0))
|
||||
and os.path.exists(held)
|
||||
and shape_fits(entry, held, candidate.get("size", 0))
|
||||
):
|
||||
return held, prefix
|
||||
break
|
||||
@@ -1148,8 +1185,12 @@ def core_size_refusal(
|
||||
if not local_path or not os.path.isfile(local_path):
|
||||
return None
|
||||
size = os.path.getsize(local_path)
|
||||
if any(name_match_size_ok(entry, size) for entry in (file_entry, *alternatives)):
|
||||
sized = [entry for entry in (file_entry, *alternatives) if name_match_size_ok(entry, size)]
|
||||
if any(content_fits(entry, local_path) for entry in sized):
|
||||
return None
|
||||
if sized:
|
||||
tests = sorted({check for entry in sized for check in declared_content_checks(entry)})
|
||||
return f"the core's {', '.join(tests)} test turns it away"
|
||||
bounds = (
|
||||
f"{file_entry['size']}" if file_entry.get("size") is not None
|
||||
else f"{file_entry.get('min_size', 0)} to {file_entry.get('max_size', 'any')}"
|
||||
|
||||
Reference in new issue
Block a user