diff --git a/install/batocera.json b/install/batocera.json index 04bf3ece..65a10468 100644 --- a/install/batocera.json +++ b/install/batocera.json @@ -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 }, { diff --git a/install/lakka.json b/install/lakka.json index 96ebef72..818d4bcc 100644 --- a/install/lakka.json +++ b/install/lakka.json @@ -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", diff --git a/install/retroarch.json b/install/retroarch.json index 494de1e0..a3ff8458 100644 --- a/install/retroarch.json +++ b/install/retroarch.json @@ -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", diff --git a/install/retrodeck.json b/install/retrodeck.json index 076addd8..c3467158 100644 --- a/install/retrodeck.json +++ b/install/retrodeck.json @@ -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", diff --git a/install/retropie.json b/install/retropie.json index 4bff7201..a19785aa 100644 --- a/install/retropie.json +++ b/install/retropie.json @@ -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", diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index dee880c3..d6d18107 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -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 diff --git a/tests/test_shipped_content.py b/tests/test_shipped_content.py new file mode 100644 index 00000000..dd7af502 --- /dev/null +++ b/tests/test_shipped_content.py @@ -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()