mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 17:33:30 -05:00
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:
1 parent
9c0a430f5c
commit
23d9474f41
2 files changed
+28
-111
No files matched your search
@@ -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
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user