mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-16 16:33:27 -05:00
349 lines
16 KiB
Python
349 lines
16 KiB
Python
"""A profile's checks are the ones the emulator's code runs.
|
|
|
|
Each case reads a profile from emulators/ and offers its entry files of
|
|
chosen sizes or contents, then compares the verdict with what the code at
|
|
the profile's pin does with the same file. The cases come from reading that
|
|
code: a check the code does not run rejects a file the emulator loads, and a
|
|
check left out lets through a file the emulator refuses.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
from validation import _build_validation_index, check_file_validation # noqa: E402
|
|
|
|
|
|
def _profile(name: str) -> dict:
|
|
with open(REPO_ROOT / "emulators" / f"{name}.yml", encoding="utf-8") as fh:
|
|
return yaml.safe_load(fh)
|
|
|
|
|
|
class _Probe(unittest.TestCase):
|
|
"""Offer a profile entry a file and read the verdict."""
|
|
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup)
|
|
|
|
def accepts(self, emulator: str, name: str, data: bytes, strict: bool = False) -> bool:
|
|
"""strict counts a check the code only warns about."""
|
|
index = _build_validation_index({emulator: _profile(emulator)})
|
|
path = Path(self._tmp.name) / name
|
|
path.write_bytes(data)
|
|
return check_file_validation(
|
|
str(path), name, index, emulators={emulator}, strict=strict
|
|
) is None
|
|
|
|
|
|
class BeebEmTeletextFont(_Probe):
|
|
"""BuildMode7Font reads 96 characters of 18 two-byte rows, 3456 bytes,
|
|
and returns false at the first EOF, which stops BeebEm at startup;
|
|
bytes past them are never read (Src/Video.cpp:167-215 at a982ba6)."""
|
|
|
|
def test_short_is_refused_and_longer_is_read(self):
|
|
self.assertFalse(self.accepts("beebem", "Teletext.fnt", b"\0" * 100))
|
|
self.assertTrue(self.accepts("beebem", "Teletext.fnt", b"\0" * 3456))
|
|
self.assertTrue(self.accepts("beebem", "Teletext.fnt", b"\0" * 4000))
|
|
|
|
|
|
class BeebEmSystemRoms(_Probe):
|
|
"""The OS, Tube, Z80 and Master 512 ROMs are read with one fread of the
|
|
slot's size. A short read reports an error and BeebEm runs with what it
|
|
read; bytes past the slot are never read (Src/BeebMem.cpp:1291-1308,
|
|
Src/Tube.cpp:1540-1553, Src/Z80Support.cpp:247-261,
|
|
Src/Master512CoPro.cpp:3777-3790 at a982ba6)."""
|
|
|
|
CASES = (("OS12.rom", 16384), ("B+MOS.rom", 16384), ("MOS.rom", 16384),
|
|
("MOS400.rom", 16384), ("6502Tube.rom", 2048), ("Z80.rom", 4096),
|
|
("BIOS.rom", 16384))
|
|
|
|
def test_longer_is_read_and_shorter_only_warns(self):
|
|
for name, size in self.CASES:
|
|
with self.subTest(name=name):
|
|
self.assertTrue(self.accepts("beebem", name, b"\0" * (size * 2)))
|
|
self.assertTrue(self.accepts("beebem", name, b"\0" * (size // 2)))
|
|
self.assertFalse(
|
|
self.accepts("beebem", name, b"\0" * (size // 2), strict=True)
|
|
)
|
|
|
|
|
|
class BeebEmSidewaysRoms(_Probe):
|
|
"""ReadRom loads any file up to MAX_PALROM_SIZE (131072) into a bank and
|
|
refuses a larger one, leaving the bank empty (Src/BeebMem.cpp:1207-1262,
|
|
Src/PALRom.h:52 at a982ba6)."""
|
|
|
|
def test_up_to_128k_is_read_and_larger_refused(self):
|
|
for size in (8192, 16384, 32768, 131072):
|
|
with self.subTest(size=size):
|
|
self.assertTrue(self.accepts("beebem", "BASIC2.rom", b"\0" * size))
|
|
self.assertFalse(self.accepts("beebem", "BASIC2.rom", b"\0" * 200000))
|
|
|
|
|
|
class BeetleLynxBootRom(_Probe):
|
|
"""CRom drops a boot ROM under 512 bytes and keeps the 0x88 fill, so no
|
|
game boots; a longer one is read for its first 512 bytes
|
|
(mednafen/lynx/rom.cpp:61-75 at fcdefcf)."""
|
|
|
|
def test_short_is_refused_and_longer_is_read(self):
|
|
self.assertFalse(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 256))
|
|
self.assertTrue(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 512))
|
|
self.assertTrue(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 1024))
|
|
|
|
|
|
class BeetleGbaBios(_Probe):
|
|
"""CPUInit copies the BIOS only when it is exactly 0x4000 bytes and
|
|
otherwise falls back to HLE (mednafen/gba/GBA.cpp:2639-2673 at
|
|
b158166)."""
|
|
|
|
def test_only_16k_is_used(self):
|
|
self.assertTrue(self.accepts("beetle_gba", "gba_bios.bin", b"\0" * 16384))
|
|
for size in (1000, 32768):
|
|
with self.subTest(size=size):
|
|
self.assertFalse(self.accepts("beetle_gba", "gba_bios.bin", b"\0" * size))
|
|
|
|
|
|
class Atari800SystemRoms(_Probe):
|
|
"""SYSROM_FindInDir takes a file of an allowed length whose CRC32 is in
|
|
SYSROM_roms, else, by its lowercased name, any file of that length as a
|
|
custom ROM (atari800/src/sysrom.c:88-112, 281-315, 387-416 at 4e7fbc7).
|
|
BB01R4_OS.ROM and XEGAME.ROM are on no name list: only their CRC32 makes
|
|
them that ROM."""
|
|
|
|
def test_named_roms_need_only_the_length(self):
|
|
for name, size in (("ATARIOSA.ROM", 10240), ("ATARIOSB.ROM", 10240),
|
|
("ATARIXL.ROM", 16384), ("5200.rom", 2048),
|
|
("ATARIBAS.ROM", 8192)):
|
|
with self.subTest(name=name):
|
|
self.assertTrue(self.accepts("atari800", name, b"\1" * size))
|
|
self.assertFalse(self.accepts("atari800", name, b"\1" * (size + 1)))
|
|
|
|
def test_unlisted_names_need_their_crc(self):
|
|
for name, size in (("BB01R4_OS.ROM", 16384), ("XEGAME.ROM", 8192)):
|
|
with self.subTest(name=name):
|
|
self.assertFalse(self.accepts("atari800", name, b"\1" * size))
|
|
|
|
|
|
class AresAtari5200Bios(unittest.TestCase):
|
|
"""ares registers an Atari 5200 firmware by sha256 and refuses to load a
|
|
game without it (desktop-ui/emulator/atari-5200.cpp:12-16, 82-88 at
|
|
4cb8d92); the profile left the system out, so no pack carried the BIOS."""
|
|
|
|
def test_the_profile_declares_it(self):
|
|
profile = _profile("ares")
|
|
self.assertIn("atari-5200", profile["systems"])
|
|
entry = next(f for f in profile["files"] if f.get("system") == "atari-5200")
|
|
self.assertTrue(entry["required"])
|
|
self.assertEqual(
|
|
entry["sha256"],
|
|
"06b250f18983d058c0f156ce7ee88ae48b6eaf11e6f10f21dccf6ac7ffb6a6af",
|
|
)
|
|
|
|
|
|
class A5200Bios(_Probe):
|
|
"""The core reads 0x800 bytes and falls back to the Altirra OS on a short
|
|
read; bytes past them are never read (libretro/libretro.c:456-468 at
|
|
40c6f2f)."""
|
|
|
|
def test_short_falls_back_and_longer_is_read(self):
|
|
self.assertFalse(self.accepts("a5200", "5200.rom", b"\0" * 1024))
|
|
self.assertTrue(self.accepts("a5200", "5200.rom", b"\0" * 2048))
|
|
self.assertTrue(self.accepts("a5200", "5200.rom", b"\0" * 4096))
|
|
|
|
|
|
class BeetlePceSystemCards(_Probe):
|
|
"""PCE_LoadCD hands the file to HuC_Load, which skips a 512-byte copier
|
|
header and checks no hash (mednafen/pce/pce.cpp:441-457,
|
|
mednafen/pce/huc.cpp:185-202 at b96c11e). The profile carried md5 and
|
|
sha1 values no code holds, and a headered card the core boots was
|
|
reported as a hash discrepancy."""
|
|
|
|
def test_a_headered_card_is_not_judged(self):
|
|
self.assertTrue(self.accepts("beetle_pce", "syscard3.pce", b"\0" * 262656))
|
|
|
|
def test_no_hash_the_code_does_not_hold(self):
|
|
for entry in _profile("beetle_pce")["files"]:
|
|
with self.subTest(name=entry["name"]):
|
|
self.assertFalse({"md5", "sha1", "crc32", "sha256"} & set(entry))
|
|
|
|
|
|
class EightyOneRoms(_Probe):
|
|
"""memory_load reads the ROM and CRC32Block's value only picks the tape
|
|
patches in rompatch.c; a ROM the table does not know loads without them
|
|
(81/zx81/zx81.c:102-103, 81/Ace/ace.c:77-78, 81/zx81/rompatch.c at
|
|
fa70949). The profile declared a crc32 check with no value."""
|
|
|
|
def test_an_unknown_rom_is_not_refused(self):
|
|
for name, size in (("zx81.edition3.rom", 8192), ("zx80.rom", 4096),
|
|
("jupiterace.rom", 8192)):
|
|
with self.subTest(name=name):
|
|
self.assertTrue(self.accepts("81", name, b"\1" * size))
|
|
entry = next(f for f in _profile("81")["files"] if f["name"] == name)
|
|
self.assertNotIn("crc32", entry.get("validation") or [])
|
|
|
|
|
|
class AdvanceMameArchives(_Probe):
|
|
"""verify_length_and_hash checks each member and only adds a warning; the
|
|
archive itself has no size or hash anyone checks (src/romload.c:327-363
|
|
at 21f8f6f). The 26 BIOS archives declared archive-level checks with no
|
|
archive values."""
|
|
|
|
def test_no_archive_level_check(self):
|
|
# a7800's MAME warns the same way (src/emu/romload.cpp:427-452 at 7a2afdc).
|
|
for emulator in ("advancemame", "a7800"):
|
|
for entry in _profile(emulator)["files"]:
|
|
if entry.get("category") != "bios_zip":
|
|
continue
|
|
with self.subTest(emulator=emulator, name=entry["name"]):
|
|
self.assertFalse(entry.get("validation"))
|
|
self.assertTrue(entry.get("contents"))
|
|
|
|
|
|
class MameMemberChecksOnlyWarn(unittest.TestCase):
|
|
"""libretro MAME and mame2015 warn on a member's wrong length or checksums
|
|
and run (src/emu/romload.cpp:602-625 at 9069f39, src/emu/romload.c:501-519
|
|
at 7adbf44); an archive-level check with no archive value ran nothing."""
|
|
|
|
def test_no_valueless_archive_check(self):
|
|
for emulator, names in (
|
|
("mame", ("vsmile.zip", "qsound.zip", "ym2413.zip", "ym2608.zip")),
|
|
("cdi2015", ("cdimono1.zip",)),
|
|
):
|
|
entries = {f["name"]: f for f in _profile(emulator)["files"]}
|
|
for name in names:
|
|
with self.subTest(emulator=emulator, name=name):
|
|
self.assertFalse(entries[name].get("validation"))
|
|
|
|
|
|
class HypseusArchives(unittest.TestCase):
|
|
"""Hypseus checks each member's size and CRC32, never the archive
|
|
(game.cpp:636-906 at cda8796); its 71 archives declared archive-level
|
|
checks with no archive value."""
|
|
|
|
def test_no_archive_level_check(self):
|
|
for entry in _profile("hypseus-singe")["files"]:
|
|
if entry.get("category") == "bios_zip":
|
|
with self.subTest(name=entry["name"]):
|
|
self.assertFalse(entry.get("validation"))
|
|
|
|
|
|
class DolphinForksIplDumps(_Probe):
|
|
"""Load_BS2 compares the IPL's CRC32 with six Redump dumps and only
|
|
raises "The IPL file is not a known good dump" before booting it
|
|
(Source/Core/Core/Boot/Boot.cpp:296-337 at Dolphin-MMJR 1b095b6,
|
|
408-449 at Dolphin-MMJR2 4d0fdb8). The profiles declared the check with
|
|
no value, so nothing told a known dump from any other."""
|
|
|
|
def test_an_unknown_ipl_only_warns(self):
|
|
for emulator in ("dolphin-mmjr", "dolphin-mmjr2"):
|
|
with self.subTest(emulator=emulator):
|
|
data = b"\0" * 2097152
|
|
self.assertTrue(self.accepts(emulator, "IPL.bin", data))
|
|
self.assertFalse(self.accepts(emulator, "IPL.bin", data, strict=True))
|
|
|
|
|
|
class GambatteBootRoms(_Probe):
|
|
"""get_bootloader_from_file reads 0x100 or 0x900 bytes and gives up on a
|
|
short read; it never reads past them (libgambatte/libretro/libretro.cpp
|
|
1426-1486 at d9d6cd0). The upstream crc32 check the profile claimed has
|
|
no code behind it: sinamas/gambatte was reset to a README in 2021."""
|
|
|
|
def test_short_is_refused_and_longer_is_read(self):
|
|
for name, size in (("gb_bios.bin", 256), ("gbc_bios.bin", 2304)):
|
|
with self.subTest(name=name):
|
|
self.assertFalse(self.accepts("gambatte", name, b"\0" * (size - 1)))
|
|
self.assertTrue(self.accepts("gambatte", name, b"\0" * (size + 512)))
|
|
|
|
|
|
class GeargrafxCards(_Probe):
|
|
"""LoadBiosData strips a 512-byte header and refuses any other size; the
|
|
CRC32 only names the card in the log, and an unknown LaserActive CRC32
|
|
is logged and loaded (src/media.cpp:570-632, 662-705, 1028-1063 at
|
|
d0307bf). The profiles declared crc32 checks with no value."""
|
|
|
|
def test_size_decides_and_an_unknown_card_loads(self):
|
|
for name, size in (("syscard3.pce", 262144), ("gexpress.pce", 32768),
|
|
("pac-n10.bin", 524288)):
|
|
with self.subTest(name=name):
|
|
self.assertTrue(self.accepts("geargrafx", name, b"\1" * size, strict=True))
|
|
self.assertTrue(self.accepts("geargrafx", name, b"\1" * (size + 512)))
|
|
self.assertFalse(self.accepts("geargrafx", name, b"\1" * (size - 1)))
|
|
|
|
|
|
class MgbaGameBoyBootRoms(_Probe):
|
|
"""The libretro port hands the boot ROM to the core and GBReset drops one
|
|
whose CRC32 GBIsCompatibleBIOS does not list for the model
|
|
(src/gb/gb.c:50-63, 605-628, 661-665 at 7a12d6d). The profile declared
|
|
the crc32 check with no value."""
|
|
|
|
def test_an_unknown_boot_rom_is_dropped(self):
|
|
for name, size in (("gb_bios.bin", 256), ("sgb_bios.bin", 256),
|
|
("gbc_bios.bin", 2304)):
|
|
with self.subTest(name=name):
|
|
self.assertFalse(self.accepts("mgba", name, b"\0" * size))
|
|
|
|
|
|
class NestopiaFdsBios(_Probe):
|
|
"""Fds::Bios logs "warning, unknown BIOS ROM!" for a CRC32 other than the
|
|
Famicom and Twin Famicom ones and uses the ROM (source/core/NstFds.cpp
|
|
57-127 at b9fdc9c). The profile declared the check with no value."""
|
|
|
|
def test_an_unknown_bios_only_warns(self):
|
|
self.assertTrue(self.accepts("nestopia", "disksys.rom", b"\0" * 8192))
|
|
self.assertFalse(self.accepts("nestopia", "disksys.rom", b"\0" * 8192, strict=True))
|
|
|
|
|
|
class AppleIIgsRomSizes(_Probe):
|
|
"""GSplus and KEGS read a 32, 128 or 256 KB ROM and refuse any other size
|
|
("The ROM size should be 128K or 256K", gsplus/src/config.c:1297-1316 at
|
|
41e7103). The profiles declared the size check with no size."""
|
|
|
|
def test_three_sizes_and_nothing_else(self):
|
|
for emulator in ("gsplus", "kegs"):
|
|
for size in (32768, 131072, 262144):
|
|
with self.subTest(emulator=emulator, size=size):
|
|
self.assertTrue(self.accepts(emulator, "ROM", b"\0" * size))
|
|
with self.subTest(emulator=emulator, size=65536):
|
|
self.assertFalse(self.accepts(emulator, "ROM", b"\0" * 65536))
|
|
|
|
|
|
class AltirraHappy810(_Probe):
|
|
"""ATFirmwareDetection recognises the pre-v7 Happy 810 ROM by its size
|
|
(3K, 4K or a doubled overdump) and a CRC32 that skips the hidden 1K and
|
|
the serial; the whole-file CRC32 the profile checked is never computed,
|
|
so a dump with another serial was refused (firmwaredetect.cpp:186-231 in
|
|
the 4.40 source)."""
|
|
|
|
def test_any_serial_passes_and_size_only_warns(self):
|
|
self.assertTrue(self.accepts("altirra", "happy-810-prev7-4k.rom", b"\5" * 4096, strict=True))
|
|
self.assertTrue(self.accepts("altirra", "happy-810-prev7-3k.rom", b"\5" * 3072, strict=True))
|
|
self.assertFalse(self.accepts("altirra", "happy-810-prev7-4k.rom", b"\5" * 5000, strict=True))
|
|
self.assertTrue(self.accepts("altirra", "happy-810-prev7-4k.rom", b"\5" * 5000))
|
|
|
|
|
|
class AzaharConsoleUniqueData(_Probe):
|
|
"""LoadSecureInfoA, LoadLocalFriendCodeSeedB and LoadMovable refuse a
|
|
file of the wrong size, and only log a failed signature: the data is
|
|
loaded and used (src/core/hw/unique_data.cpp:43-123, 165-202,
|
|
src/core/hle/service/cfg/cfg.cpp:405-410 at 9dd12da)."""
|
|
|
|
def test_size_refuses_and_signature_only_warns(self):
|
|
for name, size in (("SecureInfo_A", 273), ("LocalFriendCodeSeed_B", 272),
|
|
("movable.sed", 288)):
|
|
with self.subTest(name=name):
|
|
self.assertTrue(self.accepts("azahar", name, b"\0" * size))
|
|
self.assertFalse(self.accepts("azahar", name, b"\0" * size, strict=True))
|
|
self.assertFalse(self.accepts("azahar", name, b"\0" * (size + 7)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|