573in1/tools/decodeDump.py

197 lines
5.0 KiB
Python
Raw Normal View History

2023-05-30 18:08:52 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__version__ = "0.3.0"
__author__ = "spicyjpeg"
import sys
from argparse import ArgumentParser, FileType, Namespace
from enum import IntEnum, IntFlag
from struct import Struct
from typing import BinaryIO, ByteString, Mapping, Sequence, TextIO
from zlib import decompress
## Base45 decoder
BASE45_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
def decodeBase45(data: str) -> bytearray:
mapped: map = map(BASE45_CHARSET.index, data)
output: bytearray = bytearray()
for a, b, c in zip(mapped, mapped, mapped):
value: int = a + (b * 45) + (c * 2025)
output.append(value >> 8)
output.append(value & 0xff)
return output
def serialNumberToString(_id: ByteString) -> str:
value: int = int.from_bytes(_id[1:7], "little")
if value >= 100000000:
return "xxxx-xxxx"
return f"{(value // 10000) % 10000:04d}-{value % 10000:04d}"
## Dump parser
DUMP_START: str = "573::"
DUMP_END: str = "::"
DUMP_STRUCT: Struct = Struct("< 3B x 8s 8s 8s 8s 8s 512s")
DUMP_VERSION: int = 1
class ChipType(IntEnum):
TYPE_NONE = 0
TYPE_X76F041 = 1
TYPE_X76F100 = 2
TYPE_ZS01 = 3
class CartFlag(IntFlag):
HAS_DIGITAL_IO = 1 << 0
HAS_DS2401 = 1 << 1
CONFIG_OK = 1 << 2
SYSTEM_ID_OK = 1 << 3
CART_ID_OK = 1 << 4
ZS_ID_OK = 1 << 5
PUBLIC_DATA_OK = 1 << 6
PRIVATE_DATA_OK = 1 << 7
CHIP_NAMES: Mapping[ChipType, str] = {
ChipType.TYPE_NONE: "None",
ChipType.TYPE_X76F041: "Xicor X76F041",
ChipType.TYPE_X76F100: "Xicor X76F100",
ChipType.TYPE_ZS01: "Konami ZS01 (PIC16CE625)"
}
DATA_LENGTHS: Mapping[ChipType, int] = {
ChipType.TYPE_NONE: 0,
ChipType.TYPE_X76F041: 512,
ChipType.TYPE_X76F100: 112,
ChipType.TYPE_ZS01: 112
}
def toPrintableChar(value: int):
if (value < 0x20) or (value > 0x7e):
return "."
return chr(value)
def hexdump(data: ByteString | Sequence[int], output: TextIO, width: int = 16):
for i in range(0, len(data), width):
hexBytes: map = map(lambda value: f"{value:02x}", data[i:i + width])
hexLine: str = " ".join(hexBytes).ljust(width * 3 - 1)
asciiBytes: map = map(toPrintableChar, data[i:i + width])
asciiLine: str = "".join(asciiBytes).ljust(width)
output.write(f"{i:04x}: {hexLine} |{asciiLine}|\n")
def parseDump(
dumpString: str, logOutput: TextIO | None = None,
exportOutput: BinaryIO | None = None
):
_dumpString: str = dumpString.strip().upper()
if (
not _dumpString.startswith(DUMP_START) or
not _dumpString.endswith(DUMP_END)
):
raise ValueError(f"dump string does not begin with '{DUMP_START}' and end with '{DUMP_END}'")
_dumpString = _dumpString[len(DUMP_START):-len(DUMP_END)]
dump: bytes = decompress(decodeBase45(_dumpString))
version, chipType, flags, dataKey, config, systemID, cartID, zsID, data = \
DUMP_STRUCT.unpack(dump[0:DUMP_STRUCT.size])
if version != DUMP_VERSION:
raise ValueError(f"unsupported dump version {version}")
chipType: ChipType = ChipType(chipType)
flags: CartFlag = CartFlag(flags)
data: bytes = data[0:DATA_LENGTHS[chipType]]
if logOutput:
if flags & CartFlag.SYSTEM_ID_OK:
logOutput.write(f"Digital I/O ID: {systemID.hex('-')}\n")
logOutput.write(f"Serial number: {serialNumberToString(systemID)}\n")
logOutput.write(f"Cartridge type: {CHIP_NAMES[chipType]}\n")
if flags & CartFlag.CART_ID_OK:
logOutput.write(f"DS2401 identifier: {cartID.hex('-')}\n")
if flags & CartFlag.ZS_ID_OK:
logOutput.write(f"ZS01 identifier: {zsID.hex('-')}\n")
if flags & CartFlag.CONFIG_OK:
logOutput.write(f"Configuration: {config.hex('-')}\n")
logOutput.write("\nEEPROM dump:\n")
hexdump(data, logOutput)
logOutput.write("\n")
if exportOutput:
pass # TODO: implement exporting
## Main
def createParser() -> ArgumentParser:
parser = ArgumentParser(
description = "Decodes the contents of a QR code generated by the tool.",
add_help = False
)
group = parser.add_argument_group("Tool options")
group.add_argument(
"-h", "--help",
action = "help",
help = "Show this help message and exit"
)
group = parser.add_argument_group("File paths")
group.add_argument(
"-i", "--input",
type = FileType("rt"),
help = "read dump from specified file",
metavar = "file"
)
group.add_argument(
"-l", "--log",
type = FileType("at"),
default = sys.stdout,
help = "log cartridge info to specified file (stdout by default)",
metavar = "file"
)
group.add_argument(
"-e", "--export",
type = FileType("wb"),
help = "export dump in MAME format to specified file",
metavar = "file"
)
group.add_argument(
"dump",
type = str,
nargs = "?",
help = "QR string to decode (if -i was not passed)"
)
return parser
def main():
parser: ArgumentParser = createParser()
args: Namespace = parser.parse_args()
if args.input:
with args.input as _file:
dump: str = _file.read()
elif args.dump:
dump: str = args.dump
else:
parser.error("a dump must be passed on the command line or using -i")
parseDump(dump, args.log, args.export)
if __name__ == "__main__":
main()