fix: let the constrained declaration win in manifests too

A destination can be declared by more than one system, bare in one and
hash-constrained in another. generate_pack resolves that with
_preferred_entries so the constrained sibling claims the destination;
generate_manifest never did, and named whatever answered to the name.

RetroDECK's bios/d2fdc.zip is the case: declared with an md5 in the
arcade system and bare under apple-ii. The pack carried the right
archive, 262 bytes reached through the MAME clone map, while the
manifest sent install.py to a 256-byte Apple II ROM. Downloading the
ZIP and running the installer gave different files.

A test now hashes every manifest entry a platform pins and compares it
against what the platform declares: 3295 entries, on the platforms whose
frontend reads the bytes. It accepts any of several declarations for one
destination, the member-composite MD5 Recalbox pins for arcade archives,
and Batocera's 29-character prefixes. Reinstating the old manifest entry
fails it.
This commit is contained in:
Abdessamad Derraz committed 2026-08-23 12:14:56 +02:00
1 parent 3a266be7a5
commit 232ef1a6b8
7 files changed
+359 -156

No files matched your search

+17 -17
View File
@@ -238,7 +238,7 @@
}
],
"total_files": 1828,
"total_size": 4314448722,
"total_size": 4314432234,
"total_omitted": 9,
"omitted_files": [
{
@@ -543,10 +543,10 @@
},
{
"dest": "d2fdc.zip",
"sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4",
"sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2",
"size": 256,
"repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom",
"sha1": "af56c948598291b284a528f3fce06b961dba55e3",
"sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69",
"size": 262,
"repo_path": "bios/Arcade/Arcade/a3fdc.zip",
"cores": null
},
{
@@ -1399,10 +1399,10 @@
},
{
"dest": "coco_fdc_v11.zip",
"sha1": "45795e083e635c7beab9a40793c628a16f9990e6",
"sha256": "61e9bc795b0406b2efe4f3c5a07baa6e84ddc817299f89f26a09aa665423517e",
"size": 23115,
"repo_path": "bios/Arcade/Arcade/coco_fdc_v11.zip",
"sha1": "533870dbc4671a84f0765a96a2b7fea24564afd1",
"sha256": "d0159776a6e3ba40f32020c373c1050d9685c61fc3041fd4a3025221dcb2f5ba",
"size": 7134,
"repo_path": "bios/Arcade/Arcade/.variants/coco_fdc_v11.zip.e94f7c27",
"cores": null
},
{
@@ -2473,10 +2473,10 @@
},
{
"dest": "pcw9512.zip",
"sha1": "844988dc5a36917b002d0140d3a7692d1407f783",
"sha256": "bdc25c862aa0f2a91b2fdac9aa4c91d00d34e95ae2ca50b76ece7a7e35c7cfed",
"size": 2625,
"repo_path": "bios/Amstrad/PCW/pcw9512.zip",
"sha1": "675597949a7b0e1b393840bb4355376509cfa816",
"sha256": "dd4bca9f7efd22e80a196444f877c7b60e328916f9e6a1d183238f34fcc6a7d0",
"size": 2134,
"repo_path": "bios/Arcade/Arcade/pcw9512.zip",
"cores": null
},
{
@@ -2935,10 +2935,10 @@
},
{
"dest": "ym2608.zip",
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64",
"sha256": "187ede0f57d1439092a7ac50ea923533871ab7ee65e29baabc4dcdfa7ad4823f",
"size": 7609,
"repo_path": "bios/Arcade/Arcade/ym2608.zip",
"sha1": "0b6519209766ed883e3fca4c61bf866804c89004",
"sha256": "5ac777862d538bcef10e3bec3ee66c218bce6da3896e5c35e524b0ae8e6acfd5",
"size": 7587,
"repo_path": "bios/Arcade/Arcade/.variants/ym2608.zip.0b651920",
"cores": null
},
{
+32 -32
View File
@@ -1328,38 +1328,6 @@
"repo_path": "bios/Microsoft/MSX/MSX_8020-20bios.rom",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "quasi88/n88jisho.rom",
"sha1": "deef0cc2a9734ba891a6d6c022aa70ffc66f783e",
@@ -1448,6 +1416,38 @@
"repo_path": "bios/NEC/PC-98/N88EXT3.ROM",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "np2kai/2608_BD.WAV",
"sha1": "0a56c142ef40cec50f3ee56a6e42d0029c9e2818",
+32 -32
View File
@@ -1567,38 +1567,6 @@
"repo_path": "bios/Microsoft/MSX/MSX_8020-20bios.rom",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "quasi88/n88jisho.rom",
"sha1": "deef0cc2a9734ba891a6d6c022aa70ffc66f783e",
@@ -1687,6 +1655,38 @@
"repo_path": "bios/NEC/PC-98/N88EXT3.ROM",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "np2kai/2608_BD.WAV",
"sha1": "0a56c142ef40cec50f3ee56a6e42d0029c9e2818",
+41 -41
View File
@@ -238,7 +238,7 @@
}
],
"total_files": 3511,
"total_size": 6661468439,
"total_size": 6661468445,
"total_omitted": 9,
"omitted_files": [
{
@@ -469,14 +469,6 @@
"repo_path": "bios/Amstrad/CPC/amsdos.rom",
"cores": null
},
{
"dest": "bios/a2diskiing.zip",
"sha1": "d9bb189730e15d46c27e7246d7bb23137e7d761e",
"sha256": "e73eee381d01faecb0fa7f767b0d999f38692f023bcdbe1f9f8d57ad87941f6d",
"size": 371,
"repo_path": "bios/Apple/Apple II/a2diskiing.zip",
"cores": null
},
{
"dest": "bios/apple2e.zip",
"sha1": "891f44b09ab1a948d3d887f8a30b9c9265849ada",
@@ -485,30 +477,6 @@
"repo_path": "bios/Apple/Apple II/apple2e.zip",
"cores": null
},
{
"dest": "bios/d2fdc.zip",
"sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4",
"sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2",
"size": 256,
"repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom",
"cores": null
},
{
"dest": "bios/votrsc01.zip",
"sha1": "a57f14469867ce0d0865995a645b69f41a0ea718",
"sha256": "14217ca8e25435ea8497b272d8963ebc79ae29cdc21e972817ab09115e2f1b2d",
"size": 588,
"repo_path": "bios/Apple/Apple II/votrsc01.zip",
"cores": null
},
{
"dest": "bios/votrsc01a.zip",
"sha1": "14a5ff2c8ca68d35a77ebfbc227ed9c8e19d7524",
"sha256": "67e3bd12aa4db426a035ec65410fbdeee0d008ba89012285c7e61c0542a5c167",
"size": 590,
"repo_path": "bios/Arcade/Arcade/votrax.zip",
"cores": null
},
{
"dest": "bios/apple2gs.zip",
"sha1": "799e2fc90d6bfd8cb74e331e04d5afd36f2f21a1",
@@ -893,6 +861,14 @@
"repo_path": "bios/Arcade/Arcade/a2corvus.zip",
"cores": null
},
{
"dest": "bios/a2diskiing.zip",
"sha1": "d9bb189730e15d46c27e7246d7bb23137e7d761e",
"sha256": "e73eee381d01faecb0fa7f767b0d999f38692f023bcdbe1f9f8d57ad87941f6d",
"size": 371,
"repo_path": "bios/Apple/Apple II/a2diskiing.zip",
"cores": null
},
{
"dest": "bios/a2grafex.zip",
"sha1": "dc594fea200ce87d1bd33a4a1e73f0221b68b9eb",
@@ -4669,6 +4645,14 @@
"repo_path": "bios/Arcade/Arcade/cubo.zip",
"cores": null
},
{
"dest": "bios/d2fdc.zip",
"sha1": "af56c948598291b284a528f3fce06b961dba55e3",
"sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69",
"size": 262,
"repo_path": "bios/Arcade/Arcade/a3fdc.zip",
"cores": null
},
{
"dest": "bios/dc320b.zip",
"sha1": "5f091ab41db54f929d4a4c484b34c3a18eb63f38",
@@ -12037,6 +12021,14 @@
"repo_path": "bios/Arcade/MAME/vision968.zip",
"cores": null
},
{
"dest": "bios/votrsc01.zip",
"sha1": "a57f14469867ce0d0865995a645b69f41a0ea718",
"sha256": "14217ca8e25435ea8497b272d8963ebc79ae29cdc21e972817ab09115e2f1b2d",
"size": 588,
"repo_path": "bios/Apple/Apple II/votrsc01.zip",
"cores": null
},
{
"dest": "bios/votraxtnt.zip",
"sha1": "b32303333a66101a83d008af1a8c1966751ed156",
@@ -12069,6 +12061,14 @@
"repo_path": "bios/Arcade/Arcade/vp575.zip",
"cores": null
},
{
"dest": "bios/votrsc01a.zip",
"sha1": "14a5ff2c8ca68d35a77ebfbc227ed9c8e19d7524",
"sha256": "67e3bd12aa4db426a035ec65410fbdeee0d008ba89012285c7e61c0542a5c167",
"size": 590,
"repo_path": "bios/Arcade/Arcade/votrax.zip",
"cores": null
},
{
"dest": "bios/vrc5074.zip",
"sha1": "1a1826d7962e6bf6b94fc1bc2b17b8633eaaa0f5",
@@ -12461,14 +12461,6 @@
"repo_path": "bios/Arcade/FBNeo/spec1282a.zip",
"cores": null
},
{
"dest": "bios/channelf.zip",
"sha1": "1cb23b462b990241013deb4b5e07ce741af28267",
"sha256": "c158f7c177c61c7b441eb1061e5148737597e871cbcfa1e2d60ace15e90c8e70",
"size": 2705,
"repo_path": "bios/Arcade/Arcade/channelf.zip",
"cores": null
},
{
"dest": "bios/ROM_OS_A_NTSC",
"sha1": "f821911f16ed613d72d4b255b3e68ab2d07041ac",
@@ -13213,6 +13205,14 @@
"repo_path": "bios/Arcade/FBNeo/sl31253.rom",
"cores": null
},
{
"dest": "bios/channelf.zip",
"sha1": "1cb23b462b990241013deb4b5e07ce741af28267",
"sha256": "c158f7c177c61c7b441eb1061e5148737597e871cbcfa1e2d60ace15e90c8e70",
"size": 2705,
"repo_path": "bios/Arcade/Arcade/channelf.zip",
"cores": null
},
{
"dest": "bios/sl31254.bin",
"sha1": "8f70d1b74483ba3a37e86cf16c849d601a8c3d2c",
+32 -32
View File
@@ -1327,38 +1327,6 @@
"repo_path": "bios/Microsoft/MSX/MSX_8020-20bios.rom",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "quasi88/n88jisho.rom",
"sha1": "deef0cc2a9734ba891a6d6c022aa70ffc66f783e",
@@ -1447,6 +1415,38 @@
"repo_path": "bios/NEC/PC-98/N88EXT3.ROM",
"cores": null
},
{
"dest": "quasi88/n88_0.rom",
"sha1": "d1ae642aed4f0584eeb81ff50180db694e5101d4",
"sha256": "8b584cd9143a670ddfce657ca65067c86f6be1db757a3085f48b1479ac6e8cf3",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_0.rom",
"cores": null
},
{
"dest": "quasi88/n88_1.rom",
"sha1": "4ae4d37409ff99411a623da9f6a44192170a854e",
"sha256": "810936574cdf68849822a1570046091f0bad0590dee3635dbc89a4cc4e099555",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_1.rom",
"cores": null
},
{
"dest": "quasi88/n88_2.rom",
"sha1": "e94278682ef9e9bbb82201f72c50382748dcea2a",
"sha256": "7d361b1bfd8523cd580bd9ca73902154556fbf45f60f0f539beef397d9462646",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_2.rom",
"cores": null
},
{
"dest": "quasi88/n88_3.rom",
"sha1": "af452aed79b072c4d17985830b7c5dca64d4b412",
"sha256": "3dc431af6e0c724582a3aa49e5c22cb9cc99d8298a4cce500e62bbaf1bc4bffb",
"size": 8192,
"repo_path": "bios/NEC/PC-98/n88_3.rom",
"cores": null
},
{
"dest": "np2kai/2608_BD.WAV",
"sha1": "0a56c142ef40cec50f3ee56a6e42d0029c9e2818",
+19 -2
View File
@@ -635,9 +635,11 @@ def generate_pack(
preferred_entries: dict[str, int] = {}
if source != "truth":
# A manifest lists the full set and carries no data directories, so
# the preference is resolved with neither narrowing nor a cache.
preferred_entries = _preferred_entries(
pack_systems, db, bios_dir, base_dest, required_only,
zip_contents, data_registry, offline,
pack_systems, db, bios_dir, base_dest, False,
zip_contents, None, offline,
)
# Region selection is decided once, over both the platform baseline and the
@@ -2830,6 +2832,17 @@ def generate_manifest(
# Phase 1: baseline files
if source != "truth":
# The same destination can be declared by more than one system, bare
# in one and hash-constrained in another. The builder lets the
# constrained sibling claim it; without the same rule here the
# manifest named whatever answered to the name, so the pack and the
# installer disagreed about which file a destination means.
# A manifest lists the full set and carries no data directories, so
# the preference is resolved with neither narrowing nor a cache.
preferred_entries = _preferred_entries(
pack_systems, db, bios_dir, base_dest, False,
zip_contents, None, offline,
)
for sys_id, system in sorted(pack_systems.items()):
for file_entry in system.get("files", []):
dest = sanitize_pack_path(file_entry.get("destination", file_entry["name"]))
@@ -2839,6 +2852,10 @@ def generate_manifest(
continue
full_dest = f"{base_dest}/{dest}" if base_dest else dest
preferred = preferred_entries.get(full_dest)
if preferred is not None and id(file_entry) != preferred:
continue
dedup_key = full_dest
if dedup_key in seen_destinations:
continue
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""What ships must satisfy the declaration that put it there.
A file reaches a pack because something asked for it: a platform's own list,
or a profile describing what a core loads. Whichever it was, the bytes shipped
have to satisfy that declaration. Two files in the collection did not, and
both were distributed for months before anyone compared them: a C128 kernal
that was two ROMs concatenated, and a Cave Story build the core would not
accept.
This is checked against the published manifests rather than the built packs,
because the manifest is what install.py fetches and it names the repository
path each entry comes from.
"""
from __future__ import annotations
import hashlib
import json
import sys
import unittest
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
import common # noqa: E402
from nativemode import reads_file_contents # noqa: E402
def _declared_digests(entry: dict) -> dict[str, set[str]]:
"""Every content value a declaration commits to, lowercased."""
out: dict[str, set[str]] = {}
for algo in ("md5", "sha1"):
raw = entry.get(algo)
values = raw if isinstance(raw, list) else [raw]
cleaned = set()
for value in values:
if isinstance(value, str) and value.strip():
# Recalbox publishes several acceptable hashes per entry.
cleaned.update(v.strip().lower() for v in value.split(",") if v.strip())
if cleaned:
out[algo] = cleaned
return out
class ShippedFilesSatisfyTheirDeclaration(unittest.TestCase):
def setUp(self):
self.install = ROOT / "install"
if not self.install.is_dir() or not list(self.install.glob("*.json")):
self.skipTest("no published manifests in install/")
def test_every_manifest_entry_matches_its_platform_declaration(self):
"""A pinned hash and the shipped bytes must be the same file.
Only on platforms whose frontend reads the bytes. Under an existence
mode the code never opens the file, so an upstream hash contradicted
by the local dump is reported and the file still ships: withholding
it would deny the frontend something it would have loaded.
Only entries the platform pins by hash are checkable here. An entry
with no declared hash commits to nothing, and one whose bytes come
from a release asset is not in the working tree to hash.
"""
violations = []
checked = 0
for manifest_path in sorted(self.install.glob("*.json")):
platform = manifest_path.stem
try:
config = common.load_platform_config(platform, str(ROOT / "platforms"))
except (FileNotFoundError, KeyError):
continue
if not reads_file_contents(config.get("verification_mode")):
continue
# A destination may be declared by more than one system, bare in
# one and hash-constrained in another; the builder lets whichever
# resolves claim it, so any of them satisfying the bytes is enough.
declared: dict[str, list[dict]] = {}
for system in config.get("systems", {}).values():
for entry in system.get("files", []):
dest = entry.get("destination") or entry.get("name", "")
if dest:
declared.setdefault(
common.sanitize_pack_path(dest), []
).append(entry)
manifest = json.loads(manifest_path.read_text())
for shipped in manifest.get("files", []):
entries = declared.get(
common.sanitize_pack_path(shipped.get("dest", "")), []
)
entries = [e for e in entries if not e.get("zipped_file")]
candidates = [(e, _declared_digests(e)) for e in entries]
candidates = [(e, w) for e, w in candidates if w]
if not candidates:
continue
repo_path = shipped.get("repo_path", "")
if not repo_path:
continue
blob_path = ROOT / repo_path
if not blob_path.is_file():
continue
blob = blob_path.read_bytes()
checked += 1
digests = {
algo: [hashlib.new(algo, blob).hexdigest()]
for algo in ("md5", "sha1")
}
# Recalbox pins some arcade archives by the MD5 of their
# members rather than of the container, which is what
# md5_composite computes and what the resolver matches on.
# A variant carries its hash after the extension
# (cdimono1.zip.e6714b3d), so the name is not the test.
try:
digests["md5"].append(common.md5_composite(str(blob_path)))
except (zipfile.BadZipFile, OSError):
pass
# Batocera publishes 29-character prefixes, so compare by prefix.
satisfied = any(
all(
any(
got.startswith(a)
for got in digests[algo]
for a in accepted
)
for algo, accepted in wanted.items()
)
for _e, wanted in candidates
)
if not satisfied:
first = candidates[0][1]
algo = sorted(first)[0]
violations.append(
f"{platform}: {shipped['dest']} ships {repo_path} "
f"with {algo} {digests[algo][0]}, declared "
f"{sorted(first[algo])[0]}"
)
self.assertEqual(
violations, [],
f"{len(violations)} of {checked} pinned entries ship contradicted "
"bytes:\n " + "\n ".join(violations[:12]),
)
self.assertGreater(checked, 200, "the check reached almost nothing")
class ProfileContradictionsAreKnown(unittest.TestCase):
"""A profile's hash and the file answering to it may legitimately differ.
Emulators ship their own build of a shared data file: four hiscore.dat
versions are held, and ti99sim converts its cartridges differently from
the release Recalbox pinned. What must not happen is such a file reaching
a pack, because there the platform's own declaration governs and is
checked above. This test records how many profile declarations no held
file satisfies, so the number cannot grow unnoticed.
"""
def test_the_count_of_unsatisfied_profile_hashes_is_known(self):
database = ROOT / "database.json"
if not database.is_file():
self.skipTest("database.json not built")
db = common.load_database(str(database))
profiles = common.load_emulator_profiles(str(ROOT / "emulators"))
unsatisfied = []
for name, profile in sorted(profiles.items()):
if profile.get("type") in ("alias", "launcher"):
continue
for entry in profile.get("files", []):
if not isinstance(entry, dict) or entry.get("unsourceable"):
continue
if not _declared_digests(entry):
continue
path, status = common.resolve_local_file(
entry, db, dest_hint=entry.get("path", "")
)
if path and status == "hash_mismatch":
unsatisfied.append(f"{name}/{entry.get('name','')}")
self.assertLessEqual(
len(unsatisfied), 20,
"profile declarations no held file satisfies grew past the known "
f"set ({len(unsatisfied)}):\n " + "\n ".join(sorted(unsatisfied)),
)
if __name__ == "__main__":
unittest.main()