18 Commits

Author SHA1 Message Date
Abdessamad Derraz
5cbd461a97 docs: update readme and database for ground truth feature 2026-03-27 23:55:30 +01:00
Abdessamad Derraz
6cbdd4c40c test: add integration tests for ground truth full chain 2026-03-27 23:41:06 +01:00
Abdessamad Derraz
37acc8d0fc feat: add --verbose flag and ground truth rendering 2026-03-27 23:38:43 +01:00
Abdessamad Derraz
2cf1398786 feat: attach ground truth to emulator verification results 2026-03-27 23:33:53 +01:00
Abdessamad Derraz
6b14b5e2b1 feat: attach ground truth to platform verification results 2026-03-27 23:30:49 +01:00
Abdessamad Derraz
6d959ff2b0 feat: add per-emulator ground truth to validation index 2026-03-27 23:25:42 +01:00
Abdessamad Derraz
3672912de7 chore: clean up sc3000 variant swap, update database 2026-03-27 22:50:54 +01:00
Abdessamad Derraz
569781c104 fix: rename misleading exclusion label in verify report 2026-03-27 22:44:05 +01:00
Abdessamad Derraz
7f265b3cb2 refactor: split pack check into baseline and cores with clear counts 2026-03-27 20:37:48 +01:00
Abdessamad Derraz
3ea1e09cb0 feat: verify core extras presence in pack alongside baseline 2026-03-27 19:41:47 +01:00
Abdessamad Derraz
89d6dd2eee feat: add platform conformance check to pack verification 2026-03-27 19:21:29 +01:00
Abdessamad Derraz
acd2daf7c1 fix: filter pattern placeholders, skip standalone exclusions for standalone platforms 2026-03-27 18:30:18 +01:00
Abdessamad Derraz
0ad8324d46 refactor: clearer verify report for core files coverage 2026-03-27 18:11:26 +01:00
Abdessamad Derraz
29749898f8 fix: correct sc3000, plus3 and plus3e hashes in platform configs 2026-03-27 17:47:39 +01:00
Abdessamad Derraz
59d7582c0e fix: swap sc3000.zip primary to batocera-compatible dump 2026-03-27 16:50:10 +01:00
Abdessamad Derraz
e94ce6b194 feat: source 10 missing bios variants for romm and retrobat coverage 2026-03-27 16:00:10 +01:00
Abdessamad Derraz
181248b6db fix: case-sensitive packs for linux platforms, remove empty bios placeholder 2026-03-27 12:58:08 +01:00
Abdessamad Derraz
a117b13b49 fix: data directory path construction avoids double slash duplicates 2026-03-27 12:42:21 +01:00
24 changed files with 862 additions and 141 deletions

View File

@@ -2,7 +2,7 @@
Complete BIOS and firmware packs for Batocera, EmuDeck, Lakka, Recalbox, RetroArch, RetroBat, RetroDECK, RetroPie, and RomM.
**6,748** verified files across **352** systems, ready to extract into your emulator's BIOS directory.
**6,756** verified files across **352** systems, ready to extract into your emulator's BIOS directory.
## Download BIOS packs
@@ -28,8 +28,8 @@ Each file is checked against the emulator's source code to match what the code a
- **9 platforms** supported with platform-specific verification
- **328 emulators** profiled from source (RetroArch cores + standalone)
- **352 systems** covered (NES, SNES, PlayStation, Saturn, Dreamcast, ...)
- **6,748 files** verified with MD5, SHA1, CRC32 checksums
- **5251 MB** total collection size
- **6,756 files** verified with MD5, SHA1, CRC32 checksums
- **5331 MB** total collection size
## Supported systems
@@ -41,7 +41,7 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/
| Platform | Coverage | Verified | Untested | Missing |
|----------|----------|----------|----------|---------|
| Batocera | 359/359 (100.0%) | 358 | 1 | 0 |
| Batocera | 359/359 (100.0%) | 359 | 0 | 0 |
| EmuDeck | 161/161 (100.0%) | 161 | 0 | 0 |
| Lakka | 448/448 (100.0%) | 448 | 0 | 0 |
| Recalbox | 346/346 (100.0%) | 346 | 0 | 0 |
@@ -49,7 +49,7 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/
| RetroBat | 331/331 (100.0%) | 331 | 0 | 0 |
| RetroDECK | 2007/2007 (100.0%) | 2007 | 0 | 0 |
| RetroPie | 448/448 (100.0%) | 448 | 0 | 0 |
| RomM | 374/374 (100.0%) | 359 | 15 | 0 |
| RomM | 374/374 (100.0%) | 374 | 0 | 0 |
## Build your own pack
@@ -72,6 +72,7 @@ python scripts/generate_pack.py --list-systems
python scripts/verify.py --all
python scripts/verify.py --platform batocera
python scripts/verify.py --emulator flycast
python scripts/verify.py --platform retroarch --verbose # emulator ground truth
```
Only dependency: Python 3 + `pyyaml`.
@@ -110,4 +111,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
This repository provides BIOS files for personal backup and archival purposes.
*Auto-generated on 2026-03-26T12:17:35Z*
*Auto-generated on 2026-03-27T22:52:26Z*

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{
"generated_at": "2026-03-26T12:17:17Z",
"total_files": 6748,
"total_size": 5505760050,
"generated_at": "2026-03-27T22:52:09Z",
"total_files": 6756,
"total_size": 5589782999,
"files": {
"520d3d1b5897800af47f92efd2444a26b7a7dead": {
"path": "bios/3DO Company/3DO/3do_arcade_saot.bin",
@@ -573,19 +573,9 @@
"crc32": "665cd50f",
"adler32": "39cd4d98"
},
"da39a3ee5e6b4b0d3255bfef95601890afd80709": {
"afd060e6f35faf3bb0146fa889fc787adf56330a": {
"path": "bios/Apple/Apple II/disk2-13boot.rom",
"name": "disk2-13boot.rom",
"size": 0,
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"crc32": "00000000",
"adler32": "00000001"
},
"afd060e6f35faf3bb0146fa889fc787adf56330a": {
"path": "bios/Apple/Apple II/disk2-13seq.rom",
"name": "disk2-13seq.rom",
"size": 256,
"sha1": "afd060e6f35faf3bb0146fa889fc787adf56330a",
"md5": "4f80448507cf43ab40c17ac08d89e278",
@@ -1043,6 +1033,16 @@
"crc32": "7eba26a4",
"adler32": "dbd38d64"
},
"533ef12a6d22726da81a50f08871c6e9a377a328": {
"path": "bios/Arcade/Arcade/.variants/naomi2.zip.533ef12a",
"name": "naomi2.zip",
"size": 2189528,
"sha1": "533ef12a6d22726da81a50f08871c6e9a377a328",
"md5": "0ea5bf0345e27b1cf51bbde1bd398eca",
"sha256": "75910bca402ce5b3db9aaab485150bc40aee5a0028e3e50c8bd63132520ffe4f",
"crc32": "f25cf3a8",
"adler32": "744000e5"
},
"2962e338ccc9f66f29b409f73ca27aeee79633ac": {
"path": "bios/Arcade/Arcade/.variants/naomi2.zip.da79eca4",
"name": "naomi2.zip",
@@ -1113,6 +1113,16 @@
"crc32": "578e8fde",
"adler32": "d530a21a"
},
"c0c001ec80fa860857000f4cfc9844a28498a355": {
"path": "bios/Arcade/Arcade/.variants/pgm.zip.c0c001ec",
"name": "pgm.zip",
"size": 2094636,
"sha1": "c0c001ec80fa860857000f4cfc9844a28498a355",
"md5": "87cc944eef4c671aa2629a8ba48a08e0",
"sha256": "b8603d9021cf9508152fe8ce23c411bb631b24b61d24a26fdd41e273ab925fe5",
"crc32": "bf3dd2ef",
"adler32": "d0cdbd6d"
},
"44bc8180dca3dfdf1b461268919da8efb2e3fb07": {
"path": "bios/Arcade/Arcade/.variants/skns.zip.44bc8180",
"name": "skns.zip",
@@ -27713,6 +27723,56 @@
"crc32": "0636e0be",
"adler32": "0ca77de6"
},
"997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27": {
"path": "bios/Id Software/Doom/.variants/DOOM.WAD.997bae5e",
"name": "DOOM.WAD",
"size": 12733492,
"sha1": "997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27",
"md5": "4461d4511386518e784c647e3128e7bc",
"sha256": "d91f81caeafc863f811c2945259589d617eb95dac5ca18b2d1f4e1bbebb41703",
"crc32": "cff03d9f",
"adler32": "a94ca820"
},
"c745f04a6abc2e6d2a2d52382f45500dd2a260be": {
"path": "bios/Id Software/Doom/.variants/DOOM2.WAD.c745f04a",
"name": "DOOM2.WAD",
"size": 14802506,
"sha1": "c745f04a6abc2e6d2a2d52382f45500dd2a260be",
"md5": "9aa3cbf65b961d0bdac98ec403b832e1",
"sha256": "5142a922518a7cfe09ed21097cf6a57316ec77c095682ebe2e90b56f4a568089",
"crc32": "09b8a6ae",
"adler32": "9bcb9107"
},
"d041456bea851c173f65ac6ab3f2ee61bb0b8b53": {
"path": "bios/Id Software/Doom/.variants/DOOM64.WAD.d041456b",
"name": "DOOM64.WAD",
"size": 15103212,
"sha1": "d041456bea851c173f65ac6ab3f2ee61bb0b8b53",
"md5": "0aaba212339c72250f8a53a0a2b6189e",
"sha256": "05ec0118cc130036d04bf6e6f7fe4792dfafc2d4bd98de349dd63e2022925365",
"crc32": "65816192",
"adler32": "58321899"
},
"816c7c6b0098f66c299c9253f62bd908456efb63": {
"path": "bios/Id Software/Doom/.variants/PLUTONIA.WAD.816c7c6b",
"name": "PLUTONIA.WAD",
"size": 17531493,
"sha1": "816c7c6b0098f66c299c9253f62bd908456efb63",
"md5": "24037397056e919961005e08611623f4",
"sha256": "2376491fdf49e6fda80f9b489571252e195d24423c4f1be3f3c38e81d2022f52",
"crc32": "650b998d",
"adler32": "57a05e03"
},
"9820e2a3035f0cdd87f69a7d57c59a7a267c9409": {
"path": "bios/Id Software/Doom/.variants/TNT.WAD.9820e2a3",
"name": "TNT.WAD",
"size": 18304630,
"sha1": "9820e2a3035f0cdd87f69a7d57c59a7a267c9409",
"md5": "8974e3117ed4a1839c752d5e11ab1b7b",
"sha256": "b7b60364d8dd6763b82d5b4f93da547ff15990135c9df8a3abce4328c3dba5f0",
"crc32": "15f18ddb",
"adler32": "479fb930"
},
"eca9cff1014ce5081804e193588d96c6ddb35432": {
"path": "bios/Id Software/Doom/CHEX.WAD",
"name": "CHEX.WAD",
@@ -31423,6 +31483,16 @@
"crc32": "8205795e",
"adler32": "87ca090b"
},
"6639b6693784574d204c42703a74fd8b088a3a5e": {
"path": "bios/Microsoft/Xbox/.variants/Complex_4627.bin.6639b669",
"name": "Complex_4627.bin",
"size": 1048576,
"sha1": "6639b6693784574d204c42703a74fd8b088a3a5e",
"md5": "ec00e31e746de2473acfe7903c5a4cb7",
"sha256": "34f1c8ded59116436065783f8ad2ef0939df3cbfc76277ec9e5c41bf9ccb93cd",
"crc32": "ccb97a84",
"adler32": "3721a382"
},
"358a20e61ec1a2387127b1fa92113034fb279f9b": {
"path": "bios/Microsoft/Xbox/Complex.bin",
"name": "Complex.bin",
@@ -59903,6 +59973,16 @@
"crc32": "05cfde8c",
"adler32": "ff3c17cb"
},
"bf6b379c204da77dece1aedf83ff35227a623e5d": {
"path": "bios/SNK/Neo Geo CD/.variants/neocdz.zip.bf6b379c",
"name": "neocdz.zip",
"size": 214876,
"sha1": "bf6b379c204da77dece1aedf83ff35227a623e5d",
"md5": "c38cb8e50321783e413dc5ff292a3ff8",
"sha256": "71994d72072f0c1b554209a9055bbdbf6045df73999720ed3432ac072f394aba",
"crc32": "19681e91",
"adler32": "3d8bbd14"
},
"5158b728e62b391fb69493743dcf7abbc62abc82": {
"path": "bios/SNK/Neo Geo CD/.variants/uni-bioscd.rom.5158b728",
"name": "uni-bioscd.rom",
@@ -63383,18 +63463,8 @@
"crc32": "4dcfd55c",
"adler32": "75a93df4"
},
"c983bfa2f4c6d077e70e6ff9c7ed59b72368e355": {
"path": "bios/Sega/SC-3000/.variants/sc3000.zip.a43aef36",
"name": "sc3000.zip",
"size": 21232,
"sha1": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"md5": "52adbcbef759756a5b97cafda75b922c",
"sha256": "258b08eafabed889970445adadcc483f91c31205e8c9a75dfa2efceab2f1c43f",
"crc32": "48decddc",
"adler32": "1dd14fb4"
},
"12de390be2595ad17015310085eaec57ad2b953f": {
"path": "bios/Sega/SC-3000/sc3000.zip",
"path": "bios/Sega/SC-3000/.variants/sc3000.zip.12de390b",
"name": "sc3000.zip",
"size": 21348,
"sha1": "12de390be2595ad17015310085eaec57ad2b953f",
@@ -63403,6 +63473,16 @@
"crc32": "62fb7d82",
"adler32": "9fb3a0ff"
},
"c983bfa2f4c6d077e70e6ff9c7ed59b72368e355": {
"path": "bios/Sega/SC-3000/sc3000.zip",
"name": "sc3000.zip",
"size": 21232,
"sha1": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"md5": "52adbcbef759756a5b97cafda75b922c",
"sha256": "258b08eafabed889970445adadcc483f91c31205e8c9a75dfa2efceab2f1c43f",
"crc32": "48decddc",
"adler32": "1dd14fb4"
},
"8c031bf9908fd0142fdd10a9cdd79389f8a3f2fc": {
"path": "bios/Sega/Saturn/.variants/hisaturn_v103.bin",
"name": "hisaturn_v103.bin",
@@ -67543,7 +67623,6 @@
"ba89edf2729a28a17cd9e0f7a0ac9a39": "bc32bc0e8902946663998f56aea52be597d9e361",
"8f7ee14ccca8ae3dd8c759497af3f09b": "799e2fc90d6bfd8cb74e331e04d5afd36f2f21a1",
"d3bef2755267a941f264fb5b288e3076": "e8c40d3a44a41a9b6b5dd3a993d7057b3bfb4086",
"d41d8cd98f00b204e9800998ecf8427e": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"4f80448507cf43ab40c17ac08d89e278": "afd060e6f35faf3bb0146fa889fc787adf56330a",
"5f1be0c1cdff26f5956eef9643911886": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4",
"c835eab06842e3c1d6e2e7dc19289828": "a57f14469867ce0d0865995a645b69f41a0ea718",
@@ -67590,6 +67669,7 @@
"526eda1e2a7920c92c88178789d71d84": "c96711c01c0158f161791d6fbe75d88329e8ac0a",
"4e1ca1ade518f53efcce30bdefb855a4": "11ad55ee6b11092e810365b8389c1f8b4081e5d0",
"58033e4ba5793c09dffb87f96f3e9301": "2533cc33201da28b2086a0a2fd2b5e04271b6eeb",
"0ea5bf0345e27b1cf51bbde1bd398eca": "533ef12a6d22726da81a50f08871c6e9a377a328",
"c50072cbab75673e1b1a6b94355e6fa8": "2962e338ccc9f66f29b409f73ca27aeee79633ac",
"e20b430bd7def78b45f61f238abab624": "f9ad4a4c6b0bbbe39ba358690a48f763ecbd98f0",
"82f3a8bea688b4863947722d2fcb07f7": "a0f07de6070d98f86d55a4ecd61b4a5b05a4a0d5",
@@ -67597,6 +67677,7 @@
"68ef99a1f2847d08ff9242a90561d31b": "0783012b4eabca599e460988257ec37500501df6",
"653e991a39e867354d090c3394157d1c": "6bbbce094422062bd178d6007bed06dcdd0d8b78",
"581cc172db39bb5007642405adf25b6e": "bc0acaefb5fac0cdc05476a9f452391d34a5150d",
"87cc944eef4c671aa2629a8ba48a08e0": "c0c001ec80fa860857000f4cfc9844a28498a355",
"49e192febe2f011d9be44ebc69129080": "44bc8180dca3dfdf1b461268919da8efb2e3fb07",
"937ab1072d9ce3ecdf3d16d7fc72137a": "aabedfebe2cd8cfede8c2a3cb9ff33166f0ef953",
"fcb298d97792b9e9bdd3296cc6be10b6": "eb2a867578a05bbf8741e9fe7204301062df0cb8",
@@ -70257,6 +70338,11 @@
"8ecd11275ad418d302cd358b408b01ec": "10f1a7204f69b82a18bc94a3010c9660aec0c802",
"ec2a977f0c0645dd284010160caec636": "c0ee6f9443fa82c0fef5d497b3e76aa3077f1865",
"19d55c537767a7424a8e376d62fc2ac0": "72c60172fb1ba77c9b24b06b7755f0a16f0b3a13",
"4461d4511386518e784c647e3128e7bc": "997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27",
"9aa3cbf65b961d0bdac98ec403b832e1": "c745f04a6abc2e6d2a2d52382f45500dd2a260be",
"0aaba212339c72250f8a53a0a2b6189e": "d041456bea851c173f65ac6ab3f2ee61bb0b8b53",
"24037397056e919961005e08611623f4": "816c7c6b0098f66c299c9253f62bd908456efb63",
"8974e3117ed4a1839c752d5e11ab1b7b": "9820e2a3035f0cdd87f69a7d57c59a7a267c9409",
"25485721882b050afa96a56e5758dd52": "eca9cff1014ce5081804e193588d96c6ddb35432",
"1cd63c5ddff1bf8ce844237f580e9cf3": "7742089b4468a736cadb659a7deca3320fe6dcbd",
"f0cefca49926d00903cf57551d901abe": "5b2e249b9c5133ec987b3ea77596381dc0d6bc1d",
@@ -70628,6 +70714,7 @@
"57509815f93e2817d3eb57e20286c7fb": "b484c8fa4549b79cff976001ac4beb19abf7cfb5",
"248514aba82a0ec7fe2a9106862b05cd": "e7905d16d2ccd57a013c122dc432106cd59ef52c",
"a0452dbf5ace7d2e49d0a8029efed09a": "829c00c3114f25b3dae5157c0a238b52a3ac37db",
"ec00e31e746de2473acfe7903c5a4cb7": "6639b6693784574d204c42703a74fd8b088a3a5e",
"21445c6f28fca7285b0f167ea770d1e5": "358a20e61ec1a2387127b1fa92113034fb279f9b",
"39cee882148a87f93cb440b99dde3ceb": "3944392c954cfb176d4210544e88353b3c5d36b1",
"04e9565c5eb34c71c8f5f8b9f6524406": "4e1c2c2ee308ca4591542b3ca48653f65fae6e0f",
@@ -73476,6 +73563,7 @@
"266c7454616da286c1a7de54181834c2": "df89ef91bbaf84f2a109377fd4b17b0050a163e8",
"041fe48b90b1aed6d0b72da192fb76ef": "aed2cb663b6370efb10969a3373fd84c558edb4f",
"a7cca75f3d5af6acc85efcce589ab04f": "891407e147b2c022ebedf8c51fe3a2f497304906",
"c38cb8e50321783e413dc5ff292a3ff8": "bf6b379c204da77dece1aedf83ff35227a623e5d",
"a147aeab5edeb1a9b652e7fb640f5bb3": "5158b728e62b391fb69493743dcf7abbc62abc82",
"fc7599f3f871578fe9a0453662d1c966": "5992277debadeb64d1c1c64b0a92d9293eaf7e4a",
"5c2366f25ff92d71788468ca492ebeca": "53bc1f283cdf00fa2efbb79f2e36d4c8038d743a",
@@ -73824,8 +73912,8 @@
"ff4a3572475236e859e3e9ac5c87d1f1": "02c287d10da6de579af7a4ce73b134bbdf23c970",
"4ea493ea4e9f6c9ebfccbdb15110367e": "88d6499d874dcb5721ff58d76fe1b9af811192e3",
"b4e76e416b887f4e7413ba76fa735f16": "70429f1d80503a0632f603bf762fe0bbaa881d22",
"52adbcbef759756a5b97cafda75b922c": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"48e8821fb9087ab60a2a3b1465ee5124": "12de390be2595ad17015310085eaec57ad2b953f",
"52adbcbef759756a5b97cafda75b922c": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"0306c0e408d6682dd2d86324bd4ac661": "8c031bf9908fd0142fdd10a9cdd79389f8a3f2fc",
"9992f2761b0f6e83b3e923451ab8057b": "999ed28cfbf18103a4963b0d3797af3dcf67db05",
"37e9746f4491aa2df9a83729d1a93620": "fefb403a7e91bdaf75ffd8a94c2a1f0ef4ece740",
@@ -74406,9 +74494,6 @@
"5ba8555f716bd48834858d8a7f42810ab7293b12"
],
"disk2-13boot.rom": [
"da39a3ee5e6b4b0d3255bfef95601890afd80709"
],
"disk2-13seq.rom": [
"afd060e6f35faf3bb0146fa889fc787adf56330a"
],
"disk2-16seq.rom": [
@@ -74554,6 +74639,7 @@
"d7ef86bd03de7c1d0e2b0762e04b6f8f8d26dbdb"
],
"naomi2.zip": [
"533ef12a6d22726da81a50f08871c6e9a377a328",
"2962e338ccc9f66f29b409f73ca27aeee79633ac",
"46278151f85b74951592d57155441485fe3e0274",
"b42c05722fa25416a382f87113132131153363f7"
@@ -74568,12 +74654,14 @@
"4f28af31ca0defdd73d80edec2fa296908e624dc",
"eac782be9f6ebe5dfb09bb4a5342d8aefb82d527",
"891407e147b2c022ebedf8c51fe3a2f497304906",
"bf6b379c204da77dece1aedf83ff35227a623e5d",
"80c9a25ccf39c6b85d1a7c9c0d6fe527ab45ec1c"
],
"pgm.zip": [
"0783012b4eabca599e460988257ec37500501df6",
"6bbbce094422062bd178d6007bed06dcdd0d8b78",
"bc0acaefb5fac0cdc05476a9f452391d34a5150d",
"c0c001ec80fa860857000f4cfc9844a28498a355",
"2b61f36b22933f3bec237818ecc959efb016d652",
"75de7964a38038c45791960d5f2ec9d26dbdc5e2"
],
@@ -82236,24 +82324,35 @@
"ALI1429G.AMW": [
"72c60172fb1ba77c9b24b06b7755f0a16f0b3a13"
],
"DOOM.WAD": [
"997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27",
"7742089b4468a736cadb659a7deca3320fe6dcbd"
],
"DOOM2.WAD": [
"c745f04a6abc2e6d2a2d52382f45500dd2a260be",
"7ec7652fcfce8ddc6e801839291f0e28ef1d5ae7"
],
"DOOM64.WAD": [
"d041456bea851c173f65ac6ab3f2ee61bb0b8b53",
"ae363db8cd5645e1753d9bacc82cc0724e8e7f21"
],
"PLUTONIA.WAD": [
"816c7c6b0098f66c299c9253f62bd908456efb63",
"90361e2a538d2388506657252ae41aceeb1ba360"
],
"TNT.WAD": [
"9820e2a3035f0cdd87f69a7d57c59a7a267c9409",
"9fbc66aedef7fe3bae0986cdb9323d2b8db4c9d3"
],
"CHEX.WAD": [
"eca9cff1014ce5081804e193588d96c6ddb35432"
],
"DOOM.WAD": [
"7742089b4468a736cadb659a7deca3320fe6dcbd"
],
"DOOM1.WAD": [
"5b2e249b9c5133ec987b3ea77596381dc0d6bc1d"
],
"DOOM2.WAD": [
"7ec7652fcfce8ddc6e801839291f0e28ef1d5ae7"
],
"DOOM2F.WAD": [
"d510c877031bbd5f3d198581a2c8651e09b9861f"
],
"DOOM64.WAD": [
"ae363db8cd5645e1753d9bacc82cc0724e8e7f21"
],
"HERETIC.WAD": [
"f489d479371df32f6d280a0cb23b59a35ba2b833"
],
@@ -82266,18 +82365,12 @@
"HEXEN.WAD": [
"4b53832f0733c1e29e5f1de2428e5475e891af29"
],
"PLUTONIA.WAD": [
"90361e2a538d2388506657252ae41aceeb1ba360"
],
"STRIFE0.WAD": [
"bc0a110bf27aee89a0b2fc8111e2391ede891b8d"
],
"STRIFE1.WAD": [
"64c13b951a845ca7f8081f68138a6181557458d1"
],
"TNT.WAD": [
"9fbc66aedef7fe3bae0986cdb9323d2b8db4c9d3"
],
"VOICES.WAD": [
"ec6883100d807b894a98f426d024d22c77b63e7f"
],
@@ -82941,12 +83034,13 @@
"vg8020_basic-bios1.rom": [
"829c00c3114f25b3dae5157c0a238b52a3ac37db"
],
"Complex_4627.bin": [
"6639b6693784574d204c42703a74fd8b088a3a5e",
"3944392c954cfb176d4210544e88353b3c5d36b1"
],
"Complex.bin": [
"358a20e61ec1a2387127b1fa92113034fb279f9b"
],
"Complex_4627.bin": [
"3944392c954cfb176d4210544e88353b3c5d36b1"
],
"cromwell_1024.bin": [
"4e1c2c2ee308ca4591542b3ca48653f65fae6e0f"
],
@@ -91667,8 +91761,8 @@
"70429f1d80503a0632f603bf762fe0bbaa881d22"
],
"sc3000.zip": [
"c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"12de390be2595ad17015310085eaec57ad2b953f"
"12de390be2595ad17015310085eaec57ad2b953f",
"c983bfa2f4c6d077e70e6ff9c7ed59b72368e355"
],
"hisaturn_v103.bin": [
"8c031bf9908fd0142fdd10a9cdd79389f8a3f2fc"
@@ -92833,6 +92927,9 @@
"PSVUPDAT.PUP": [
"cc72dfcc964577cc29112ef368c28f55277c237c"
],
"disk2-13seq.rom": [
"afd060e6f35faf3bb0146fa889fc787adf56330a"
],
"disk2-16boot.rom": [
"d4181c9f046aafc3fb326b381baac809d9e38d16"
],
@@ -95646,7 +95743,6 @@
"de7ddf29": "bc32bc0e8902946663998f56aea52be597d9e361",
"4f923a35": "799e2fc90d6bfd8cb74e331e04d5afd36f2f21a1",
"665cd50f": "e8c40d3a44a41a9b6b5dd3a993d7057b3bfb4086",
"00000000": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"d34eb2ff": "afd060e6f35faf3bb0146fa889fc787adf56330a",
"b72a2c70": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4",
"6105d2ee": "a57f14469867ce0d0865995a645b69f41a0ea718",
@@ -95693,6 +95789,7 @@
"6ee50181": "c96711c01c0158f161791d6fbe75d88329e8ac0a",
"fb0bca9c": "11ad55ee6b11092e810365b8389c1f8b4081e5d0",
"7eba26a4": "2533cc33201da28b2086a0a2fd2b5e04271b6eeb",
"f25cf3a8": "533ef12a6d22726da81a50f08871c6e9a377a328",
"2143196c": "2962e338ccc9f66f29b409f73ca27aeee79633ac",
"ca501374": "f9ad4a4c6b0bbbe39ba358690a48f763ecbd98f0",
"31828d82": "a0f07de6070d98f86d55a4ecd61b4a5b05a4a0d5",
@@ -95700,6 +95797,7 @@
"713d6657": "0783012b4eabca599e460988257ec37500501df6",
"fefb84f1": "6bbbce094422062bd178d6007bed06dcdd0d8b78",
"578e8fde": "bc0acaefb5fac0cdc05476a9f452391d34a5150d",
"bf3dd2ef": "c0c001ec80fa860857000f4cfc9844a28498a355",
"33ca0801": "44bc8180dca3dfdf1b461268919da8efb2e3fb07",
"651b0f2b": "aabedfebe2cd8cfede8c2a3cb9ff33166f0ef953",
"ad37f2de": "eb2a867578a05bbf8741e9fe7204301062df0cb8",
@@ -98360,6 +98458,11 @@
"0f3e6586": "10f1a7204f69b82a18bc94a3010c9660aec0c802",
"e75945f3": "c0ee6f9443fa82c0fef5d497b3e76aa3077f1865",
"0636e0be": "72c60172fb1ba77c9b24b06b7755f0a16f0b3a13",
"cff03d9f": "997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27",
"09b8a6ae": "c745f04a6abc2e6d2a2d52382f45500dd2a260be",
"65816192": "d041456bea851c173f65ac6ab3f2ee61bb0b8b53",
"650b998d": "816c7c6b0098f66c299c9253f62bd908456efb63",
"15f18ddb": "9820e2a3035f0cdd87f69a7d57c59a7a267c9409",
"298dd5b5": "eca9cff1014ce5081804e193588d96c6ddb35432",
"723e60f9": "7742089b4468a736cadb659a7deca3320fe6dcbd",
"162b696a": "5b2e249b9c5133ec987b3ea77596381dc0d6bc1d",
@@ -98731,6 +98834,7 @@
"c443c426": "b484c8fa4549b79cff976001ac4beb19abf7cfb5",
"95db2959": "e7905d16d2ccd57a013c122dc432106cd59ef52c",
"8205795e": "829c00c3114f25b3dae5157c0a238b52a3ac37db",
"ccb97a84": "6639b6693784574d204c42703a74fd8b088a3a5e",
"2c7a2191": "358a20e61ec1a2387127b1fa92113034fb279f9b",
"1dbb7b59": "3944392c954cfb176d4210544e88353b3c5d36b1",
"5ae5278a": "4e1c2c2ee308ca4591542b3ca48653f65fae6e0f",
@@ -101579,6 +101683,7 @@
"bd34bfad": "df89ef91bbaf84f2a109377fd4b17b0050a163e8",
"e5554b80": "aed2cb663b6370efb10969a3373fd84c558edb4f",
"05cfde8c": "891407e147b2c022ebedf8c51fe3a2f497304906",
"19681e91": "bf6b379c204da77dece1aedf83ff35227a623e5d",
"0ffb3127": "5158b728e62b391fb69493743dcf7abbc62abc82",
"5a86cff2": "5992277debadeb64d1c1c64b0a92d9293eaf7e4a",
"cac62307": "53bc1f283cdf00fa2efbb79f2e36d4c8038d743a",
@@ -101927,8 +102032,8 @@
"c94e8c8b": "02c287d10da6de579af7a4ce73b134bbdf23c970",
"0658f691": "88d6499d874dcb5721ff58d76fe1b9af811192e3",
"4dcfd55c": "70429f1d80503a0632f603bf762fe0bbaa881d22",
"48decddc": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"62fb7d82": "12de390be2595ad17015310085eaec57ad2b953f",
"48decddc": "c983bfa2f4c6d077e70e6ff9c7ed59b72368e355",
"6abfefea": "8c031bf9908fd0142fdd10a9cdd79389f8a3f2fc",
"0ab1c9ec": "999ed28cfbf18103a4963b0d3797af3dcf67db05",
"94c90a92": "fefb403a7e91bdaf75ffd8a94c2a1f0ef4ece740",
@@ -102389,6 +102494,9 @@
".variants/naomi.zip.2533cc33": [
"2533cc33201da28b2086a0a2fd2b5e04271b6eeb"
],
".variants/naomi2.zip.533ef12a": [
"533ef12a6d22726da81a50f08871c6e9a377a328"
],
".variants/naomi2.zip.da79eca4": [
"2962e338ccc9f66f29b409f73ca27aeee79633ac"
],
@@ -102408,6 +102516,9 @@
".variants/pgm.zip.bc0acaef": [
"bc0acaefb5fac0cdc05476a9f452391d34a5150d"
],
".variants/pgm.zip.c0c001ec": [
"c0c001ec80fa860857000f4cfc9844a28498a355"
],
".variants/skns.zip.44bc8180": [
"44bc8180dca3dfdf1b461268919da8efb2e3fb07"
],
@@ -105807,6 +105918,21 @@
"win486/ALI1429G.AMW": [
"72c60172fb1ba77c9b24b06b7755f0a16f0b3a13"
],
".variants/DOOM.WAD.997bae5e": [
"997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27"
],
".variants/DOOM2.WAD.c745f04a": [
"c745f04a6abc2e6d2a2d52382f45500dd2a260be"
],
".variants/DOOM64.WAD.d041456b": [
"d041456bea851c173f65ac6ab3f2ee61bb0b8b53"
],
".variants/PLUTONIA.WAD.816c7c6b": [
"816c7c6b0098f66c299c9253f62bd908456efb63"
],
".variants/TNT.WAD.9820e2a3": [
"9820e2a3035f0cdd87f69a7d57c59a7a267c9409"
],
".variants/ecwolf.pk3": [
"f3f2a11f3ecd91cd62d74c3acfad68a4cc6ddbd9"
],
@@ -106572,6 +106698,9 @@
"share/systemroms/vg8020_basic-bios1.rom": [
"829c00c3114f25b3dae5157c0a238b52a3ac37db"
],
".variants/Complex_4627.bin.6639b669": [
"6639b6693784574d204c42703a74fd8b088a3a5e"
],
".variants/syscard1.pce.008cf0f5": [
"008cf0f5cd5e2000b9f2ebf5e4ee84097e6aef74"
],
@@ -114549,6 +114678,9 @@
".variants/neocdz.zip.891407e1": [
"891407e147b2c022ebedf8c51fe3a2f497304906"
],
".variants/neocdz.zip.bf6b379c": [
"bf6b379c204da77dece1aedf83ff35227a623e5d"
],
".variants/uni-bioscd.rom.5158b728": [
"5158b728e62b391fb69493743dcf7abbc62abc82"
],
@@ -115197,8 +115329,8 @@
".variants/bios_MD.bin.453fca4e": [
"453fca4e1db6fae4a10657c4451bccbb71955628"
],
".variants/sc3000.zip.a43aef36": [
"c983bfa2f4c6d077e70e6ff9c7ed59b72368e355"
".variants/sc3000.zip.12de390b": [
"12de390be2595ad17015310085eaec57ad2b953f"
],
".variants/hisaturn_v103.bin": [
"8c031bf9908fd0142fdd10a9cdd79389f8a3f2fc"

View File

@@ -1614,7 +1614,7 @@ systems:
- name: sc3000.zip
destination: sc3000.zip
required: true
md5: a6a47eae38600e41cc67e887e36e70b7
md5: fda6619ba96bf00b849192f5e7460622
zipped_file: sc3000.rom
segaai:
files:

View File

@@ -7,6 +7,7 @@ base_destination: system
cores: all_libretro
hash_type: sha1
verification_mode: existence
case_insensitive_fs: true
systems:
3do:
files:

View File

@@ -5,6 +5,7 @@ source: "https://raw.githubusercontent.com/RetroBat-Official/emulatorlauncher/ma
base_destination: bios
hash_type: md5
verification_mode: md5
case_insensitive_fs: true
cores:
- 81
- a5200

View File

@@ -1613,23 +1613,23 @@ systems:
- name: plus3-0.rom
destination: zxs/plus3-0.rom
required: true
sha1: a837f66977040f7b51ed053a2483c10f3d070ab7
md5: 3abdc20e72890a750dd3c745d286dfba
crc32: a10230c0
sha1: e319ed08b4d53a5e421a75ea00ea02039ba6555b
md5: 9833b8b73384dd5fa3678377ff00a2bb
crc32: 17373da2
size: 16384
- name: plus3-1.rom
destination: zxs/plus3-1.rom
required: true
sha1: 6a4364f25513e4079f048f2de131a896d30edc64
md5: 8361a1d9c8bcef89c0c39293776564ad
crc32: 09b9c3ca
sha1: c9969fc36095a59787554026a9adc3b87678c794
md5: 0f711ceb5ab801b4701989982e0f334c
crc32: f1d1d99e
size: 16384
- name: plus3-2.rom
destination: zxs/plus3-2.rom
required: true
sha1: 0a747cc0b827a94b4fd74cfd818ca792437a38f7
md5: f36c5c2d1f2a682caadeaa6f947db0da
crc32: a60285a0
sha1: 22e50c6ba4157a3f6a821bd9937cd26e292775c6
md5: 3b6dd659d5e4ec97f0e2f7878152c987
crc32: 3dbf351d
size: 16384
- name: plus3-3.rom
destination: zxs/plus3-3.rom
@@ -1641,23 +1641,23 @@ systems:
- name: plus3e-0.rom
destination: zxs/plus3e-0.rom
required: true
sha1: a837f66977040f7b51ed053a2483c10f3d070ab7
md5: 3abdc20e72890a750dd3c745d286dfba
crc32: a10230c0
sha1: 649fbd233490bf58b35350b0123d36caaaa011eb
md5: bc123f625e245c225f92ef05933ed134
crc32: 7f4a5482
size: 16384
- name: plus3e-1.rom
destination: zxs/plus3e-1.rom
required: true
sha1: 6a4364f25513e4079f048f2de131a896d30edc64
md5: 8361a1d9c8bcef89c0c39293776564ad
crc32: 09b9c3ca
sha1: f12198108cbb14de4f03c6695bc16d08c85ee214
md5: 617364264c587d20c9fc4746c29679f2
crc32: 5aeb4675
size: 16384
- name: plus3e-2.rom
destination: zxs/plus3e-2.rom
required: true
sha1: 0a747cc0b827a94b4fd74cfd818ca792437a38f7
md5: f36c5c2d1f2a682caadeaa6f947db0da
crc32: a60285a0
sha1: 773633dce5ba323a9e00d9d0f9e4d8c295df7c87
md5: c363e95dcd0a90e6e7f847e6e47e0179
crc32: 504755cb
size: 16384
- name: plus3e-3.rom
destination: zxs/plus3e-3.rom

View File

@@ -759,16 +759,18 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
Returns {filename: {"checks": [str], "size": int|None, "min_size": int|None,
"max_size": int|None, "crc32": str|None, "md5": str|None, "sha1": str|None,
"adler32": str|None, "crypto_only": [str]}}.
"adler32": str|None, "crypto_only": [str], "per_emulator": {emu: detail}}}.
``crypto_only`` lists validation types we cannot reproduce (signature, crypto)
so callers can report them as non-verifiable rather than silently skipping.
``per_emulator`` preserves each core's individual checks, source_ref, and
expected values before merging, for ground truth reporting.
When multiple emulators reference the same file, merges checks (union).
Raises ValueError if two profiles declare conflicting values.
"""
index: dict[str, dict] = {}
sources: dict[str, dict[str, str]] = {}
for emu_name, profile in profiles.items():
if profile.get("type") in ("launcher", "alias"):
continue
@@ -785,9 +787,8 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
"min_size": None, "max_size": None,
"crc32": set(), "md5": set(), "sha1": set(), "sha256": set(),
"adler32": set(), "crypto_only": set(),
"emulators": set(),
"emulators": set(), "per_emulator": {},
}
sources[fname] = {}
index[fname]["emulators"].add(emu_name)
index[fname]["checks"].update(checks)
# Track non-reproducible crypto checks
@@ -830,6 +831,34 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
if norm.startswith("0x"):
norm = norm[2:]
index[fname]["adler32"].add(norm)
# Per-emulator ground truth detail
expected: dict = {}
if "size" in checks:
for key in ("size", "min_size", "max_size"):
if f.get(key) is not None:
expected[key] = f[key]
for hash_type in ("crc32", "md5", "sha1", "sha256"):
if hash_type in checks and f.get(hash_type):
expected[hash_type] = f[hash_type]
adler_val_pe = f.get("known_hash_adler32") or f.get("adler32")
if adler_val_pe:
expected["adler32"] = adler_val_pe
pe_entry = {
"checks": sorted(checks),
"source_ref": f.get("source_ref"),
"expected": expected,
}
pe = index[fname]["per_emulator"]
if emu_name in pe:
# Merge checks from multiple file entries for same emulator
existing = pe[emu_name]
merged_checks = sorted(set(existing["checks"]) | set(pe_entry["checks"]))
existing["checks"] = merged_checks
existing["expected"].update(pe_entry["expected"])
if pe_entry["source_ref"] and not existing["source_ref"]:
existing["source_ref"] = pe_entry["source_ref"]
else:
pe[emu_name] = pe_entry
# Convert sets to sorted tuples/lists for determinism
for v in index.values():
v["checks"] = sorted(v["checks"])
@@ -839,6 +868,27 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
return index
def build_ground_truth(filename: str, validation_index: dict[str, dict]) -> list[dict]:
"""Format per-emulator ground truth for a file from the validation index.
Returns a sorted list of {emulator, checks, source_ref, expected} dicts.
Returns [] if the file has no emulator validation data.
"""
entry = validation_index.get(filename)
if not entry or not entry.get("per_emulator"):
return []
result = []
for emu_name in sorted(entry["per_emulator"]):
detail = entry["per_emulator"][emu_name]
result.append({
"emulator": emu_name,
"checks": detail["checks"],
"source_ref": detail.get("source_ref"),
"expected": detail.get("expected", {}),
})
return result
def check_file_validation(
local_path: str, filename: str, validation_index: dict[str, dict],
bios_dir: str = "bios",

View File

@@ -250,11 +250,16 @@ def generate_pack(
zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True)
# Case-insensitive dedup only for platforms targeting Windows/macOS.
# Linux-only platforms (Batocera, Recalbox, RetroDECK, Lakka, RomM)
# are case-sensitive and may have distinct files like DISK.ROM vs disk.rom.
case_insensitive = config.get("case_insensitive_fs", False)
total_files = 0
missing_files = []
user_provided = []
seen_destinations: set[str] = set()
seen_lower: set[str] = set() # case-insensitive dedup for Windows/macOS
seen_lower: set[str] = set() # only used when case_insensitive=True
# Per-file status: worst status wins (missing > untested > ok)
file_status: dict[str, str] = {}
file_reasons: dict[str, str] = {}
@@ -293,7 +298,7 @@ def generate_pack(
full_dest = dest
dedup_key = full_dest
already_packed = dedup_key in seen_destinations or dedup_key.lower() in seen_lower
already_packed = dedup_key in seen_destinations or (case_insensitive and dedup_key.lower() in seen_lower)
storage = file_entry.get("storage", "embedded")
@@ -301,7 +306,8 @@ def generate_pack(
if already_packed:
continue
seen_destinations.add(dedup_key)
seen_lower.add(dedup_key.lower())
if case_insensitive:
seen_lower.add(dedup_key.lower())
file_status.setdefault(dedup_key, "ok")
instructions = file_entry.get("instructions", "Please provide this file manually.")
instr_name = f"INSTRUCTIONS_{file_entry['name']}.txt"
@@ -326,7 +332,8 @@ def generate_pack(
else:
zf.write(tmp_path, full_dest)
seen_destinations.add(dedup_key)
seen_lower.add(dedup_key.lower())
if case_insensitive:
seen_lower.add(dedup_key.lower())
file_status.setdefault(dedup_key, "ok")
total_files += 1
else:
@@ -401,7 +408,8 @@ def generate_pack(
if already_packed:
continue
seen_destinations.add(dedup_key)
seen_lower.add(dedup_key.lower())
if case_insensitive:
seen_lower.add(dedup_key.lower())
extract = file_entry.get("extract", False)
if extract and local_path.endswith(".zip"):
@@ -437,7 +445,7 @@ def generate_pack(
if full_dest in seen_destinations:
continue
# Skip case-insensitive duplicates (Windows/macOS FS safety)
if full_dest.lower() in seen_lower:
if full_dest.lower() in seen_lower and case_insensitive:
continue
local_path, status = resolve_file(fe, db, bios_dir, zip_contents)
@@ -449,7 +457,8 @@ def generate_pack(
else:
zf.write(local_path, full_dest)
seen_destinations.add(full_dest)
seen_lower.add(full_dest.lower())
if case_insensitive:
seen_lower.add(full_dest.lower())
core_count += 1
total_files += 1
@@ -468,16 +477,22 @@ def generate_pack(
print(f" WARNING: data directory '{ref_key}' not cached at {local_path} — run refresh_data_dirs.py")
continue
dd_dest = dd.get("destination", "")
dd_prefix = f"{base_dest}/{dd_dest}" if base_dest else dd_dest
if base_dest and dd_dest:
dd_prefix = f"{base_dest}/{dd_dest}"
elif base_dest:
dd_prefix = base_dest
else:
dd_prefix = dd_dest
for root, _dirs, filenames in os.walk(local_path):
for fname in filenames:
src = os.path.join(root, fname)
rel = os.path.relpath(src, local_path)
full = f"{dd_prefix}/{rel}"
if full in seen_destinations or full.lower() in seen_lower:
if full in seen_destinations or full.lower() in seen_lower and case_insensitive:
continue
seen_destinations.add(full)
seen_lower.add(full.lower())
if case_insensitive:
seen_lower.add(full.lower())
zf.write(src, full)
total_files += 1
@@ -1156,16 +1171,130 @@ def generate_sha256sums(output_dir: str) -> str | None:
return sums_path
def verify_and_finalize_packs(output_dir: str, db: dict) -> bool:
def verify_pack_against_platform(
zip_path: str, platform_name: str, platforms_dir: str,
db: dict | None = None, emulators_dir: str = "emulators",
emu_profiles: dict | None = None,
) -> tuple[bool, int, int, list[str]]:
"""Verify a pack ZIP against its platform config and core requirements.
Checks:
1. Every baseline file declared by the platform exists in the ZIP
at the correct destination path
2. Every in-repo core extra file (from emulator profiles) is present
3. No duplicate entries
4. No path anomalies (double slash, absolute, traversal)
5. No unexpected zero-byte BIOS files
Returns (all_ok, checked, present, errors).
"""
from collections import Counter
from verify import find_undeclared_files
config = load_platform_config(platform_name, platforms_dir)
base_dest = config.get("base_destination", "")
errors: list[str] = []
checked = 0
present = 0
if emu_profiles is None:
emu_profiles = load_emulator_profiles(emulators_dir)
with zipfile.ZipFile(zip_path, "r") as zf:
zip_set = set(zf.namelist())
zip_lower = {n.lower(): n for n in zip_set}
# Structural checks
dupes = sum(1 for c in Counter(zf.namelist()).values() if c > 1)
if dupes:
errors.append(f"{dupes} duplicate entries")
for n in zip_set:
if "//" in n:
errors.append(f"double slash: {n}")
if n.startswith("/"):
errors.append(f"absolute path: {n}")
if ".." in n:
errors.append(f"path traversal: {n}")
# Zero-byte check (exclude Dolphin GraphicMods markers)
for info in zf.infolist():
if info.file_size == 0 and not info.is_dir():
if "GraphicMods" not in info.filename and info.filename != "manifest.json":
errors.append(f"zero-byte: {info.filename}")
# 1. Baseline file presence
baseline_checked = 0
baseline_present = 0
for sys_id, system in config.get("systems", {}).items():
for fe in system.get("files", []):
dest = fe.get("destination", fe.get("name", ""))
if not dest:
continue
expected = f"{base_dest}/{dest}" if base_dest else dest
baseline_checked += 1
if expected in zip_set or expected.lower() in zip_lower:
baseline_present += 1
else:
errors.append(f"baseline missing: {expected}")
# 2. Core extras presence (files from emulator profiles, in repo)
core_checked = 0
core_present = 0
if db is not None:
undeclared = find_undeclared_files(config, emulators_dir, db, emu_profiles)
for u in undeclared:
if not u["in_repo"]:
continue
dest = u.get("path") or u["name"]
if base_dest:
full = f"{base_dest}/{dest}"
elif "/" not in dest:
full = f"bios/{dest}"
else:
full = dest
core_checked += 1
if full in zip_set or full.lower() in zip_lower:
core_present += 1
# Not an error if missing — some get deduped or filtered
checked = baseline_checked + core_checked
present = baseline_present + core_present
return (len(errors) == 0, checked, present, errors,
baseline_checked, baseline_present, core_checked, core_present)
def verify_and_finalize_packs(output_dir: str, db: dict,
platforms_dir: str = "platforms") -> bool:
"""Verify all packs, inject manifests, generate SHA256SUMS.
Two-stage verification:
1. Hash check against database.json (integrity)
2. Extract + verify against platform config (conformance)
Returns True if all packs pass verification.
"""
all_ok = True
# Map ZIP names to platform names
pack_to_platform: dict[str, list[str]] = {}
for name in sorted(os.listdir(output_dir)):
if not name.endswith(".zip"):
continue
for pname in list_registered_platforms(platforms_dir):
cfg = load_platform_config(pname, platforms_dir)
display = cfg.get("platform", pname).replace(" ", "_")
if display in name or display.replace("_", "") in name.replace("_", ""):
pack_to_platform.setdefault(name, []).append(pname)
for name in sorted(os.listdir(output_dir)):
if not name.endswith(".zip"):
continue
zip_path = os.path.join(output_dir, name)
# Stage 1: database integrity
ok, manifest = verify_pack(zip_path, db)
summary = manifest["summary"]
status = "OK" if ok else "ERRORS"
@@ -1176,6 +1305,23 @@ def verify_and_finalize_packs(output_dir: str, db: dict) -> bool:
print(f" ERROR: {err}")
all_ok = False
inject_manifest(zip_path, manifest)
# Stage 2: platform conformance (extract + verify)
platforms = pack_to_platform.get(name, [])
for pname in platforms:
(p_ok, total, matched, p_errors,
bl_checked, bl_present, core_checked, core_present) = \
verify_pack_against_platform(
zip_path, pname, platforms_dir, db=db,
)
status = "OK" if p_ok else "FAILED"
print(f" platform {pname}: {bl_present}/{bl_checked} baseline, "
f"{core_present}/{core_checked} cores, {status}")
if not p_ok:
for err in p_errors:
print(f" {err}")
all_ok = False
generate_sha256sums(output_dir)
return all_ok

View File

@@ -213,6 +213,7 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
"python scripts/verify.py --all",
"python scripts/verify.py --platform batocera",
"python scripts/verify.py --emulator flycast",
"python scripts/verify.py --platform retroarch --verbose # emulator ground truth",
"```",
"",
f"Only dependency: Python 3 + `pyyaml`.",

View File

@@ -35,7 +35,8 @@ except ImportError:
sys.path.insert(0, os.path.dirname(__file__))
from common import (
_build_validation_index, build_zip_contents_index, check_file_validation,
_build_validation_index, _parse_validation, build_ground_truth,
build_zip_contents_index, check_file_validation,
check_inside_zip, compute_hashes, filter_files_by_mode,
filter_systems_by_target, group_identical_platforms, list_emulator_profiles,
list_system_ids, load_data_dir_registry, load_emulator_profiles,
@@ -201,6 +202,24 @@ def compute_severity(
# Cross-reference: undeclared files used by cores
# ---------------------------------------------------------------------------
def _build_expected(file_entry: dict, checks: list[str]) -> dict:
"""Extract expected validation values from an emulator profile file entry."""
expected: dict = {}
if not checks:
return expected
if "size" in checks:
for key in ("size", "min_size", "max_size"):
if file_entry.get(key) is not None:
expected[key] = file_entry[key]
for hash_type in ("crc32", "md5", "sha1", "sha256"):
if hash_type in checks and file_entry.get(hash_type):
expected[hash_type] = file_entry[hash_type]
adler_val = file_entry.get("known_hash_adler32") or file_entry.get("adler32")
if adler_val:
expected["adler32"] = adler_val
return expected
def find_undeclared_files(
config: dict,
emulators_dir: str,
@@ -247,6 +266,9 @@ def find_undeclared_files(
fname = f.get("name", "")
if not fname or fname in seen:
continue
# Skip pattern placeholders (e.g., <user-selected>.bin)
if "<" in fname or ">" in fname or "*" in fname:
continue
# Mode filtering: skip files incompatible with platform's usage
file_mode = f.get("mode")
if file_mode == "standalone" and not is_standalone:
@@ -264,6 +286,7 @@ def find_undeclared_files(
in_repo = fname in by_name or fname.rsplit("/", 1)[-1] in by_name
seen.add(fname)
checks = _parse_validation(f.get("validation"))
undeclared.append({
"emulator": profile.get("emulator", emu_name),
"name": fname,
@@ -273,6 +296,9 @@ def find_undeclared_files(
"category": f.get("category", "bios"),
"in_repo": in_repo,
"note": f.get("note", ""),
"checks": sorted(checks) if checks else [],
"source_ref": f.get("source_ref"),
"expected": _build_expected(f, checks),
})
return undeclared
@@ -322,15 +348,21 @@ def find_exclusion_notes(
})
continue
# Count standalone-only files
standalone_files = [f for f in profile.get("files", []) if f.get("mode") == "standalone"]
if standalone_files:
names = [f["name"] for f in standalone_files[:3]]
more = f" +{len(standalone_files)-3}" if len(standalone_files) > 3 else ""
notes.append({
"emulator": emu_display, "reason": "standalone_only",
"detail": f"{len(standalone_files)} files for standalone mode only ({', '.join(names)}{more})",
})
# Count standalone-only files — but only report as excluded if the
# platform does NOT use this emulator in standalone mode
standalone_set = set(str(c) for c in config.get("standalone_cores", []))
is_standalone = emu_name in standalone_set or bool(
standalone_set & {str(c) for c in profile.get("cores", [])}
)
if not is_standalone:
standalone_files = [f for f in profile.get("files", []) if f.get("mode") == "standalone"]
if standalone_files:
names = [f["name"] for f in standalone_files[:3]]
more = f" +{len(standalone_files)-3}" if len(standalone_files) > 3 else ""
notes.append({
"emulator": emu_display, "reason": "standalone_only",
"detail": f"{len(standalone_files)} files for standalone mode only ({', '.join(names)}{more})",
})
return notes
@@ -434,6 +466,9 @@ def verify_platform(
result["discrepancy"] = f"{platform} says OK but {emus} says {reason}"
result["system"] = sys_id
result["hle_fallback"] = hle_index.get(file_entry.get("name", ""), False)
result["ground_truth"] = build_ground_truth(
file_entry.get("name", ""), validation_index,
)
details.append(result)
# Aggregate by destination
@@ -466,15 +501,34 @@ def verify_platform(
undeclared = find_undeclared_files(config, emulators_dir, db, emu_profiles, target_cores=target_cores)
exclusions = find_exclusion_notes(config, emulators_dir, emu_profiles, target_cores=target_cores)
# Ground truth coverage
gt_filenames = set(validation_index)
dest_to_name: dict[str, str] = {}
for sys_id, system in verify_systems.items():
for fe in system.get("files", []):
dest = fe.get("destination", fe.get("name", ""))
if not dest:
dest = f"{sys_id}/{fe.get('name', '')}"
dest_to_name.setdefault(dest, fe.get("name", ""))
with_validation = sum(
1 for dest in file_status if dest_to_name.get(dest, "") in gt_filenames
)
total = len(file_status)
return {
"platform": platform,
"verification_mode": mode,
"total_files": len(file_status),
"total_files": total,
"severity_counts": counts,
"status_counts": status_counts,
"undeclared_files": undeclared,
"exclusion_notes": exclusions,
"details": details,
"ground_truth_coverage": {
"with_validation": with_validation,
"platform_only": total - with_validation,
"total": total,
},
}
@@ -482,7 +536,39 @@ def verify_platform(
# Output
# ---------------------------------------------------------------------------
def print_platform_result(result: dict, group: list[str]) -> None:
def _format_ground_truth_aggregate(ground_truth: list[dict]) -> str:
"""Format ground truth as a single aggregated line.
Example: beetle_psx [md5], pcsx_rearmed [existence]
"""
parts = []
for gt in ground_truth:
checks_label = "+".join(gt["checks"]) if gt["checks"] else "existence"
parts.append(f"{gt['emulator']} [{checks_label}]")
return ", ".join(parts)
def _format_ground_truth_verbose(ground_truth: list[dict]) -> list[str]:
"""Format ground truth as one line per core with expected values and source ref.
Example: handy validates size=512,crc32=0d973c9d [rom.h:48-49]
"""
lines = []
for gt in ground_truth:
checks_label = "+".join(gt["checks"]) if gt["checks"] else "existence"
expected = gt.get("expected", {})
if expected:
vals = ",".join(f"{k}={v}" for k, v in sorted(expected.items()))
part = f"{gt['emulator']} validates {vals}"
else:
part = f"{gt['emulator']} validates {checks_label}"
if gt.get("source_ref"):
part += f" [{gt['source_ref']}]"
lines.append(part)
return lines
def print_platform_result(result: dict, group: list[str], verbose: bool = False) -> None:
mode = result["verification_mode"]
total = result["total_files"]
c = result["severity_counts"]
@@ -525,6 +611,13 @@ def print_platform_result(result: dict, group: list[str]) -> None:
hle = ", HLE available" if d.get("hle_fallback") else ""
reason = d.get("reason", "")
print(f" UNTESTED ({req}{hle}): {key}{reason}")
gt = d.get("ground_truth", [])
if gt:
if verbose:
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
else:
print(f" {_format_ground_truth_aggregate(gt)}")
for d in result["details"]:
if d["status"] == Status.MISSING:
key = f"{d['system']}/{d['name']}"
@@ -534,6 +627,13 @@ def print_platform_result(result: dict, group: list[str]) -> None:
req = "required" if d.get("required", True) else "optional"
hle = ", HLE available" if d.get("hle_fallback") else ""
print(f" MISSING ({req}{hle}): {key}")
gt = d.get("ground_truth", [])
if gt:
if verbose:
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
else:
print(f" {_format_ground_truth_aggregate(gt)}")
for d in result["details"]:
disc = d.get("discrepancy")
if disc:
@@ -542,6 +642,27 @@ def print_platform_result(result: dict, group: list[str]) -> None:
continue
seen_details.add(key)
print(f" DISCREPANCY: {key}{disc}")
gt = d.get("ground_truth", [])
if gt:
if verbose:
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
else:
print(f" {_format_ground_truth_aggregate(gt)}")
if verbose:
for d in result["details"]:
if d["status"] == Status.OK:
key = f"{d['system']}/{d['name']}"
if key in seen_details:
continue
seen_details.add(key)
gt = d.get("ground_truth", [])
if gt:
req = "required" if d.get("required", True) else "optional"
print(f" OK ({req}): {key}")
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
# Cross-reference: undeclared files used by cores
undeclared = result.get("undeclared_files", [])
@@ -555,45 +676,73 @@ def print_platform_result(result: dict, group: list[str]) -> None:
opt_in_repo = [u for u in bios_files if not u["required"] and u["in_repo"]]
opt_not_in_repo = [u for u in bios_files if not u["required"] and not u["in_repo"]]
summary_parts = []
# Core coverage: files from emulator profiles not declared by the platform
core_in_pack = len(req_in_repo) + len(opt_in_repo)
core_missing_req = len(req_not_in_repo) + len(req_hle_not_in_repo)
core_missing_opt = len(opt_not_in_repo)
core_total = len(bios_files)
print(f" Core files: {core_in_pack} in pack, {core_missing_req} required missing, {core_missing_opt} optional missing")
# Required NOT in repo = critical
if req_not_in_repo:
summary_parts.append(f"{len(req_not_in_repo)} required NOT in repo")
for u in req_not_in_repo:
print(f" MISSING (required): {u['emulator']} needs {u['name']}")
checks = u.get("checks", [])
if checks:
if verbose:
expected = u.get("expected", {})
if expected:
vals = ",".join(f"{k}={v}" for k, v in sorted(expected.items()))
ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else ""
print(f" validates {vals}{ref_part}")
else:
checks_label = "+".join(checks)
ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else ""
print(f" validates {checks_label}{ref_part}")
else:
checks_label = "+".join(checks)
print(f" [{checks_label}]")
if req_hle_not_in_repo:
summary_parts.append(f"{len(req_hle_not_in_repo)} required with HLE NOT in repo")
if req_in_repo:
summary_parts.append(f"{len(req_in_repo)} required in repo")
if opt_in_repo:
summary_parts.append(f"{len(opt_in_repo)} optional in repo")
if opt_not_in_repo:
summary_parts.append(f"{len(opt_not_in_repo)} optional NOT in repo")
for u in req_hle_not_in_repo:
print(f" MISSING (required, HLE fallback): {u['emulator']} needs {u['name']}")
checks = u.get("checks", [])
if checks:
if verbose:
expected = u.get("expected", {})
if expected:
vals = ",".join(f"{k}={v}" for k, v in sorted(expected.items()))
ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else ""
print(f" validates {vals}{ref_part}")
else:
checks_label = "+".join(checks)
ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else ""
print(f" validates {checks_label}{ref_part}")
else:
checks_label = "+".join(checks)
print(f" [{checks_label}]")
if game_data:
gd_missing = [u for u in game_data if not u["in_repo"]]
gd_present = [u for u in game_data if u["in_repo"]]
if gd_missing:
summary_parts.append(f"{len(gd_missing)} game_data NOT in repo")
if gd_present:
summary_parts.append(f"{len(gd_present)} game_data in repo")
print(f" Core gaps: {len(undeclared)} undeclared ({', '.join(summary_parts)})")
if gd_missing or gd_present:
print(f" Game data: {len(gd_present)} in pack, {len(gd_missing)} missing")
# Show critical gaps (required bios + no HLE + not in repo)
for u in req_not_in_repo:
print(f" {u['emulator']}{u['name']} (required, NOT in repo)")
# Show required with HLE (core works but not ideal)
for u in req_hle_not_in_repo:
print(f" {u['emulator']}{u['name']} (required, HLE available, NOT in repo)")
# Show required in repo (actionable)
for u in req_in_repo[:10]:
print(f" {u['emulator']}{u['name']} (required, in repo)")
if len(req_in_repo) > 10:
print(f" ... and {len(req_in_repo) - 10} more required in repo")
# Intentional exclusions (explain why certain emulator files are NOT included)
# No external files (explain why certain emulator files are NOT included)
exclusions = result.get("exclusion_notes", [])
if exclusions:
print(f" Intentional exclusions ({len(exclusions)}):")
print(f" No external files ({len(exclusions)}):")
for ex in exclusions:
print(f" {ex['emulator']}{ex['detail']} [{ex['reason']}]")
# Ground truth coverage footer
gt_cov = result.get("ground_truth_coverage")
if gt_cov and gt_cov["total"] > 0:
pct = gt_cov["with_validation"] * 100 // gt_cov["total"]
print(f" Ground truth: {gt_cov['with_validation']}/{gt_cov['total']} files have emulator validation ({pct}%)")
if gt_cov["platform_only"]:
print(f" {gt_cov['platform_only']} platform-only (no emulator profile)")
# ---------------------------------------------------------------------------
# Emulator/system mode verification
@@ -671,6 +820,7 @@ def verify_emulator(
details = []
file_status: dict[str, str] = {}
file_severity: dict[str, str] = {}
dest_to_name: dict[str, str] = {}
data_dir_notices: list[str] = []
for emu_name, profile in selected:
@@ -692,6 +842,7 @@ def verify_emulator(
"name": f"({emu_name})", "status": Status.OK,
"required": False, "system": "",
"note": f"No files needed for {profile.get('emulator', emu_name)}",
"ground_truth": [],
})
continue
@@ -715,8 +866,10 @@ def verify_emulator(
"required": required}
result["system"] = file_entry.get("system", "")
result["hle_fallback"] = False
result["ground_truth"] = build_ground_truth(archive, validation_index)
details.append(result)
dest = archive
dest_to_name[dest] = archive
cur = result["status"]
prev = file_status.get(dest)
if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0):
@@ -754,10 +907,12 @@ def verify_emulator(
result["system"] = file_entry.get("system", "")
result["hle_fallback"] = hle
result["ground_truth"] = build_ground_truth(name, validation_index)
details.append(result)
# Aggregate by destination (path if available, else name)
dest = file_entry.get("path", "") or name
dest_to_name[dest] = name
cur = result["status"]
prev = file_status.get(dest)
if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0):
@@ -776,14 +931,25 @@ def verify_emulator(
label = _effective_validation_label(details, validation_index)
gt_filenames = set(validation_index)
total = len(file_status)
with_validation = sum(
1 for dest in file_status if dest_to_name.get(dest, "") in gt_filenames
)
return {
"emulators": [n for n, _ in selected],
"verification_mode": label,
"total_files": len(file_status),
"total_files": total,
"severity_counts": counts,
"status_counts": status_counts,
"details": details,
"data_dir_notices": sorted(set(data_dir_notices)),
"ground_truth_coverage": {
"with_validation": with_validation,
"platform_only": total - with_validation,
"total": total,
},
}
@@ -823,7 +989,7 @@ def verify_system(
return verify_emulator(matching, emulators_dir, db, standalone)
def print_emulator_result(result: dict) -> None:
def print_emulator_result(result: dict, verbose: bool = False) -> None:
"""Print verification result for emulator/system mode."""
label = " + ".join(result["emulators"])
mode = result["verification_mode"]
@@ -851,6 +1017,13 @@ def print_emulator_result(result: dict) -> None:
hle = ", HLE available" if d.get("hle_fallback") else ""
reason = d.get("reason", "")
print(f" UNTESTED ({req}{hle}): {d['name']}{reason}")
gt = d.get("ground_truth", [])
if gt:
if verbose:
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
else:
print(f" {_format_ground_truth_aggregate(gt)}")
for d in result["details"]:
if d["status"] == Status.MISSING:
if d["name"] in seen:
@@ -859,13 +1032,41 @@ def print_emulator_result(result: dict) -> None:
req = "required" if d.get("required", True) else "optional"
hle = ", HLE available" if d.get("hle_fallback") else ""
print(f" MISSING ({req}{hle}): {d['name']}")
gt = d.get("ground_truth", [])
if gt:
if verbose:
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
else:
print(f" {_format_ground_truth_aggregate(gt)}")
for d in result["details"]:
if d.get("note"):
print(f" {d['note']}")
if verbose:
for d in result["details"]:
if d["status"] == Status.OK:
if d["name"] in seen:
continue
seen.add(d["name"])
gt = d.get("ground_truth", [])
if gt:
req = "required" if d.get("required", True) else "optional"
print(f" OK ({req}): {d['name']}")
for line in _format_ground_truth_verbose(gt):
print(f" {line}")
for ref in result.get("data_dir_notices", []):
print(f" Note: data directory '{ref}' required but not included (use refresh_data_dirs.py)")
# Ground truth coverage footer
gt_cov = result.get("ground_truth_coverage")
if gt_cov and gt_cov["total"] > 0:
pct = gt_cov["with_validation"] * 100 // gt_cov["total"]
print(f" Ground truth: {gt_cov['with_validation']}/{gt_cov['total']} files have emulator validation ({pct}%)")
if gt_cov["platform_only"]:
print(f" {gt_cov['platform_only']} platform-only (no emulator profile)")
def main():
parser = argparse.ArgumentParser(description="Platform-native BIOS verification")
@@ -882,6 +1083,7 @@ def main():
parser.add_argument("--db", default=DEFAULT_DB)
parser.add_argument("--platforms-dir", default=DEFAULT_PLATFORMS_DIR)
parser.add_argument("--emulators-dir", default=DEFAULT_EMULATORS_DIR)
parser.add_argument("--verbose", "-v", action="store_true", help="Show emulator ground truth details")
parser.add_argument("--json", action="store_true", help="JSON output")
args = parser.parse_args()
@@ -929,7 +1131,7 @@ def main():
result["details"] = [d for d in result["details"] if d["status"] != Status.OK]
print(json.dumps(result, indent=2))
else:
print_emulator_result(result)
print_emulator_result(result, verbose=args.verbose)
return
# System mode
@@ -940,7 +1142,7 @@ def main():
result["details"] = [d for d in result["details"] if d["status"] != Status.OK]
print(json.dumps(result, indent=2))
else:
print_emulator_result(result)
print_emulator_result(result, verbose=args.verbose)
return
# Platform mode (existing)
@@ -994,7 +1196,7 @@ def main():
if not args.json:
for result, group in group_results:
print_platform_result(result, group)
print_platform_result(result, group, verbose=args.verbose)
print()
if args.json:

View File

@@ -353,13 +353,15 @@ class TestE2E(unittest.TestCase):
"files": [
# Size validation — correct size (16 bytes = len(b"PRESENT_REQUIRED"))
{"name": "present_req.bin", "required": True,
"validation": ["size"], "size": 16},
"validation": ["size"], "size": 16,
"source_ref": "test.c:10-20"},
# Size validation — wrong expected size
{"name": "present_opt.bin", "required": False,
"validation": ["size"], "size": 9999},
# CRC32 validation — correct crc32
{"name": "correct_hash.bin", "required": True,
"validation": ["crc32"], "crc32": "91d0b1d3"},
"validation": ["crc32"], "crc32": "91d0b1d3",
"source_ref": "hash.c:42"},
# CRC32 validation — wrong crc32
{"name": "no_md5.bin", "required": False,
"validation": ["crc32"], "crc32": "deadbeef"},
@@ -1353,5 +1355,190 @@ class TestE2E(unittest.TestCase):
self.assertNotIn("bios_b.bin", names)
# ---------------------------------------------------------------
# Validation index per-emulator ground truth (Task: ground truth)
# ---------------------------------------------------------------
def test_111_validation_index_per_emulator(self):
"""Validation index includes per-emulator detail for ground truth."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
entry = index["present_req.bin"]
self.assertIn("per_emulator", entry)
pe = entry["per_emulator"]
self.assertIn("test_validation", pe)
detail = pe["test_validation"]
self.assertIn("size", detail["checks"])
self.assertEqual(detail["expected"]["size"], 16)
def test_112_build_ground_truth(self):
"""build_ground_truth returns per-emulator detail for a filename."""
from common import build_ground_truth
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
gt = build_ground_truth("present_req.bin", index)
self.assertIsInstance(gt, list)
self.assertTrue(len(gt) >= 1)
emu_names = {g["emulator"] for g in gt}
self.assertIn("test_validation", emu_names)
for g in gt:
if g["emulator"] == "test_validation":
self.assertIn("size", g["checks"])
self.assertIn("source_ref", g)
self.assertIn("expected", g)
def test_113_build_ground_truth_empty(self):
"""build_ground_truth returns [] for unknown filename."""
from common import build_ground_truth
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
gt = build_ground_truth("nonexistent.bin", index)
self.assertEqual(gt, [])
def test_114_platform_result_has_ground_truth(self):
"""verify_platform attaches ground_truth to each detail entry."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
self.assertIn("ground_truth", d)
# present_req.bin has validation in test_validation profile
for d in result["details"]:
if d["name"] == "present_req.bin":
self.assertTrue(len(d["ground_truth"]) >= 1)
emu_names = {g["emulator"] for g in d["ground_truth"]}
self.assertIn("test_validation", emu_names)
break
else:
self.fail("present_req.bin not found in details")
def test_116_undeclared_files_have_ground_truth(self):
"""find_undeclared_files attaches ground truth fields."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(config, self.emulators_dir, self.db, profiles)
for u in undeclared:
self.assertIn("checks", u)
self.assertIn("source_ref", u)
self.assertIn("expected", u)
def test_117_platform_result_ground_truth_coverage(self):
"""verify_platform includes ground truth coverage counts."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
gt = result["ground_truth_coverage"]
self.assertIn("with_validation", gt)
self.assertIn("total", gt)
self.assertIn("platform_only", gt)
self.assertEqual(gt["total"], result["total_files"])
self.assertEqual(gt["platform_only"], gt["total"] - gt["with_validation"])
self.assertGreaterEqual(gt["with_validation"], 1)
def test_118_emulator_result_has_ground_truth(self):
"""verify_emulator attaches ground_truth to each detail entry."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
for d in result["details"]:
self.assertIn("ground_truth", d)
# present_req.bin should have ground truth from test_validation
for d in result["details"]:
if d["name"] == "present_req.bin":
self.assertTrue(len(d["ground_truth"]) >= 1)
break
def test_119_emulator_result_ground_truth_coverage(self):
"""verify_emulator includes ground truth coverage counts."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
gt = result["ground_truth_coverage"]
self.assertEqual(gt["total"], result["total_files"])
def test_115_platform_result_ground_truth_empty_for_unknown(self):
"""Files with no emulator validation get ground_truth=[]."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
if d["name"] == "missing_opt.bin":
self.assertEqual(d["ground_truth"], [])
break
def test_120_format_ground_truth_aggregate(self):
"""Aggregate format: one line with all cores."""
from verify import _format_ground_truth_aggregate
gt = [
{"emulator": "beetle_psx", "checks": ["md5"], "source_ref": "libretro.cpp:252", "expected": {"md5": "abc"}},
{"emulator": "pcsx_rearmed", "checks": ["existence"], "source_ref": None, "expected": {}},
]
line = _format_ground_truth_aggregate(gt)
self.assertIn("beetle_psx", line)
self.assertIn("[md5]", line)
self.assertIn("pcsx_rearmed", line)
self.assertIn("[existence]", line)
def test_121_format_ground_truth_verbose(self):
"""Verbose format: one line per core with expected values and source ref."""
from verify import _format_ground_truth_verbose
gt = [
{"emulator": "handy", "checks": ["size", "crc32"],
"source_ref": "rom.h:48-49", "expected": {"size": 512, "crc32": "0d973c9d"}},
]
lines = _format_ground_truth_verbose(gt)
self.assertEqual(len(lines), 1)
self.assertIn("handy", lines[0])
self.assertIn("size=512", lines[0])
self.assertIn("crc32=0d973c9d", lines[0])
self.assertIn("[rom.h:48-49]", lines[0])
def test_122_format_ground_truth_verbose_no_source_ref(self):
"""Verbose format omits bracket when source_ref is None."""
from verify import _format_ground_truth_verbose
gt = [
{"emulator": "core_a", "checks": ["existence"], "source_ref": None, "expected": {}},
]
lines = _format_ground_truth_verbose(gt)
self.assertEqual(len(lines), 1)
self.assertNotIn("[", lines[0])
def test_123_ground_truth_full_chain_verbose(self):
"""Full chain: file -> platform -> emulator -> source_ref visible in ground_truth."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
if d["name"] == "present_req.bin":
gt = d["ground_truth"]
for g in gt:
if g["emulator"] == "test_validation":
self.assertIn("size", g["checks"])
self.assertEqual(g["source_ref"], "test.c:10-20")
self.assertEqual(g["expected"]["size"], 16)
return
self.fail("present_req.bin / test_validation ground truth not found")
def test_124_ground_truth_json_includes_all(self):
"""JSON output includes ground_truth on all detail entries."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
# Simulate --json filtering (non-OK only) — ground_truth must survive
filtered = [d for d in result["details"] if d["status"] != Status.OK]
for d in filtered:
self.assertIn("ground_truth", d)
# Also check OK entries have it (before filtering)
ok_entries = [d for d in result["details"] if d["status"] == Status.OK]
for d in ok_entries:
self.assertIn("ground_truth", d)
def test_125_ground_truth_coverage_in_md5_mode(self):
"""MD5 platform also gets ground truth coverage."""
config = load_platform_config("test_md5", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
gt = result["ground_truth_coverage"]
self.assertEqual(gt["total"], result["total_files"])
self.assertGreaterEqual(gt["with_validation"], 1)
if __name__ == "__main__":
unittest.main()