fix: judge a core file by the cores that read it

This commit is contained in:
Abdessamad Derraz committed 2026-10-10 14:05:57 +02:00
1 parent 840f9ceaf6
commit f5a5d6f4bd
14 files changed
+308 -65

No files matched your search

+6 -6
View File
@@ -4837,10 +4837,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -47899,10 +47899,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+3 -3
View File
@@ -1840,10 +1840,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
],
+6 -6
View File
@@ -1036,10 +1036,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
],
@@ -1270,10 +1270,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -6110,10 +6110,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -61637,10 +61637,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+3 -3
View File
@@ -9824,10 +9824,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -6349,10 +6349,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -61876,10 +61876,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -4813,10 +4813,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -47905,10 +47905,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -22689,10 +22689,10 @@
},
{
"dest": "bios/sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
],
@@ -25535,10 +25535,10 @@
},
{
"dest": "bios/GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -6423,10 +6423,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -101392,10 +101392,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+6 -6
View File
@@ -801,10 +801,10 @@
},
{
"dest": "sysdata/otp.bin",
"sha1": "f0332b7252e9c01b30bf5d5a63ceb22dc47968aa",
"sha256": "93c2e20cc377f2cb0fa9a2ee58c39be22e00baa94b890545e585e53b3eb4039f",
"sha1": "49489245b400f62c62faf1736b7f0a70714d3c49",
"sha256": "e2f1810ceff3d1fd4f1c08ffc1f4d47aa8efca551edd24cd5686205c71a3b13b",
"size": 256,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/otp.bin",
"repo_path": "bios/Nintendo/3DS/otp.bin",
"cores": [
"azahar"
]
@@ -2779,10 +2779,10 @@
},
{
"dest": "GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+3 -3
View File
@@ -6472,10 +6472,10 @@
},
{
"dest": "ngc/GC/dsp_rom.bin",
"sha1": "f4f683a49d7eb4155566f793f2c1c27e90159992",
"sha256": "7f33e6600722791ccee5fd15d4330af8d8f74165f7d1ded6738684c0294aaffe",
"sha1": "3c6cc6e04fdd0b2a392d7a6ed769455444846be7",
"sha256": "49d987ee1eab29a157425b82d54516957a81e1bac247c8834e494642605c3e8c",
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/Sys/GC/dsp_rom.bin",
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
],
+17 -1
View File
@@ -1068,6 +1068,16 @@ def generate_pack(
)
if status in ("not_found", "external", "user_provided") or not local_path:
continue
# The emulator's own check picks the copy, as for a declared file
# and in the emulator pack: the name led RetroArch's GC/dsp_rom.bin
# to Dolphin's obsolete v0.3.1 free ROM, beside Nintendo's coef
# ROM, a pair Dolphin warns about at every start.
local_path, disagreement = validated_choice(
fe, local_path, db, validation_index, bios_dir, None, dest,
validation_owners,
)
if disagreement:
core_discrepancies.append(f"{full_dest} -{disagreement}")
if status == "hash_mismatch":
# The core's declared hash comes from its source, the local dump
# is what the collection holds. Shipping it keeps the emulator
@@ -3100,6 +3110,8 @@ def _manifest_core_entries(
pack_only_sizes: list[int],
selection: SelectionIndex,
required_only: bool = False,
validation_index: dict | None = None,
owners: dict | None = None,
) -> int:
"""Add the files a platform's cores need but its list does not name.
@@ -3153,6 +3165,10 @@ def _manifest_core_entries(
[source_emu] if source_emu else [],
)
continue
# The copy the emulator accepts, as the pack ships it.
local_path, _disagreement = validated_choice(
fe, local_path, db, validation_index or {}, bios_dir, None, dest, owners,
)
source_emu = fe.get("source_profile") or fe.get("source_emulator", "")
@@ -3479,7 +3495,7 @@ def generate_manifest(
zip_contents, offline, region_drops, case_insensitive,
seen_destinations, seen_lower, seen_parents, manifest_files,
omitted_by_destination, record_omission, pack_only_sizes, selection,
required_only,
required_only, manifest_validation, manifest_owners,
)
# Phase 3: data directories. The installer does not fetch them, so they
+118 -6
View File
@@ -56,6 +56,84 @@ def _parse_validation(validation: list | dict | None) -> list[str]:
return []
def _entry_rule(emu_name: str, f: dict, checks: set[str]) -> dict:
"""One profile entry's own checks, kept apart from the merged view.
Merged by name, Azahar's crypto check on the 3DS otp.bin judged Cemu's
1 KiB Wii U otp.bin, and the variant search handed Cemu the 3DS file.
"""
def values(field: str) -> set[str]:
raw = f.get(field)
if not raw or field not in checks:
return set()
items = raw if isinstance(raw, list) else str(raw).split(",")
out = set()
for item in items:
norm = str(item).strip().lower()
if field == "crc32" and norm.startswith("0x"):
norm = norm[2:]
if norm:
out.add(norm)
return out
raw_size = f.get("size") if "size" in checks else None
adler = f.get("known_hash_adler32") or f.get("adler32")
adler_values = adler if isinstance(adler, list) else [adler] if adler else []
return {
"emulator": emu_name,
"checks": set(checks),
"sizes": set(raw_size if isinstance(raw_size, list) else [raw_size])
- {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,
"crc32": values("crc32"),
"md5": values("md5"),
"sha1": values("sha1"),
"sha256": values("sha256"),
"adler32": {
str(v).lower().removeprefix("0x") for v in adler_values if v
},
"adler32_byteswap": bool(f.get("adler32_byteswap")),
"crypto": {c for c in checks if c in _CRYPTO_CHECKS},
}
def _rule_failure(
rule: dict, local_path: str, filename: str, bios_dir: str, hashes: dict,
) -> 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}"
for hash_type in ("crc32", "md5", "sha1", "sha256"):
if rule[hash_type]:
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_byteswap"]:
actual = _adler32_byteswapped(local_path)
else:
if not hashes:
hashes.update(compute_hashes(local_path))
actual = hashes["adler32"].lower()
if actual not in rule["adler32"]:
accepted = ",".join(sorted(rule["adler32"]))
return f"adler32 mismatch: got 0x{actual}, accepted [{accepted}]"
if rule["crypto"]:
from crypto_verify import check_crypto_validation
return check_crypto_validation(local_path, filename, bios_dir)
return None
def _build_validation_index(profiles: dict) -> dict[str, dict]:
"""Build per-filename validation rules from emulator profiles.
@@ -98,7 +176,9 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
"crypto_only": set(),
"emulators": set(),
"per_emulator": {},
"rules": [],
}
index[fname]["rules"].append(_entry_rule(emu_name, f, checks))
index[fname]["emulators"].add(emu_name)
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
@@ -143,8 +223,8 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
# Adler32 -stored as known_hash_adler32 field (not in validation: list
# for Dolphin, but support it in both forms for future profiles)
adler_val = f.get("known_hash_adler32") or f.get("adler32")
if adler_val:
norm = adler_val.lower()
for value in adler_val if isinstance(adler_val, list) else [adler_val] if adler_val else []:
norm = str(value).lower()
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["adler32"].add(norm)
@@ -233,12 +313,18 @@ def check_file_validation(
filename: str,
validation_index: dict[str, dict],
bios_dir: str = "bios",
emulators: set[str] | None = None,
) -> tuple[str, list[str]] | None:
"""Check emulator-level validation on a resolved file.
Supports: size (exact/min/max), crc32, md5, sha1, adler32,
signature (RSA-2048 PKCS1v15 SHA256), crypto (AES-128-CBC + SHA256).
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.
Returns None if all checks pass or no validation applies.
Returns (reason, emulators) tuple on failure, where *emulators*
lists only those cores whose check actually failed.
@@ -246,6 +332,23 @@ def check_file_validation(
entry = validation_index.get(filename)
if not entry:
return None
if emulators is not None:
hashes: dict = {}
failures: dict[str, str] = {}
for emu in sorted(emulators):
rules = [r for r in entry.get("rules", []) if r["emulator"] == emu]
if not rules:
continue
reasons = [
_rule_failure(rule, local_path, filename, bios_dir, hashes)
for rule in rules
]
if all(reasons):
failures[emu] = reasons[0]
if not failures:
return None
first = sorted(failures)[0]
return failures[first], sorted(failures)
checks = entry["checks"]
pe = entry.get("per_emulator", {})
@@ -366,6 +469,7 @@ def find_validated_variant(
validation_index: dict,
bios_dir: str = "bios",
platform_digest: str | None = None,
emulators: set[str] | None = None,
) -> str | None:
"""A held file the emulator's own checks accept, in place of current_path.
@@ -424,7 +528,9 @@ def find_validated_variant(
seen.add(real)
if accepted and str(entry.get(platform_digest, "")).lower() not in accepted:
continue
if check_file_validation(path, fname, validation_index, bios_dir) is None:
if check_file_validation(
path, fname, validation_index, bios_dir, emulators
) is None:
return path
return None
@@ -575,18 +681,24 @@ 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:
owning = _owners_of(destination, name, owners)
if owning and not owning & set(rules["emulators"]):
if not owning & set(rules["emulators"]):
# No core that checks this name reads this destination: PC-98's
# bios.rom rules have no say over roms/ibmpcjr/bios.rom.
return local_path, None
# 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
local_path, file_entry.get("name", ""), validation_index, bios_dir, judges
)
if not check:
return local_path, None
better = find_validated_variant(
file_entry, db, local_path, validation_index, bios_dir,
platform_digest=platform_digest,
platform_digest=platform_digest, emulators=judges,
)
if better:
return better, None
+116 -1
View File
@@ -97,8 +97,123 @@ class ValidatedChoice(unittest.TestCase):
for call in ast.walk(node)
)
}
self.assertLessEqual({"generate_pack", "generate_manifest"}, callers)
self.assertLessEqual(
{"generate_pack", "generate_manifest", "_manifest_core_entries"}, callers
)
def test_core_extras_are_judged_too(self):
"""The baseline loop judged a platform's own files; the core-extras loop
shipped whatever the name found. Both loops live in generate_pack."""
tree = ast.parse((REPO_ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8"))
builder = next(
node for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef) and node.name == "generate_pack"
)
calls = [
call for call in ast.walk(builder)
if isinstance(call, ast.Call) and getattr(call.func, "id", None) == "validated_choice"
]
self.assertGreaterEqual(len(calls), 2)
class ManifestsServeWhatTheCoreAccepts(unittest.TestCase):
"""RetroArch's GC/dsp_rom.bin was Dolphin's obsolete v0.3.1 free ROM while
Nintendo's dump, which Dolphin's check accepts, sat in .variants/."""
def test_no_served_core_file_fails_its_check_beside_a_passing_variant(self):
import json # noqa: PLC0415
from common import load_emulator_profiles, load_platform_config # noqa: PLC0415
from generate_pack import _platform_cores # noqa: PLC0415
from validation import check_file_validation # noqa: PLC0415
manifest_path = REPO_ROOT / "install" / "retroarch.json"
if not (REPO_ROOT / "database.json").is_file() or not manifest_path.is_file():
self.skipTest("database.json or the manifest is not built")
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
config = load_platform_config("retroarch", str(REPO_ROOT / "platforms"))
cores = {n: profiles[n] for n in _platform_cores(config, profiles)}
index = _build_validation_index(cores)
owners = destination_owners(cores)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
wrong = []
for entry in manifest["files"]:
name = entry["dest"].rsplit("/", 1)[-1]
path = entry.get("repo_path")
if not entry.get("cores") or not path or name not in index:
continue
local = str(REPO_ROOT / path)
if not Path(local).is_file() or not check_file_validation(local, name, index):
continue
chosen, _ = validated_choice(
{"name": name}, local, db, index, str(REPO_ROOT / "bios"), None,
entry["dest"], owners,
)
if chosen != local:
wrong.append(entry["dest"])
self.assertEqual(wrong, [])
class EachReaderJudgesWithItsOwnRules(unittest.TestCase):
"""Merged by name, Azahar's 256-byte 3DS otp.bin rule judged Cemu's 1 KiB
Wii U otp.bin, and the variant search gave Cemu the 3DS file."""
def test_rules_of_another_emulator_do_not_judge(self):
from validation import check_file_validation # noqa: PLC0415
index = _build_validation_index({
"azahar": {"files": [{"name": "otp.bin", "path": "sysdata/otp.bin",
"size": 256, "validation": ["size"]}]},
"cemu": {"files": [{"name": "otp.bin", "path": "Cemu/otp.bin",
"size": 1024, "validation": ["size"]}]},
})
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
wiiu = Path(tmp) / "otp.bin"
wiiu.write_bytes(b"w" * 1024)
self.assertIsNone(check_file_validation(str(wiiu), "otp.bin", index, tmp, {"cemu"}))
self.assertIsNotNone(check_file_validation(str(wiiu), "otp.bin", index, tmp, {"azahar"}))
def test_one_of_an_emulators_entries_is_enough(self):
from validation import check_file_validation # noqa: PLC0415
index = _build_validation_index({"raze": {"files": [
{"name": "SW.GRP", "size": 100, "validation": ["size"]},
{"name": "SW.GRP", "size": 200, "validation": ["size"]},
]}})
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
grp = Path(tmp) / "SW.GRP"
grp.write_bytes(b"s" * 200)
self.assertIsNone(check_file_validation(str(grp), "SW.GRP", index, tmp, {"raze"}))
class DolphinForksJudgeTheirOwnDspTable(unittest.TestCase):
"""VerifyRoms (Source/Core/Core/DSP/DSPCore.cpp) accepts, without a prompt,
Nintendo's pair and the newest free pair its table knows: v0.4 in
Dolphin, PrimeHack, MMJR2 and Triforce, v0.3.1 in MMJR and Ishiiruka,
whose table stops there. Adler-32 over the byte-swapped words."""
FREE_V04 = REPO_ROOT / "data" / "dolphin-sys" / "GC" / "dsp_rom.bin"
FREE_V031 = REPO_ROOT / "bios" / "Nintendo" / "GameCube" / "Sys" / "GC" / "dsp_rom.bin"
def test_each_fork_accepts_its_own_free_rom(self):
from common import load_emulator_profiles # noqa: PLC0415
from validation import check_file_validation # noqa: PLC0415
if not self.FREE_V04.is_file() or not self.FREE_V031.is_file():
self.skipTest("free DSP ROMs not present")
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
expect = {"dolphin": "v04", "primehack": "v04", "dolphin-mmjr2": "v04",
"triforce": "v04", "dolphin-mmjr": "v031", "ishiiruka": "v031"}
for name, accepted in expect.items():
index = _build_validation_index({name: profiles[name]})
with self.subTest(emulator=name):
for label, path in (("v04", self.FREE_V04), ("v031", self.FREE_V031)):
verdict = check_file_validation(
str(path), "dsp_rom.bin", index, "bios", {name}
)
self.assertEqual(verdict is None, label == accepted, label)
if __name__ == "__main__":
unittest.main()