feat: add sha1 verification mode for bizhawk

This commit is contained in:
Abdessamad Derraz
2026-03-28 09:35:13 +01:00
parent f1855641c5
commit b75f2b2a43
2 changed files with 88 additions and 2 deletions

View File

@@ -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.

View File

@@ -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")