mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
chore: merge audit fixes (integrity)
# Conflicts: # install/batocera.json # install/emudeck.json # install/lakka.json # install/recalbox.json # install/retroarch.json # install/retrobat.json # install/retrodeck.json # install/retropie.json # install/rocknix.json # install/romm.json # scripts/generate_pack.py # wiki/tools.md
This commit is contained in:
commit
378a9e4da5
19 files changed
+1199
-101
No files matched your search
+88
-22
@@ -31,6 +31,7 @@ from common import (
|
||||
get_mame_clone_map,
|
||||
list_emulator_profiles,
|
||||
list_registered_platforms,
|
||||
load_data_dir_registry,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
@@ -44,7 +45,7 @@ from common import (
|
||||
select_emulator_profiles,
|
||||
)
|
||||
from hashing import parse_hash_list
|
||||
from validation import declaration_settles, outside_gap_scope
|
||||
from validation import case_folded_slots, declaration_settles, outside_gap_scope
|
||||
|
||||
yaml = require_yaml()
|
||||
|
||||
@@ -105,6 +106,25 @@ def declared_names(
|
||||
)
|
||||
|
||||
|
||||
def folded_declared_slots(
|
||||
platforms_dir: str, platforms: list[str] | None, db: dict
|
||||
) -> frozenset[str]:
|
||||
"""The slots platforms whose filesystem folds case fill, any spelling.
|
||||
|
||||
RetroBat's 32X_G_BIOS.BIN is the file BlastEm opens as 32X_G_BIOS.bin,
|
||||
and the exact-case name test called it undeclared.
|
||||
"""
|
||||
from common import platform_declarations
|
||||
|
||||
slots: set[str] = set()
|
||||
for platform_name in platforms or list_registered_platforms(
|
||||
platforms_dir, include_archived=True
|
||||
):
|
||||
config = load_platform_config(platform_name, platforms_dir)
|
||||
slots |= case_folded_slots(config, platform_declarations(config, db))
|
||||
return frozenset(slots)
|
||||
|
||||
|
||||
def _build_supplemental_index(
|
||||
data_root: str = "data", bios_root: str = "bios"
|
||||
) -> set[str]:
|
||||
@@ -178,21 +198,28 @@ def _resolve_source(
|
||||
or ``None`` (not available anywhere).
|
||||
|
||||
A name hit whose size contradicts a size the emulator verifies is not the
|
||||
file, so it does not count as available.
|
||||
file, so it does not count as available. Nor is one whose bytes carry
|
||||
none of the hashes the entry declares: a name never wins over a declared
|
||||
hash, and zquestclassic's ending.mid read as held because Hydra Castle
|
||||
Labyrinth has another file of that name.
|
||||
"""
|
||||
declared = _declared_content(file_entry or {})
|
||||
|
||||
def _name_hit(name: str) -> bool:
|
||||
"""Whether the files indexed under *name* can be this entry."""
|
||||
def _can_be(sha1s: list | str) -> bool:
|
||||
if not file_entry or db_files is None:
|
||||
return True
|
||||
sha1s = by_name.get(name) or []
|
||||
if not isinstance(sha1s, list):
|
||||
sha1s = [sha1s]
|
||||
return any(
|
||||
name_match_size_ok(file_entry, db_files.get(sha1, {}).get("size"))
|
||||
and _record_carries(sha1, db_files.get(sha1, {}), declared)
|
||||
for sha1 in sha1s
|
||||
)
|
||||
|
||||
def _name_hit(name: str) -> bool:
|
||||
"""Whether the files indexed under *name* can be this entry."""
|
||||
return _can_be(by_name.get(name) or [])
|
||||
|
||||
# bios/ via database.json by_name
|
||||
if fname in by_name and _name_hit(fname):
|
||||
return "bios"
|
||||
@@ -209,7 +236,7 @@ def _resolve_source(
|
||||
):
|
||||
return "bios"
|
||||
# bios/ via by_path_suffix (regional variants)
|
||||
if by_path_suffix and fname in by_path_suffix:
|
||||
if by_path_suffix and fname in by_path_suffix and _can_be(by_path_suffix[fname]):
|
||||
return "bios"
|
||||
# bios/ under the canonical MAME set name, as resolve_local_file does:
|
||||
# a renamed archive is held once, under the name the dedup kept.
|
||||
@@ -252,6 +279,23 @@ def _data_hit(
|
||||
_HASH_FIELDS = ("sha1", "md5", "sha256", "crc32")
|
||||
|
||||
|
||||
def _record_carries(sha1: str, record: dict, declared: dict[str, set[str]]) -> bool:
|
||||
"""Whether a database record has one of the declared hashes, or none is declared."""
|
||||
if not declared:
|
||||
return True
|
||||
actual = {
|
||||
"sha1": sha1.lower(),
|
||||
**{field: str(record.get(field) or "").lower() for field in ("md5", "sha256", "crc32")},
|
||||
}
|
||||
return any(
|
||||
actual[field] and (
|
||||
actual[field] in values
|
||||
or (field == "md5" and any(len(v) < 32 and actual[field].startswith(v) for v in values))
|
||||
)
|
||||
for field, values in declared.items()
|
||||
)
|
||||
|
||||
|
||||
def _declared_content(file_entry: dict) -> dict[str, set[str]]:
|
||||
"""The hashes an entry declares, lowercased, by algorithm."""
|
||||
declared: dict[str, set[str]] = {}
|
||||
@@ -326,9 +370,21 @@ class FileConsumers:
|
||||
file by the evidence the pack uses, so a homonym is nobody's file.
|
||||
"""
|
||||
|
||||
def __init__(self, db: dict, zip_contents: dict | None = None) -> None:
|
||||
def __init__(
|
||||
self, db: dict, zip_contents: dict | None = None,
|
||||
data_dir_registry: dict | None = None,
|
||||
) -> None:
|
||||
self._db = db
|
||||
self._zip_contents = zip_contents
|
||||
# The builder resolves with the data trees: Dolphin's codehandler.bin
|
||||
# comes from data/dolphin-sys, and without them the name step gave it
|
||||
# Dolphin-MMJR's copy, which the site then said Dolphin reads.
|
||||
self._data_dir_registry = (
|
||||
data_dir_registry if data_dir_registry is not None
|
||||
else load_data_dir_registry(
|
||||
str(Path(__file__).resolve().parent.parent / DEFAULT_PLATFORMS_DIR)
|
||||
)
|
||||
)
|
||||
self._sha1_by_path = {
|
||||
path: sha1
|
||||
for sha1, entry in db.get("files", {}).items()
|
||||
@@ -362,7 +418,8 @@ class FileConsumers:
|
||||
if owner:
|
||||
entry = {**entry, "source_profile": owner}
|
||||
path, status = resolve_local_file(
|
||||
entry, self._db, self._zip_contents, dest_hint=dest
|
||||
entry, self._db, self._zip_contents, dest_hint=dest,
|
||||
data_dir_registry=self._data_dir_registry,
|
||||
)
|
||||
if not path or status in ("not_found", "hash_mismatch"):
|
||||
return None
|
||||
@@ -470,6 +527,7 @@ def entry_source(f: dict, index: dict) -> str | None:
|
||||
{"indexes": {"by_path_suffix": by_path_suffix}}, path_field or fname
|
||||
):
|
||||
return "bios"
|
||||
found = None
|
||||
for candidate in [fname, *([path_field] if path_field != fname else []),
|
||||
*(f.get("aliases") or [])]:
|
||||
if not candidate:
|
||||
@@ -478,12 +536,15 @@ def entry_source(f: dict, index: dict) -> str | None:
|
||||
candidate, by_name, by_name_lower, data_names, by_path_suffix, f, db_files,
|
||||
index.get("bios_dir"),
|
||||
)
|
||||
if source is not None:
|
||||
if source == "bios":
|
||||
return source
|
||||
found = found or source
|
||||
|
||||
def values(field: str) -> list[str]:
|
||||
return parse_hash_list(f.get(field))
|
||||
|
||||
# The declared content anywhere in bios/ comes before a data/ copy, as in
|
||||
# the resolver: the hash steps run before the data directories.
|
||||
held = (
|
||||
any(index["by_md5"].get(v) for v in parse_md5_list(f.get("md5")))
|
||||
or any(v in db_files for v in values("sha1"))
|
||||
@@ -491,7 +552,7 @@ def entry_source(f: dict, index: dict) -> str | None:
|
||||
or any(v in index["by_sha256"] for v in values("sha256"))
|
||||
or any(index["by_crc32"].get(v) for v in values("crc32"))
|
||||
)
|
||||
return "bios" if held else None
|
||||
return "bios" if held else found
|
||||
|
||||
|
||||
def _resolve_archive_source(
|
||||
@@ -629,7 +690,9 @@ def _cross_reference_profile(
|
||||
# Check platform declaration: by name, by an alias the core answers
|
||||
# to (RetroArch lists amiberry's cd32.rom as kick40060.CD32), or by
|
||||
# archive.
|
||||
in_platform = declaration_settles(f, fname, platform_names, effective_path)
|
||||
in_platform = declaration_settles(
|
||||
f, fname, platform_names, effective_path, index.get("folded_slots", frozenset())
|
||||
)
|
||||
|
||||
if in_platform:
|
||||
seen_files.add(seen_key)
|
||||
@@ -736,6 +799,7 @@ def cross_reference(
|
||||
data_names: set[str] | None = None,
|
||||
all_declared: dict[str, list[tuple[int | None, str]]] | None = None,
|
||||
standalone_cores: set[str] | None = None,
|
||||
folded_slots: frozenset[str] = frozenset(),
|
||||
) -> dict:
|
||||
"""Compare emulator profiles against platform declarations.
|
||||
|
||||
@@ -772,6 +836,7 @@ def cross_reference(
|
||||
"db_files": db_files,
|
||||
"data_names": data_names,
|
||||
"all_declared": all_declared,
|
||||
"folded_slots": folded_slots,
|
||||
"bios_dir": db.get("bios_dir"),
|
||||
}
|
||||
for emu_name, profile in profiles.items():
|
||||
@@ -807,11 +872,16 @@ def print_report(
|
||||
if gaps == 0 and not show_clean:
|
||||
continue
|
||||
|
||||
parts = []
|
||||
for key in ("bios", "data", "large_file", "missing"):
|
||||
count = data.get(f"gap_{key}", 0)
|
||||
if count:
|
||||
parts.append(f"{count} {key}")
|
||||
# Counted once, for the heading and the total alike: a file bundled
|
||||
# with its emulator is not one to source.
|
||||
emulator_bundled = sum(
|
||||
1 for g in data["gap_details"]
|
||||
if g.get("bundled") and g.get("source") == "missing"
|
||||
)
|
||||
counts = {key: data.get(f"gap_{key}", 0) for key in totals}
|
||||
counts["missing"] -= emulator_bundled
|
||||
counts["bundled"] = emulator_bundled
|
||||
parts = [f"{count} {key}" for key, count in counts.items() if count]
|
||||
status = ", ".join(parts) if parts else "OK"
|
||||
|
||||
systems = ", ".join(data["systems"])
|
||||
@@ -840,13 +910,8 @@ def print_report(
|
||||
|
||||
total_gaps += gaps
|
||||
for key in totals:
|
||||
totals[key] += data.get(f"gap_{key}", 0)
|
||||
emulator_bundled = sum(
|
||||
1 for g in data["gap_details"]
|
||||
if g.get("bundled") and g.get("source") == "missing"
|
||||
)
|
||||
totals[key] += counts[key]
|
||||
bundled += emulator_bundled
|
||||
totals["missing"] -= emulator_bundled
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Total: {total_gaps} undeclared files across {scope}")
|
||||
@@ -957,6 +1022,7 @@ def main() -> None:
|
||||
profiles, declared, db, plat_data_dirs, data_names,
|
||||
all_declared=declared_names(args.platforms_dir, selected, db),
|
||||
standalone_cores=standalone_cores,
|
||||
folded_slots=folded_declared_slots(args.platforms_dir, selected, db),
|
||||
)
|
||||
|
||||
if args.json:
|
||||
|
||||
Reference in new issue
Block a user