fix: take an unchecked version as a match in packs

Raze lists three sized BLOOD.RFF versions and a fourth it takes without a check, the shareware RFF the collection holds. The Batocera pack printed two DISCREPANCY lines for it that verify never showed. A slot with an unchecked sibling now accepts any copy for that core, in the validation rules and in _matches_an_alternative. When every version refuses a copy, the message names all accepted values instead of the first.
This commit is contained in:
Abdessamad Derraz committed 2026-11-05 19:37:33 +01:00
1 parent 3d0ee19bcb
commit 0d21566183
3 files changed
+84 -1

No files matched your search

+5
View File
@@ -98,6 +98,7 @@ from validation import (
validated_choice,
agnostic_substitute,
_build_validation_index,
_parse_validation,
deliverable,
with_archive_members,
extra_alternatives,
@@ -623,6 +624,10 @@ def _matches_an_alternative(fe: dict, local_path: str, profiles: dict) -> bool:
siblings = extra_alternatives(fe, profiles)
if len(siblings) < 2:
return False
# A sibling the code takes without a check (Raze's "BLOOD: Unknown
# Version") accepts whatever copy fills the slot.
if any(not _parse_validation(entry.get("validation")) for entry in siblings):
return True
actual = compute_hashes(local_path, frozenset({"sha1", "md5", "crc32"}))
for entry in siblings:
for algorithm in ("sha1", "md5", "crc32"):
+29 -1
View File
@@ -8,6 +8,7 @@ truth data for reporting.
from __future__ import annotations
import os
import re
import zipfile
from collections.abc import Callable, Iterator, Mapping, Sequence
@@ -161,6 +162,11 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
Raises ValueError if two profiles declare conflicting values.
"""
index: dict[str, dict] = {}
# A slot where one version is taken without any check (Raze's "BLOOD:
# Unknown Version", which holds the shareware RFF) accepts every copy for
# that core: its validated siblings are alternatives, not requirements.
unchecked: set[tuple[str, str, str]] = set()
checked: set[tuple[str, str, str]] = set()
for emu_name, profile in profiles.items():
if profile.get("type") in ("launcher", "alias"):
continue
@@ -169,8 +175,11 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
if not fname:
continue
checks = _parse_validation(f.get("validation"))
slot = (fname, emu_name, f.get("path") or fname)
if not checks:
unchecked.add(slot)
continue
checked.add(slot)
if fname not in index:
index[fname] = {
"checks": set(),
@@ -276,6 +285,8 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
existing["source_ref"] = pe_entry["source_ref"]
else:
pe[emu_name] = pe_entry
for fname, emu_name, _path in sorted(checked & unchecked):
index[fname]["rules"].append(_entry_rule(emu_name, {}, set()))
# Convert sets to sorted tuples/lists for determinism
for v in index.values():
v["checks"] = sorted(v["checks"])
@@ -324,6 +335,23 @@ def _emulators_for_check(
return sorted(result)
_REASON = re.compile(r"^(\w+ mismatch): got (\S+), accepted \[(.*)\]$")
def _merged_reason(reasons: list[str]) -> str:
"""One reason for every entry of a core that turned the copy away.
Raze lists three BLOOD.RFF sizes and the message named the first one
alone. Reasons of one kind are merged into a single accepted list.
"""
parsed = [_REASON.match(reason) for reason in reasons]
if all(parsed) and len({(m.group(1), m.group(2)) for m in parsed}) == 1:
accepted = sorted({v for m in parsed for v in m.group(3).split(",") if v},
key=lambda v: (len(v), v))
return f"{parsed[0].group(1)}: got {parsed[0].group(2)}, accepted [{','.join(accepted)}]"
return reasons[0]
def check_file_validation(
local_path: str,
filename: str,
@@ -363,7 +391,7 @@ def check_file_validation(
for rule in rules
]
if all(reasons):
failures[emu] = reasons[0]
failures[emu] = _merged_reason(reasons)
if not failures:
return None
first = sorted(failures)[0]
+50
View File
@@ -325,5 +325,55 @@ class AlternativesAreLookedUpBySlot(unittest.TestCase):
self.assertEqual(built.call_count, 2)
class AVersionTakenWithoutACheck(unittest.TestCase):
"""The Batocera pack printed two DISCREPANCY lines for BLOOD.RFF that
verify never showed: Raze lists three sized versions and a fourth,
"Unknown Version", it takes without a check (the shareware RFF), and
the message named one accepted size of the three."""
RAZE = {"files": [
{"name": "BLOOD.RFF", "path": "BLOOD.RFF", "size": 8200353,
"crc32": "b291418f", "validation": ["size", "crc32"]},
{"name": "BLOOD.RFF", "path": "BLOOD.RFF", "size": 9570681,
"crc32": "a8fdda84", "validation": ["size", "crc32"]},
{"name": "BLOOD.RFF", "path": "BLOOD.RFF"},
]}
def _file(self, tmp: str) -> str:
path = Path(tmp) / "BLOOD.RFF"
path.write_bytes(b"shareware")
return str(path)
def test_the_unchecked_version_accepts_the_copy(self):
from validation import check_file_validation # noqa: PLC0415
index = _build_validation_index({"raze": self.RAZE})
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
self.assertIsNone(check_file_validation(
self._file(tmp), "BLOOD.RFF", index, emulators={"raze"}
))
def test_the_pack_reads_it_as_an_alternative(self):
import generate_pack # noqa: PLC0415
fe = {"name": "BLOOD.RFF", "source_profile": "raze", "source_path": "BLOOD.RFF"}
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
self.assertTrue(generate_pack._matches_an_alternative(
fe, self._file(tmp), {"raze": self.RAZE}
))
def test_a_refusal_names_every_accepted_size(self):
from validation import check_file_validation # noqa: PLC0415
checked = {"files": self.RAZE["files"][:2]}
index = _build_validation_index({"raze": checked})
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
reason, emulators = check_file_validation(
self._file(tmp), "BLOOD.RFF", index, emulators={"raze"}
)
self.assertEqual(emulators, ["raze"])
self.assertEqual(reason, "size mismatch: got 9, accepted [8200353,9570681]")
if __name__ == "__main__":
unittest.main()