fix: serve BizHawk's database as BizHawk wrote it

The export only indexed firmware BizHawk leaves unhashed on purpose,
per-console dumps declared with SHA1Checksum.Dummy, and could only
apply that hash to a same-named call of another system. Every hash
the file states is BizHawk's own, so nothing is rewritten and no
correction is reported.
This commit is contained in:
Abdessamad Derraz committed 2026-10-18 04:09:40 +02:00
1 parent 9c0a430f5c
commit 23d9474f41
2 files changed
+28 -111

No files matched your search

+20 -105
View File
@@ -2,35 +2,24 @@
The database is C#: every firmware is a call whose arguments are the SHA1,
the size, the file name and a description, wired into option lists and
status flags that only the source expresses. The calls are rewritten in
place, and nothing else in the file is touched.
status flags that only the source expresses. Every hash it states is
BizHawk's own and every hash it leaves out is left out on purpose, so the
file is served as BizHawk wrote it, checked for its structure.
"""
from __future__ import annotations
import re
from .base_exporter import BaseExporter
from .baseline import NativeFile, NativeSystem, Report, _hash_values
from .baseline import NativeFile, NativeSystem, Report
SOURCE_URL = (
"https://raw.githubusercontent.com/TASEmulators/BizHawk/master"
"/src/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs"
)
# File("<sha1>", <size>, "<name>", ...) and the same three arguments inside
# FirmwareAndOption(<sha1>, <size>, <system>, <id>, <name>, ...).
_FILE_CALL = re.compile(
r'(File\(\s*")([0-9A-Fa-f]{40})("\s*,\s*)(\d+)(\s*,\s*")([^"]+)(")'
)
_FIRMWARE_AND_OPTION = re.compile(
r'(FirmwareAndOption\(\s*")([0-9A-Fa-f]{40})("\s*,\s*)(\d+)'
r'(\s*,\s*"[^"]*"\s*,\s*"[^"]*"\s*,\s*")([^"]+)(")'
)
class Exporter(BaseExporter):
"""Write BizHawk's FirmwareDatabase.cs, corrected."""
"""Serve BizHawk's FirmwareDatabase.cs as BizHawk wrote it."""
@staticmethod
def platform_name() -> str:
@@ -40,18 +29,14 @@ class Exporter(BaseExporter):
def native_filename() -> str:
return "FirmwareDatabase.cs"
@staticmethod
def carries() -> frozenset[str]:
return frozenset({"sha1", "size"})
@staticmethod
def native_sources() -> dict[str, str]:
return {"FirmwareDatabase.cs": SOURCE_URL}
@staticmethod
def can_add() -> bool:
# A firmware is a call wired into an option list and a status; the
# exporter corrects the calls that exist, it does not write C#.
# A firmware is a call wired into an option list and a status: the
# exporter does not write C#.
return False
@staticmethod
@@ -60,49 +45,23 @@ class Exporter(BaseExporter):
# hang off exist nowhere else.
return True
def states(self, fe: NativeFile, field_name: str) -> bool:
"""Only a call the render rewrote receives the change.
@staticmethod
def carries() -> frozenset[str]:
return frozenset({"sha1", "size"})
A name that resolves to several SHA1s is left alone, and the entries
BizHawk declares without a hash are FirmwareAndOption(SHA1Checksum
.Dummy, ...) calls, which hold no hash to replace.
def states(self, fe: NativeFile, field_name: str) -> bool:
"""No hash or size is written, so no correction is delivered.
Every call holding a hash holds one BizHawk declares, the authority
for its cores; the calls without one are SHA1Checksum.Dummy, a
per-console dump (DSi firmware, NAND) BizHawk leaves unhashed on
purpose. The rewrite this exporter had could only ever fire on a
same-named call of another system, giving it the wrong system's hash.
"""
written = getattr(self, "_written", None)
if field_name in ("sha1", "size") and written is not None and fe.name.casefold() not in written:
if field_name in ("sha1", "size"):
return False
return super().states(fe, field_name)
@staticmethod
def _unambiguous(systems: dict[str, NativeSystem]) -> dict[str, NativeFile]:
"""Files whose name identifies exactly one entry with a SHA1.
BizHawk names a firmware by file name inside a system, and the same
name recurs across systems. Correcting on a name that resolves to
two different sets of bytes would corrupt the database, so only the
names that resolve to one are touched.
"""
seen: dict[str, list[NativeFile]] = {}
for system in systems.values():
for fe in system.files:
# A SHA1 BizHawk declares is BizHawk's own code, the authority
# for its cores: the truth's MSX.rom came from FBNeo and would
# have made MSXHawk refuse the dump it accepts. An entry the
# database never declared (can_add refuses it) names no call
# of its own either: by name it would rewrite the homonym of
# another system.
if (
fe.platform is not None
and fe.hash("sha1")
and not _hash_values(fe.platform, "sha1")
):
seen.setdefault(fe.name.casefold(), []).append(fe)
resolved: dict[str, NativeFile] = {}
for name, entries in seen.items():
hashes = {fe.hash("sha1").lower() for fe in entries}
if len(hashes) == 1:
resolved[name] = entries[0]
return resolved
def render(
self,
systems: dict[str, NativeSystem],
@@ -116,37 +75,7 @@ class Exporter(BaseExporter):
"FirmwareDatabase.cs cannot be written without BizHawk's own "
"file: the database is C#, not data"
)
index = self._unambiguous(systems)
self._written: set[str] = set()
def commented_out(text: str, position: int) -> bool:
"""Whether the call sits on a line the compiler never sees.
BizHawk keeps disabled entries in place behind //, and a hash
written into one of those is a change to a comment.
"""
line_start = text.rfind("\n", 0, position) + 1
return text[line_start:position].lstrip().startswith("//")
def rewrite(match: re.Match[str], name_group: int) -> str:
if commented_out(match.string, match.start()):
return match.group(0)
name = match.group(name_group)
fe = index.get(name.casefold())
if fe is None:
return match.group(0)
sha1 = fe.hash("sha1").upper()
size = fe.size() or int(match.group(4))
groups = list(match.groups())
groups[1] = sha1
groups[3] = str(size)
self._written.add(name.casefold())
return "".join(groups)
patched = _FILE_CALL.sub(lambda m: rewrite(m, 6), source)
patched = _FIRMWARE_AND_OPTION.sub(lambda m: rewrite(m, 6), patched)
return {self.native_filename(): patched}
return {self.native_filename(): source}
def validate(
self,
@@ -160,18 +89,4 @@ class Exporter(BaseExporter):
issues.append("the class the database lives in is missing")
if content.count("{") != content.count("}"):
issues.append("braces are unbalanced, the file would not compile")
index = self._unambiguous(systems)
declared: dict[str, str] = {}
for pattern in (_FILE_CALL, _FIRMWARE_AND_OPTION):
for match in pattern.finditer(content):
line_start = content.rfind("\n", 0, match.start()) + 1
if content[line_start : match.start()].lstrip().startswith("//"):
continue
declared[match.group(6).casefold()] = match.group(2).lower()
for name, fe in index.items():
written = declared.get(name)
if written is not None and written != fe.hash("sha1").lower():
issues.append(f"hash not applied: {fe.name}")
return issues
+8 -6
View File
@@ -849,12 +849,14 @@ class BizhawkExport(unittest.TestCase):
self.assertEqual(produced, source)
self.assertFalse(exporter.states(entry, "sha1"))
def test_a_fill_the_call_holds_is_counted(self):
exporter, entry, _, produced = self._export(
"bios.bin", f'File("{"0" * 40}", 4096, "bios.bin", "BIOS");'
)
self.assertIn(self.SHA1.upper(), produced)
self.assertTrue(exporter.states(entry, "sha1"))
def test_a_homonym_in_another_system_keeps_bizhawks_hash(self):
"""A Dummy entry the truth gained a SHA1 for (DSi_Firmware.bin) could
only be rewritten through a same-named File call of another system,
which then took the NDS hash and refused the dump it accepted."""
call = f'File("{"0" * 40}", 131072, "DSi_Firmware.bin", "Other system");'
exporter, entry, source, produced = self._export("DSi_Firmware.bin", call)
self.assertEqual(produced, source)
self.assertFalse(exporter.states(entry, "sha1"))
class RetrodeckExport(unittest.TestCase):