573in1/tools/decodeDump.py

160 lines
4.2 KiB
Python
Raw Normal View History

2023-05-30 18:08:52 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2023-06-07 21:39:02 +02:00
__version__ = "0.3.1"
2023-05-30 18:08:52 +02:00
__author__ = "spicyjpeg"
import sys
from argparse import ArgumentParser, FileType, Namespace
from struct import Struct
from typing import BinaryIO, ByteString, Mapping, Sequence, TextIO
from zlib import decompress
2023-06-07 21:39:02 +02:00
from _common import *
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
## Utilities
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
# This encoding is similar to standard base45, but with some problematic
# characters (' ', '$', '%', '*') excluded.
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:"
def decodeBase41(data: str) -> bytearray:
mapped: map[int] = map(_BASE41_CHARSET.index, data)
2023-05-30 18:08:52 +02:00
output: bytearray = bytearray()
for a, b, c in zip(mapped, mapped, mapped):
2023-06-07 21:39:02 +02:00
value: int = a + (b * 41) + (c * 1681)
2023-05-30 18:08:52 +02:00
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}"
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):
2023-06-07 21:39:02 +02:00
hexBytes: map[str] = map(lambda value: f"{value:02x}", data[i:i + width])
hexLine: str = " ".join(hexBytes).ljust(width * 3 - 1)
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
asciiBytes: map[str] = map(toPrintableChar, data[i:i + width])
asciiLine: str = "".join(asciiBytes).ljust(width)
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
output.write(f" {i:04x}: {hexLine} |{asciiLine}|\n")
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
## Dump parser
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
_DUMP_START: str = "573::"
_DUMP_END: str = "::"
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
_CHIP_NAMES: Mapping[ChipType, str] = {
ChipType.NONE: "None",
ChipType.X76F041: "Xicor X76F041",
ChipType.X76F100: "Xicor X76F100",
ChipType.ZS01: "Konami ZS01 (PIC16CE625)"
}
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
def parseDumpString(data: str) -> Dump:
_data: str = data.strip().upper()
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
if not _data.startswith(_DUMP_START) or not _data.endswith(_DUMP_END):
raise ValueError(f"dump string does not begin with '{_DUMP_START}' and end with '{_DUMP_END}'")
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
_data = _data[len(_DUMP_START):-len(_DUMP_END)]
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
return parseDump(decompress(decodeBase41(_data)))
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
def printDumpInfo(dump: Dump, output: TextIO):
if dump.flags & DumpFlag.DUMP_SYSTEM_ID_OK:
output.write(f"Digital I/O ID: {dump.systemID.hex('-')}\n")
output.write(f"Serial number: {serialNumberToString(dump.systemID)}\n")
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
output.write(f"Cartridge type: {_CHIP_NAMES[dump.chipType]}\n")
if dump.flags & DumpFlag.DUMP_CART_ID_OK:
output.write(f"DS2401 identifier: {dump.cartID.hex('-')}\n")
if dump.flags & DumpFlag.DUMP_ZS_ID_OK:
output.write(f"ZS01 identifier: {dump.zsID.hex('-')}\n")
if dump.flags & DumpFlag.DUMP_CONFIG_OK:
output.write(f"Configuration: {dump.config.hex('-')}\n")
2023-05-30 18:08:52 +02:00
2023-06-07 21:39:02 +02:00
output.write("\nEEPROM dump:\n")
hexdump(dump.data, output)
output.write("\n")
2023-05-30 18:08:52 +02:00
## 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"
)
2023-06-07 21:39:02 +02:00
#group.add_argument(
#"-e", "--export",
#type = FileType("wb"),
#help = "export dump in MAME format to specified file",
#metavar = "file"
#)
2023-05-30 18:08:52 +02:00
group.add_argument(
2023-06-07 21:39:02 +02:00
"data",
2023-05-30 18:08:52 +02:00
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:
2023-06-07 21:39:02 +02:00
data: str = _file.read()
elif args.data:
data: str = args.data
2023-05-30 18:08:52 +02:00
else:
parser.error("a dump must be passed on the command line or using -i")
2023-06-07 21:39:02 +02:00
dump: Dump = parseDumpString(data)
if args.log:
printDumpInfo(dump, args.log)
2023-05-30 18:08:52 +02:00
if __name__ == "__main__":
main()