Files
libretro/tests/test_profile_checks.py
T

334 lines
15 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))
if __name__ == "__main__":
unittest.main()