mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
docs: state each verification rule once
architecture.md drew path_suffix ahead of SHA1 and listed a core resolution that missed the retroarch list entry and built_by. Both now link to the one statement that matches common.py. The Recalbox table read hashMatchMandatory into verify.py, which never reads it, and md5_composite hashes member contents, not member MD5s (Zip.cpp:71-96). The validation index is built from the platform's resolved profiles only. The requirement table gains the fields that make a platform YAML reversible, and pages that name a platform list say what changes rather than judging it.
This commit is contained in:
1 parent
9edf785513
commit
5e70fe7ff5
7 files changed
+249
-110
No files matched your search
@@ -102,6 +102,31 @@ class InstallerOutputExample(unittest.TestCase):
|
||||
self.assertEqual(menu.groups(), (count, size))
|
||||
|
||||
|
||||
class OneResolutionChain(unittest.TestCase):
|
||||
"""architecture.md drew path_suffix as the first step, before SHA1, while
|
||||
the code and verification-modes.md try it after every hash."""
|
||||
|
||||
ORDER = ("SHA1", "SHA256", "CRC32", "MD5", "Path suffix")
|
||||
|
||||
def test_the_table_follows_the_code(self):
|
||||
import common
|
||||
|
||||
source = inspect.getsource(common.resolve_local_file)
|
||||
steps = re.findall(r"# \d+[a-z]?\. (SHA1|SHA256|CRC32|MD5|Path suffix)", source)
|
||||
self.assertEqual(tuple(dict.fromkeys(steps)), self.ORDER)
|
||||
table = _table_rows(_section(_page("verification-modes.md"), "File resolution chain"))
|
||||
methods = [row[1] for row in table[: len(self.ORDER)]]
|
||||
self.assertEqual(
|
||||
[next(m for m in self.ORDER if method.startswith(m)) for method in methods],
|
||||
list(self.ORDER),
|
||||
)
|
||||
|
||||
def test_architecture_links_instead_of_drawing_a_second_one(self):
|
||||
section = _section(_page("architecture.md"), "File resolution chain")
|
||||
self.assertNotIn("```mermaid", section)
|
||||
self.assertIn("verification-modes.md#file-resolution-chain", section)
|
||||
|
||||
|
||||
class ReleaseRefPromise(unittest.TestCase):
|
||||
"""RETROBIOS_REF was said to pin a reproducible file set, while every
|
||||
release deletes the previous tags and large files come from one fixed
|
||||
@@ -124,6 +149,30 @@ class ReleaseRefPromise(unittest.TestCase):
|
||||
self.assertNotIn("reproducible", paragraph, page.name)
|
||||
|
||||
|
||||
class RequirementFieldsTable(unittest.TestCase):
|
||||
"""The table stopped at native_id: native_path, mirror_of, alt_md5 and
|
||||
the native dict, the fields that make a platform YAML reversible, were
|
||||
named nowhere a scraper author reads first."""
|
||||
|
||||
def test_every_field_and_native_key_is_listed(self):
|
||||
from dataclasses import fields
|
||||
|
||||
from scraper.base_scraper import BiosRequirement
|
||||
|
||||
section = _section(_page("adding-a-platform.md"), "BiosRequirement fields")
|
||||
listed = {row[0].strip("`") for row in _table_rows(section)}
|
||||
self.assertEqual(listed, {f.name for f in fields(BiosRequirement)})
|
||||
native_row = next(row for row in _table_rows(section) if row[0] == "`native`")[2]
|
||||
keys: set[str] = set()
|
||||
for path in (ROOT / "scripts" / "scraper").glob("*_scraper.py"):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
keys |= set(re.findall(r'native\["(\w+)"\]', source))
|
||||
keys |= set(re.findall(r'native(?:: dict\[str, object\])?\s*=\s*\{"(\w+)"', source))
|
||||
self.assertTrue(keys)
|
||||
for key in keys:
|
||||
self.assertIn(f"`{key}`", native_row)
|
||||
|
||||
|
||||
class ReleaseCommitCarriesTheManifests(unittest.TestCase):
|
||||
"""The documented release commit left out install/*.json, which the
|
||||
release pipeline regenerates: v2026.10.10 added three by hand."""
|
||||
@@ -170,6 +219,108 @@ class NoHedgedRules(unittest.TestCase):
|
||||
self.assertEqual(offenders, [], "\n".join(offenders))
|
||||
|
||||
|
||||
class PeerToneTowardPlatforms(unittest.TestCase):
|
||||
"""Platform lists were called wrong, stale or corrected on public pages
|
||||
that their maintainers read."""
|
||||
|
||||
WORDS = re.compile(
|
||||
r"(?i)\bgets wrong\b|\bstale (hash|entries|entry)\b|<their file>|\bpatch their\b"
|
||||
r"|, corrected\b|\bcorrected in place\b|\bdeclare incorrectly\b|\blist error\b"
|
||||
)
|
||||
|
||||
def test_published_pages(self):
|
||||
offenders = [
|
||||
f"{page.name}: {line[:80]}"
|
||||
for page in PUBLISHED
|
||||
for line in page.read_text(encoding="utf-8").splitlines()
|
||||
if self.WORDS.search(line)
|
||||
]
|
||||
self.assertEqual(offenders, [], "\n".join(offenders))
|
||||
|
||||
|
||||
class RecalboxSeverity(unittest.TestCase):
|
||||
"""The table gave verify.py a mapping from hashMatchMandatory that the
|
||||
code never reads, and called a missing optional file GREEN."""
|
||||
|
||||
def test_the_verify_column_is_what_compute_severity_returns(self):
|
||||
from verify import Status, compute_severity
|
||||
|
||||
section = _section(_page("verification-modes.md"), "Mandatory levels (Recalbox)")
|
||||
rows = _table_rows(section)
|
||||
self.assertTrue(rows)
|
||||
status = {"hash matches": Status.OK, "missing": Status.MISSING, "hash differs": Status.UNTESTED}
|
||||
for state, mandatory, _hash_match, _colour, expected in rows:
|
||||
for required in ((True, False) if mandatory == "(any)" else (mandatory == "true",)):
|
||||
self.assertEqual(
|
||||
compute_severity(status[state], required, "md5").upper(), expected,
|
||||
f"{state} mandatory={mandatory}",
|
||||
)
|
||||
self.assertNotIn("severity levels", _page("tools.md"))
|
||||
|
||||
|
||||
class Md5CompositeDefinition(unittest.TestCase):
|
||||
"""The page described the MD5 of the members' MD5s, the code hashes the
|
||||
members' contents in name order."""
|
||||
|
||||
def test_page_and_code_agree(self):
|
||||
from hashing import md5_composite_of
|
||||
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w") as zf:
|
||||
zf.writestr("b.rom", b"BBBB")
|
||||
zf.writestr("a.rom", b"AAAA")
|
||||
buffer.seek(0)
|
||||
self.assertEqual(md5_composite_of(buffer), hashlib.md5(b"AAAABBBB").hexdigest())
|
||||
section = _section(_page("verification-modes.md"), "md5_composite (Recalbox ZIP verification)")
|
||||
self.assertIn("contents of every member", section)
|
||||
self.assertNotIn("inner file MD5s", section)
|
||||
|
||||
|
||||
class ValidationIndexScope(unittest.TestCase):
|
||||
"""The page said the index reads every profile. verify and the builder
|
||||
pass it only the profiles the platform resolves."""
|
||||
|
||||
def test_page_names_the_scope_the_code_uses(self):
|
||||
import verify
|
||||
|
||||
source = inspect.getsource(verify.verify_platform)
|
||||
self.assertRegex(source, r"plat_cores = resolve_platform_cores\(config, profiles\)")
|
||||
self.assertIn("_build_validation_index(platform_profiles)", source)
|
||||
section = _section(_page("verification-modes.md"), "Validation index")
|
||||
self.assertIn("resolve_platform_cores", section)
|
||||
self.assertNotIn("reads all emulator profiles", section)
|
||||
|
||||
|
||||
class CoreResolutionWrittenOnce(unittest.TestCase):
|
||||
"""architecture.md and adding-a-platform.md each stated the core
|
||||
resolution rule, and only one knew that a list naming retroarch takes
|
||||
every libretro profile. Neither said built_by profiles stay out."""
|
||||
|
||||
def test_one_statement_matching_the_platforms_and_the_code(self):
|
||||
import yaml
|
||||
|
||||
import common
|
||||
|
||||
self.assertNotIn("cores: [list]", _page("architecture.md"))
|
||||
source = inspect.getsource(common.resolve_platform_cores)
|
||||
self.assertIn('not p.get("built_by")', source)
|
||||
self.assertIn('"retroarch" in core_set', source)
|
||||
naming = set()
|
||||
for path in (ROOT / "platforms").glob("*.yml"):
|
||||
config = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
cores = config.get("cores")
|
||||
if isinstance(cores, list) and "retroarch" in map(str, cores):
|
||||
naming.add(str(config.get("platform") or path.stem).lower())
|
||||
section = _section(_page("adding-a-platform.md"), "Required fields")
|
||||
bullet = next(p for p in section.split("\n- ") if p.startswith("**Explicit list**"))
|
||||
used_by = re.search(r"Used by ([^.]+)\.", bullet.replace("\n", " ")).group(1)
|
||||
names = [n.strip().lower() for n in used_by.split(",")]
|
||||
self.assertIn("the last\n four", bullet)
|
||||
self.assertEqual(set(names[-4:]), naming)
|
||||
libretro = next(p for p in section.split("\n- ") if p.startswith("**all_libretro**"))
|
||||
self.assertIn("built_by", libretro)
|
||||
|
||||
|
||||
class ExitStatusTable(unittest.TestCase):
|
||||
"""Code 2 was documented as 'every file failed' only, while argparse
|
||||
exits 2 on every refused option, and the 141 example named an option
|
||||
|
||||
+34
-18
@@ -67,7 +67,21 @@ def validate_format(self, raw_data: str) -> bool:
|
||||
| `destination` | `str` | Relative path within the BIOS directory |
|
||||
| `required` | `bool` | Whether the platform considers this file mandatory |
|
||||
| `zipped_file` | `str \| None` | If set, the hash refers to a ROM inside a ZIP |
|
||||
| `native_id` | `str \| None` | Original system name before normalization |
|
||||
| `native_id` | `str \| None` | The platform's own system id, written as `native_system` |
|
||||
| `sha256` | `str \| None` | SHA256 hash if available |
|
||||
| `alt_md5` | `str \| None` | A second accepted MD5 (Batocera and ROCKNIX `altmd5`) |
|
||||
| `native_path` | `str \| None` | The file reference as the platform writes it, kept when it differs from `name`. libretro writes `iplromco.dat` bare although the file lives under `keropi/`, and `ep128emu/roms/cpc464.rom` with its directory |
|
||||
| `mirror_of` | `str \| None` | The native system whose entry this one repeats under another system. The platform's own file does not carry the copy, so the export skips it |
|
||||
| `native` | `dict` | Fields the platform declares that the model has no place for, written back unchanged by the export: `native_name`, `core`, `note`, `description`, `hash_match_mandatory`, `mandatory_declared`, `alt_paths`, `component`, `required_label` |
|
||||
|
||||
`requirement_entry()` in `base_scraper.py` is the single serializer from a
|
||||
requirement to a platform YAML entry, and every scraper goes through it.
|
||||
Anything an export writes back is recorded by the scraper, never rebuilt by the
|
||||
exporter: several native systems can fall onto one slug (Recalbox files
|
||||
`pcengine`, `pcenginecd` and `supergrafx` under `nec-pc-engine`), and a path
|
||||
or a label the YAML does not hold cannot be derived. When an export needs a
|
||||
value the YAML lacks, the scraper and `schemas/platform.schema.json` gain it,
|
||||
then the platform is scraped again.
|
||||
|
||||
### System ID mapping
|
||||
|
||||
@@ -147,16 +161,22 @@ none is edited by hand: RetroPie inherits RetroArch's file set and its scraper
|
||||
writes the packages it builds, read from the `rp_module_id` of each
|
||||
scriptmodule, and any manual change is overwritten on the next scrape.
|
||||
|
||||
The `cores` field determines which emulator profiles are resolved for this platform.
|
||||
Three strategies exist:
|
||||
The `cores` field determines which emulator profiles are resolved for this
|
||||
platform (`resolve_platform_cores` in `common.py`). Alias profiles are never
|
||||
resolved. Three strategies exist:
|
||||
|
||||
- **Explicit list**: `cores: [beetle_psx, dolphin, ...]` - match by profile key name
|
||||
or by any name in a profile's `cores:`. Used by Batocera, Recalbox, RetroBat, RomM,
|
||||
RetroPie, EmuDeck, RetroDECK, ROCKNIX. A list that names `retroarch` (the last four)
|
||||
also takes every libretro profile.
|
||||
- **all_libretro**: `cores: all_libretro` - include every profile with `type: libretro`
|
||||
or `type: standalone + libretro`. Used by RetroArch and Lakka.
|
||||
- **Omitted**: fallback to system ID intersection.
|
||||
or by any name in a profile's `cores:`. When a profile's `built_by` names this
|
||||
platform, that profile answers for the name. Used by Batocera, Recalbox, RetroBat,
|
||||
RomM, RetroPie, EmuDeck, RetroDECK, ROCKNIX. A list that names `retroarch` (the last
|
||||
four) also takes every libretro profile, as `all_libretro` does. An empty list
|
||||
(BizHawk, MiSTer FPGA) resolves no profile, and the pack holds the platform's own
|
||||
files.
|
||||
- **all_libretro**: `cores: all_libretro` - every profile whose `type` contains
|
||||
`libretro`, except those that carry `built_by`: a build a platform compiles itself
|
||||
is absent from the buildbot and is reached only by its name (Recalbox's `mame0258`
|
||||
and `mame0278`). Used by RetroArch and Lakka.
|
||||
- **Omitted**: fallback to system ID intersection, `built_by` profiles excluded.
|
||||
|
||||
Two lists say how each resolved emulator is laid out. `standalone_cores` names the
|
||||
ones the platform launches as standalone apps: their files go to `standalone_path`.
|
||||
@@ -397,17 +417,13 @@ class Exporter(BaseExporter):
|
||||
|
||||
### Round-trip validation
|
||||
|
||||
```bash
|
||||
python -m scripts.scraper.myplatform_scraper --output platforms/myplatform.yml
|
||||
python scripts/export_native.py --platform myplatform --fetch --output-dir tmp/native
|
||||
diff .cache/upstream-native/myplatform/<their file> \
|
||||
tmp/native/myplatform/<their file>
|
||||
```
|
||||
|
||||
The export must not lose anything the platform already declares. If your
|
||||
The scraper and the exporter are checked against each other as
|
||||
[round-trip testing](adding-a-scraper.md#round-trip-testing) describes. The
|
||||
export must not lose anything the platform already declares. If your
|
||||
format carries code as well as data (a script, a C# database, a manifest
|
||||
whose BIOS list sits beside launch configuration), say so with
|
||||
`needs_original()` and patch their file rather than writing a new one. An
|
||||
`needs_original()` and patch the platform's original file rather than writing
|
||||
a new one. An
|
||||
exporter that regenerates such a file from BIOS data alone hands the
|
||||
maintainer something that no longer runs.
|
||||
|
||||
|
||||
@@ -425,19 +425,19 @@ be missing from the round trip.
|
||||
# Scrape upstream -> platform YAML
|
||||
python -m scripts.scraper.myplatform_scraper --output platforms/myplatform.yml
|
||||
|
||||
# Rewrite the platform's own file, corrected
|
||||
# Rewrite the platform's own file with the changes applied
|
||||
python scripts/export_native.py --platform myplatform --fetch \
|
||||
--output-dir tmp/native
|
||||
|
||||
# Compare with the file upstream publishes, cached by --fetch
|
||||
diff .cache/upstream-native/myplatform/<their file> \
|
||||
tmp/native/myplatform/<their file>
|
||||
diff .cache/upstream-native/myplatform/<original file> \
|
||||
tmp/native/myplatform/<original file>
|
||||
```
|
||||
|
||||
Every line in that diff should be a correction you can defend. A line that
|
||||
merely reshapes their formatting means the scraper lost something the
|
||||
exporter then had to invent: add it to the requirement instead, through
|
||||
`native_path`, `native_system` or the `native` dict.
|
||||
Every line of that diff is a change the emulator source supports. A line that
|
||||
only reshapes the original formatting means the scraper lost something the
|
||||
exporter then had to invent: record it in the requirement instead, through
|
||||
the fields listed in [BiosRequirement fields](adding-a-platform.md#biosrequirement-fields).
|
||||
|
||||
### Common issues
|
||||
|
||||
|
||||
@@ -297,8 +297,8 @@ without `--target`.
|
||||
|
||||
Truth data is ground truth generated from emulator profiles. It represents what each
|
||||
core actually needs based on source code analysis, independent of what platform
|
||||
scrapers declare. The purpose is gap analysis: finding files that platforms miss
|
||||
or declare incorrectly.
|
||||
scrapers declare. The purpose is gap analysis: finding the files the code loads that a platform
|
||||
list does not name, and the values that differ from the code.
|
||||
|
||||
### Generate truth
|
||||
|
||||
|
||||
+13
-52
@@ -88,7 +88,7 @@ emulators/*.yml verify.py checks generate_pack.py resolves
|
||||
|
||||
truth.py generates diff_truth.py export_native.py
|
||||
ground truth from compares truth vs rewrites each platform's
|
||||
emulator profiles scraped platform own file, corrected
|
||||
emulator profiles scraped platform own file, updated
|
||||
```
|
||||
|
||||
Pipeline runs all steps in sequence: DB, provenance report, data dirs,
|
||||
@@ -127,14 +127,13 @@ graph LR
|
||||
|
||||
The pack combines platform baseline (layer 1) with core requirements (layer 3).
|
||||
Neither too much (no files from unused cores) nor too few (no missing files for active cores).
|
||||
A same-named local file that contradicts an explicit hash is recorded as an
|
||||
unsafe omission on hash-verifying platforms, while existence platforms ship it and
|
||||
report the divergence, because their code never reads the bytes.
|
||||
A same-named local file that contradicts a declared hash is settled by the
|
||||
platform's verification mode ([discrepancy detection](verification-modes.md#discrepancy-detection)).
|
||||
|
||||
The emulator's source code serves as ground truth for what files are needed,
|
||||
what names they use, and what validation the emulator performs. Platform YAML
|
||||
configs are scraped from upstream and are generally accurate, though they can
|
||||
occasionally have gaps or stale entries. The emulator profiles complement the
|
||||
configs are scraped from upstream and record what each platform declares. A
|
||||
list kept by hand can lag behind an emulator release. The emulator profiles complement the
|
||||
platform data by documenting what the code actually loads. When the two disagree,
|
||||
the profile takes precedence for pack generation: files the code needs are included
|
||||
even if the platform does not declare them. Files the platform declares but no
|
||||
@@ -143,7 +142,8 @@ upstream may cover cases not yet profiled.
|
||||
|
||||
A platform YAML is regenerated by its scraper and never edited by hand: an
|
||||
edit would be lost at the next scrape, and the file would stop describing the
|
||||
platform. What the platform gets wrong is answered in the other two layers.
|
||||
platform. Where the emulator source states something else, the profile and
|
||||
`_shared.yml` carry it.
|
||||
Another name for the same file is an `aliases:` entry in the profile, never a
|
||||
second entry in a platform YAML or in `_shared.yml`. A name a platform's own
|
||||
launcher reads, such as the 22 names Batocera's DuckStation generator tries
|
||||
@@ -373,46 +373,10 @@ platform's own verification mode.
|
||||
|
||||
### File resolution chain
|
||||
|
||||
`resolve_local_file` in `common.py` tries each strategy in order, returning the
|
||||
first match. Used by both `verify.py` and `generate_pack.py`.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
START([resolve_local_file]) --> S0{path_suffix<br/>exact match?}
|
||||
S0 -- yes --> EXACT([exact])
|
||||
S0 -- no --> S1{SHA1<br/>exact match?}
|
||||
S1 -- yes --> EXACT
|
||||
S1 -- no --> S1B{SHA256<br/>exact match?}
|
||||
S1B -- yes --> EXACT
|
||||
S1B -- no --> S1C{CRC32 + size,<br/>no stronger hash?}
|
||||
S1C -- yes --> EXACT
|
||||
S1C -- no --> S2{MD5 direct<br/>or truncated?}
|
||||
S2 -- yes --> MD5([md5_exact])
|
||||
S2 -- no --> S3{name + aliases<br/>no MD5?}
|
||||
S3 -- yes --> EXACT
|
||||
S3 -- no --> S4{name + aliases<br/>md5_composite /<br/>direct MD5?}
|
||||
S4 -- match --> EXACT
|
||||
S4 -- name only --> HM([hash_mismatch])
|
||||
S4 -- no --> S5{zippedFile<br/>inner ROM MD5?}
|
||||
S5 -- yes --> ZE([zip_exact])
|
||||
S5 -- no --> S6{MAME clone<br/>map lookup?}
|
||||
S6 -- yes --> MC([mame_clone])
|
||||
S6 -- no --> S7{data_dir<br/>cache scan?}
|
||||
S7 -- yes --> DD([data_dir])
|
||||
S7 -- no --> S8{agnostic<br/>fallback?}
|
||||
S8 -- yes --> AG([agnostic_fallback])
|
||||
S8 -- no --> NF([not_found])
|
||||
|
||||
style START fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style EXACT fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style MD5 fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style HM fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style ZE fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style MC fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style DD fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style AG fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
style NF fill:#2d333b,stroke:#adbac7,color:#adbac7
|
||||
```
|
||||
`resolve_local_file` in `common.py` turns a file entry into a local path, and
|
||||
`verify.py` and `generate_pack.py` both call it. Every declared hash is tried
|
||||
before a path or a name, so a path never wins over a hash. The steps, in order,
|
||||
are listed under [file resolution chain](verification-modes.md#file-resolution-chain).
|
||||
|
||||
## Platform inheritance
|
||||
|
||||
@@ -420,11 +384,8 @@ Platform configs support `inherits:` to share definitions.
|
||||
Lakka inherits from RetroArch, RetroPie inherits from RetroArch with `base_destination: BIOS`.
|
||||
`overrides:` allows child platforms to modify specific systems from the parent.
|
||||
|
||||
Core resolution (`resolve_platform_cores`) uses three strategies:
|
||||
|
||||
- `cores: all_libretro` - include all profiles with `libretro` in their type
|
||||
- `cores: [list]` - include only named profiles
|
||||
- `cores:` absent - fallback to system ID intersection between platform and profiles
|
||||
Which emulator profiles a platform resolves follows its `cores` field, as
|
||||
[adding a platform](adding-a-platform.md#required-fields) describes.
|
||||
|
||||
## Hardware target filtering
|
||||
|
||||
|
||||
+4
-8
@@ -129,7 +129,7 @@ Verification modes per platform:
|
||||
|----------|------|-------|
|
||||
| RetroArch, Lakka, RetroPie | existence | file present = OK |
|
||||
| Batocera, RetroBat | md5 | MD5 hash match, plus `checkInsideZip` for `zippedFile` entries |
|
||||
| Recalbox | md5 | MD5 multi-hash, `md5_composite` for ZIPs, 3 severity levels |
|
||||
| Recalbox | md5 | MD5 multi-hash, `md5_composite` for ZIPs |
|
||||
| EmuDeck | md5 | MD5 whitelist per system |
|
||||
| RetroDECK | md5 | MD5 per file via component manifests |
|
||||
| RomM | md5 | size + any hash (MD5/SHA1/CRC32), no ZIP inspection |
|
||||
@@ -203,12 +203,8 @@ Packs include platform baseline files plus files required by the platform's core
|
||||
When a file passes platform verification but fails emulator validation,
|
||||
the tool searches for a variant that satisfies both.
|
||||
If none exists, the platform version is kept and the discrepancy is reported.
|
||||
What happens when a declared hash and the local dump disagree follows the
|
||||
platform's own verification mode. A hash platform would reject the file, so it
|
||||
is omitted and recorded in the pack report and the installer manifest. An
|
||||
existence platform reads no bytes, so the file is packed and the divergence is
|
||||
reported instead: an upstream list error must not remove a file the frontend
|
||||
would have loaded.
|
||||
A declared hash that the local dump contradicts is settled by the platform's
|
||||
own verification mode ([discrepancy detection](verification-modes.md#discrepancy-detection)).
|
||||
|
||||
### Granular options
|
||||
|
||||
@@ -292,7 +288,7 @@ python scripts/export_native.py --all --refresh-cache
|
||||
A list is written in the order the code looks: `priority:` first (lowest
|
||||
wins), then the profile's own declaration order. What a format cannot
|
||||
state is reported rather than written: EmuDeck's arrays are shared
|
||||
between emulators its file does not name, so a hash is corrected in place
|
||||
between emulators its file does not name, so a hash is updated in place
|
||||
and never added.
|
||||
|
||||
`exports/<platform>.json` lists every change under `changes`, each with
|
||||
|
||||
+38
-23
@@ -57,10 +57,11 @@ the actual hash is compared against only its first N characters.
|
||||
|
||||
### md5_composite (Recalbox ZIP verification)
|
||||
|
||||
Recalbox computes `Zip::Md5Composite` for ZIP files: the MD5 of the concatenation of all
|
||||
inner file MD5s (sorted by filename). `verify.py` replicates this with `md5_composite()`
|
||||
from `common.py`. When a ZIP file's direct MD5 does not match, the composite is tried
|
||||
before reporting a mismatch.
|
||||
Recalbox hashes a ZIP with `Zip::Md5Composite` (`es-core/src/utils/Zip.cpp:71-96` in
|
||||
recalbox-emulationstation at `f4cd0ae4`): one MD5 over the contents of every member,
|
||||
read in filename order. The members' own MD5s are not involved. `verify.py` computes
|
||||
the same value with `md5_composite()` (`scripts/hashing.py`). When a ZIP file's direct
|
||||
MD5 does not match, the composite is tried before reporting a mismatch.
|
||||
|
||||
### Multi-hash (Recalbox)
|
||||
|
||||
@@ -70,14 +71,24 @@ listed hash is OK.
|
||||
|
||||
### Mandatory levels (Recalbox)
|
||||
|
||||
Recalbox uses three severity levels derived from two YAML fields (`mandatory` and
|
||||
`hashMatchMandatory`):
|
||||
Recalbox colours each entry of `es_bios.xml` from two attributes, `mandatory` and
|
||||
`hashMatchMandatory`, both true unless written `"false"`
|
||||
(`es-app/src/bios/Bios.cpp:162-163` in recalbox-emulationstation at `f4cd0ae4`,
|
||||
colours set by `Bios::Scan` at lines 84-108). The platform YAML carries them as
|
||||
`required` and, when the attribute is written, `hash_match_mandatory`.
|
||||
|
||||
| mandatory | hashMatchMandatory | Color | verify.py mapping |
|
||||
|-----------|--------------------|--------|-------------------|
|
||||
| true | true | RED | CRITICAL |
|
||||
| true | false | YELLOW | WARNING |
|
||||
| false | (any) | GREEN | WARNING |
|
||||
| File | mandatory | hashMatchMandatory | Recalbox | verify.py |
|
||||
|------|-----------|--------------------|----------|-----------|
|
||||
| hash matches | (any) | (any) | GREEN | OK |
|
||||
| missing | true | (any) | RED | CRITICAL |
|
||||
| missing | false | (any) | YELLOW | WARNING |
|
||||
| hash differs | true | true | RED | WARNING |
|
||||
| hash differs | true | false | YELLOW | WARNING |
|
||||
| hash differs | false | (any) | YELLOW | WARNING |
|
||||
|
||||
`verify.py` applies the MD5-mode table above and does not read
|
||||
`hash_match_mandatory`, so a present file whose hash differs is a WARNING where
|
||||
Recalbox shows RED for a mandatory entry with `hashMatchMandatory`.
|
||||
|
||||
### checkInsideZip (Batocera zippedFile)
|
||||
|
||||
@@ -154,8 +165,15 @@ from `validation.py`. This layer uses data from emulator profiles (YAML files in
|
||||
|
||||
### Validation index
|
||||
|
||||
`_build_validation_index()` reads all emulator profiles and builds a per-filename
|
||||
index of validation rules. When multiple emulators reference the same file, checks
|
||||
`_build_validation_index()` (`validation.py`) builds a per-filename index of
|
||||
validation rules from the profiles it is given. A platform report, its pack and
|
||||
its install manifest pass only the profiles `resolve_platform_cores` resolves for
|
||||
that platform (`verify.py`, `verify_platform`). A profile that does not run on the
|
||||
platform has nothing to say about its pack: standalone ZEsarUX reads a 48 KB
|
||||
`cpc6128.rom` of its own, and its size check reported cap32's 32 KB file in the
|
||||
RetroArch pack as a mismatch while every profile was indexed. The same holds for
|
||||
`hle_fallback`, which is read from the same set of profiles. A report or pack for `--emulator` or `--system` indexes the
|
||||
profiles it selected. When multiple emulators reference the same file, checks
|
||||
are merged (union of all check types). Conflicting expected values are kept as sets
|
||||
(e.g., multiple accepted CRC32 values for different ROM versions).
|
||||
|
||||
@@ -256,12 +274,8 @@ A path or filename is never allowed to mask a declared strong hash: the
|
||||
identity steps run first, and a path or name is accepted only when no content
|
||||
hash was declared or when the content agrees with the one that was.
|
||||
|
||||
What the pack does with a `hash_mismatch` follows the platform's own mode. An
|
||||
MD5 or SHA1 platform would reject those bytes, so the file is left out and
|
||||
counted as an unsafe exclusion, distinct from a file the collection does not
|
||||
have. An existence platform reads no bytes at all, so the file is packed and
|
||||
the divergence is printed as a discrepancy: a value in an upstream BIOS list that
|
||||
differs from the emulator source must not remove a file the frontend would load.
|
||||
What the pack does with a `hash_mismatch` is set by the platform's mode, as
|
||||
[discrepancy detection](#discrepancy-detection) describes.
|
||||
|
||||
|
||||
## Discrepancy detection
|
||||
@@ -272,10 +286,11 @@ accepted.
|
||||
|
||||
A local file that contradicts a declared hash is handled by the platform's
|
||||
own mode, never by one global rule. An existence frontend never reads the
|
||||
bytes, so the file is packed and the disagreement reported: a stale hash in
|
||||
System.dat must not remove a file RetroArch would have loaded. An md5 or sha1
|
||||
frontend would reject it, so it is left out of the pack and counted as
|
||||
`unsafe excluded`, which is distinct from `missing`. The pack builder and the
|
||||
bytes, so the file is packed and the difference reported: a hash in System.dat
|
||||
that differs from the local dump must not remove a file RetroArch would have
|
||||
loaded. An md5 or sha1 frontend would reject it, so it is left out of the pack
|
||||
and counted as `unsafe excluded`, which is distinct from `missing`. The pack
|
||||
report and the installer manifest both record it. The pack builder and the
|
||||
pack verification apply the same rule.
|
||||
|
||||
### Variant search
|
||||
|
||||
Reference in new issue
Block a user