diff --git a/scripts/verify.py b/scripts/verify.py index f804924b..0004f478 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -7,6 +7,7 @@ Replicates the exact verification logic of each platform: - Recalbox: MD5 + mandatory/hashMatchMandatory, 3-color severity (Bios.cpp:109-130) - RetroBat: same as Batocera - EmuDeck: MD5 whitelist per system +- BizHawk: SHA1 firmware hash verification Cross-references emulator profiles to detect undeclared files used by available cores. @@ -159,6 +160,31 @@ def verify_entry_md5( "reason": f"expected {md5_list[0][:12]}… got {actual_md5[:12]}…"} +def verify_entry_sha1( + file_entry: dict, + local_path: str | None, +) -> dict: + """SHA1 verification — BizHawk firmware hash check.""" + name = file_entry.get("name", "") + expected_sha1 = file_entry.get("sha1", "") + required = file_entry.get("required", True) + base = {"name": name, "required": required} + + if not local_path: + return {**base, "status": Status.MISSING} + + if not expected_sha1: + return {**base, "status": Status.OK, "path": local_path} + + hashes = compute_hashes(local_path) + actual_sha1 = hashes["sha1"].lower() + if actual_sha1 == expected_sha1.lower(): + return {**base, "status": Status.OK, "path": local_path} + + return {**base, "status": Status.UNTESTED, "path": local_path, + "reason": f"expected {expected_sha1[:12]}… got {actual_sha1[:12]}…"} + + # --------------------------------------------------------------------------- # Severity mapping per platform # --------------------------------------------------------------------------- @@ -170,8 +196,8 @@ def compute_severity( Based on native platform behavior + emulator HLE capability: - RetroArch (existence): required+missing = warning, optional+missing = info - - Batocera (md5): no required distinction (batocera-systems has no mandatory field) - - Recalbox (md5): mandatory+missing = critical, optional+missing = warning + - Batocera/Recalbox/RetroBat/EmuDeck (md5): hash-based verification + - BizHawk (sha1): same severity rules as md5 - hle_fallback: core works without this file via HLE → always INFO when missing """ if status == Status.OK: @@ -448,6 +474,8 @@ def verify_platform( result = verify_entry_existence( file_entry, local_path, validation_index, ) + elif mode == "sha1": + result = verify_entry_sha1(file_entry, local_path) else: result = verify_entry_md5(file_entry, local_path, resolve_status) # Emulator-level validation: informational for platform packs. diff --git a/tests/test_e2e.py b/tests/test_e2e.py index c223e20a..86bdfdcb 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -112,6 +112,7 @@ class TestE2E(unittest.TestCase): self._create_md5_platform() self._create_shared_groups() self._create_inherited_platform() + self._create_sha1_platform() # -- Create emulator YAMLs -- self._create_emulator_profiles() @@ -284,6 +285,31 @@ class TestE2E(unittest.TestCase): with open(os.path.join(self.platforms_dir, "test_inherited.yml"), "w") as fh: yaml.dump(child, fh) + def _create_sha1_platform(self): + f = self.files + config = { + "platform": "TestSHA1", + "verification_mode": "sha1", + "base_destination": "system", + "systems": { + "sys-sha1": { + "files": [ + {"name": "correct_hash.bin", "destination": "correct_hash.bin", + "sha1": f["correct_hash.bin"]["sha1"], "required": True}, + {"name": "wrong_hash.bin", "destination": "wrong_hash.bin", + "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "required": True}, + {"name": "missing_sha1.bin", "destination": "missing_sha1.bin", + "sha1": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "required": True}, + {"name": "optional_missing_sha1.bin", "destination": "optional_missing_sha1.bin", + "sha1": "cccccccccccccccccccccccccccccccccccccccc", "required": False}, + {"name": "no_md5.bin", "destination": "no_md5.bin", "required": True}, + ], + }, + }, + } + with open(os.path.join(self.platforms_dir, "test_sha1.yml"), "w") as fh: + yaml.dump(config, fh) + def _create_emulator_profiles(self): # Regular emulator with aliases, standalone file, undeclared file emu = { @@ -520,6 +546,38 @@ class TestE2E(unittest.TestCase): c = result["severity_counts"] self.assertGreater(c[Severity.WARNING], 0) + def test_25_verify_sha1_platform(self): + config = load_platform_config("test_sha1", self.platforms_dir) + result = verify_platform(config, self.db, self.emulators_dir) + self.assertEqual(result["total_files"], 5) + self.assertEqual(result["verification_mode"], "sha1") + ok_count = result["severity_counts"][Severity.OK] + self.assertEqual(ok_count, 2) + + def test_26_sha1_mismatch_is_warning(self): + config = load_platform_config("test_sha1", self.platforms_dir) + result = verify_platform(config, self.db, self.emulators_dir) + by_name = {d["name"]: d for d in result["details"]} + self.assertEqual(by_name["wrong_hash.bin"]["status"], Status.UNTESTED) + + def test_27_sha1_missing_required_is_critical(self): + config = load_platform_config("test_sha1", self.platforms_dir) + result = verify_platform(config, self.db, self.emulators_dir) + c = result["severity_counts"] + self.assertGreater(c[Severity.CRITICAL], 0) + + def test_28_sha1_missing_optional_is_warning(self): + config = load_platform_config("test_sha1", self.platforms_dir) + result = verify_platform(config, self.db, self.emulators_dir) + c = result["severity_counts"] + self.assertGreater(c[Severity.WARNING], 0) + + def test_29_sha1_no_hash_is_existence_check(self): + config = load_platform_config("test_sha1", self.platforms_dir) + result = verify_platform(config, self.db, self.emulators_dir) + by_name = {d["name"]: d for d in result["details"]} + self.assertEqual(by_name["no_md5.bin"]["status"], Status.OK) + def test_30_inheritance_inherits_systems(self): config = load_platform_config("test_inherited", self.platforms_dir) self.assertEqual(config["platform"], "TestInherited")