diff --git a/scripts/exporter/bizhawk_exporter.py b/scripts/exporter/bizhawk_exporter.py index bff18a75..22401be3 100644 --- a/scripts/exporter/bizhawk_exporter.py +++ b/scripts/exporter/bizhawk_exporter.py @@ -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("", , "", ...) and the same three arguments inside -# FirmwareAndOption(, , , , , ...). -_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 diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 7e99eb78..d2e048a9 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -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):