mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-16 22:02:31 -05:00
feat: add platform conformance check to pack verification
This commit is contained in:
@@ -1171,16 +1171,102 @@ def generate_sha256sums(output_dir: str) -> str | None:
|
|||||||
return sums_path
|
return sums_path
|
||||||
|
|
||||||
|
|
||||||
def verify_and_finalize_packs(output_dir: str, db: dict) -> bool:
|
def verify_pack_against_platform(
|
||||||
|
zip_path: str, platform_name: str, platforms_dir: str,
|
||||||
|
) -> tuple[bool, int, int, list[str]]:
|
||||||
|
"""Verify a pack ZIP against its platform config.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
1. Every baseline file declared by the platform exists in the ZIP
|
||||||
|
at the correct destination path
|
||||||
|
2. No duplicate entries
|
||||||
|
3. No path anomalies (double slash, absolute, traversal)
|
||||||
|
4. No unexpected zero-byte BIOS files
|
||||||
|
|
||||||
|
Hash verification is handled by verify.py upstream (which runs
|
||||||
|
against bios/ on disk before packing). This function verifies
|
||||||
|
that the pack was assembled correctly.
|
||||||
|
|
||||||
|
Returns (all_ok, checked, present, errors).
|
||||||
|
"""
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
config = load_platform_config(platform_name, platforms_dir)
|
||||||
|
base_dest = config.get("base_destination", "")
|
||||||
|
errors: list[str] = []
|
||||||
|
checked = 0
|
||||||
|
present = 0
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||||
|
zip_set = set(zf.namelist())
|
||||||
|
zip_lower = {n.lower(): n for n in zip_set}
|
||||||
|
|
||||||
|
# Structural checks
|
||||||
|
dupes = sum(1 for c in Counter(zf.namelist()).values() if c > 1)
|
||||||
|
if dupes:
|
||||||
|
errors.append(f"{dupes} duplicate entries")
|
||||||
|
for n in zip_set:
|
||||||
|
if "//" in n:
|
||||||
|
errors.append(f"double slash: {n}")
|
||||||
|
if n.startswith("/"):
|
||||||
|
errors.append(f"absolute path: {n}")
|
||||||
|
if ".." in n:
|
||||||
|
errors.append(f"path traversal: {n}")
|
||||||
|
|
||||||
|
# Zero-byte check (exclude Dolphin GraphicMods markers)
|
||||||
|
for info in zf.infolist():
|
||||||
|
if info.file_size == 0 and not info.is_dir():
|
||||||
|
if "GraphicMods" not in info.filename and info.filename != "manifest.json":
|
||||||
|
errors.append(f"zero-byte: {info.filename}")
|
||||||
|
|
||||||
|
# Baseline file presence
|
||||||
|
for sys_id, system in config.get("systems", {}).items():
|
||||||
|
for fe in system.get("files", []):
|
||||||
|
dest = fe.get("destination", fe.get("name", ""))
|
||||||
|
if not dest:
|
||||||
|
continue
|
||||||
|
expected = f"{base_dest}/{dest}" if base_dest else dest
|
||||||
|
checked += 1
|
||||||
|
|
||||||
|
if expected in zip_set:
|
||||||
|
present += 1
|
||||||
|
elif expected.lower() in zip_lower:
|
||||||
|
present += 1 # case variant, OK on case-sensitive packs
|
||||||
|
else:
|
||||||
|
errors.append(f"missing: {expected}")
|
||||||
|
|
||||||
|
return len(errors) == 0, checked, present, errors
|
||||||
|
|
||||||
|
|
||||||
|
def verify_and_finalize_packs(output_dir: str, db: dict,
|
||||||
|
platforms_dir: str = "platforms") -> bool:
|
||||||
"""Verify all packs, inject manifests, generate SHA256SUMS.
|
"""Verify all packs, inject manifests, generate SHA256SUMS.
|
||||||
|
|
||||||
|
Two-stage verification:
|
||||||
|
1. Hash check against database.json (integrity)
|
||||||
|
2. Extract + verify against platform config (conformance)
|
||||||
|
|
||||||
Returns True if all packs pass verification.
|
Returns True if all packs pass verification.
|
||||||
"""
|
"""
|
||||||
all_ok = True
|
all_ok = True
|
||||||
|
|
||||||
|
# Map ZIP names to platform names
|
||||||
|
pack_to_platform: dict[str, list[str]] = {}
|
||||||
|
for name in sorted(os.listdir(output_dir)):
|
||||||
|
if not name.endswith(".zip"):
|
||||||
|
continue
|
||||||
|
for pname in list_registered_platforms(platforms_dir):
|
||||||
|
cfg = load_platform_config(pname, platforms_dir)
|
||||||
|
display = cfg.get("platform", pname).replace(" ", "_")
|
||||||
|
if display in name or display.replace("_", "") in name.replace("_", ""):
|
||||||
|
pack_to_platform.setdefault(name, []).append(pname)
|
||||||
|
|
||||||
for name in sorted(os.listdir(output_dir)):
|
for name in sorted(os.listdir(output_dir)):
|
||||||
if not name.endswith(".zip"):
|
if not name.endswith(".zip"):
|
||||||
continue
|
continue
|
||||||
zip_path = os.path.join(output_dir, name)
|
zip_path = os.path.join(output_dir, name)
|
||||||
|
|
||||||
|
# Stage 1: database integrity
|
||||||
ok, manifest = verify_pack(zip_path, db)
|
ok, manifest = verify_pack(zip_path, db)
|
||||||
summary = manifest["summary"]
|
summary = manifest["summary"]
|
||||||
status = "OK" if ok else "ERRORS"
|
status = "OK" if ok else "ERRORS"
|
||||||
@@ -1191,6 +1277,21 @@ def verify_and_finalize_packs(output_dir: str, db: dict) -> bool:
|
|||||||
print(f" ERROR: {err}")
|
print(f" ERROR: {err}")
|
||||||
all_ok = False
|
all_ok = False
|
||||||
inject_manifest(zip_path, manifest)
|
inject_manifest(zip_path, manifest)
|
||||||
|
|
||||||
|
# Stage 2: platform conformance (extract + verify)
|
||||||
|
platforms = pack_to_platform.get(name, [])
|
||||||
|
for pname in platforms:
|
||||||
|
p_ok, total, matched, p_errors = verify_pack_against_platform(
|
||||||
|
zip_path, pname, platforms_dir,
|
||||||
|
)
|
||||||
|
if p_ok:
|
||||||
|
print(f" platform {pname}: {matched}/{total} OK")
|
||||||
|
else:
|
||||||
|
print(f" platform {pname}: {matched}/{total} FAILED")
|
||||||
|
for err in p_errors:
|
||||||
|
print(f" {err}")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
generate_sha256sums(output_dir)
|
generate_sha256sums(output_dir)
|
||||||
return all_ok
|
return all_ok
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user