#!/usr/bin/env python3 # -*- coding: utf-8 -*- __version__ = "0.3.5" __author__ = "spicyjpeg" import sys from argparse import ArgumentParser, FileType, Namespace from typing import ByteString, Mapping, Sequence, TextIO from zlib import decompress from _common import * ## Utilities # 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) output: bytearray = bytearray() for a, b, c in zip(mapped, mapped, mapped): value: int = a + (b * 41) + (c * 1681) 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): hexBytes: map[str] = map(lambda value: f"{value:02x}", data[i:i + width]) hexLine: str = " ".join(hexBytes).ljust(width * 3 - 1) asciiBytes: map[str] = map(toPrintableChar, data[i:i + width]) asciiLine: str = "".join(asciiBytes).ljust(width) output.write(f" {i:04x}: {hexLine} |{asciiLine}|\n") ## Dump parser _DUMP_START: str = "573::" _DUMP_END: str = "::" _CHIP_NAMES: Mapping[ChipType, str] = { ChipType.NONE: "None", ChipType.X76F041: "Xicor X76F041", ChipType.X76F100: "Xicor X76F100", ChipType.ZS01: "Konami ZS01 (PIC16CE625)" } def parseDumpString(data: str) -> Dump: _data: str = data.strip().upper() 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}'") _data = _data[len(_DUMP_START):-len(_DUMP_END)] return parseDump(decompress(decodeBase41(_data))) def printDumpInfo(dump: Dump, output: TextIO): if dump.flags & DumpFlag.DUMP_SYSTEM_ID_OK: output.write("Digital I/O board:\n") output.write(f" DS2401 ID: {dump.systemID.hex('-')}\n") output.write(f" Serial number: {serialNumberToString(dump.systemID)}\n\n") output.write("Security cartridge:\n") output.write(f" Chip type: {_CHIP_NAMES[dump.chipType]}\n") if dump.flags & DumpFlag.DUMP_CART_ID_OK: output.write(f" DS2401 ID: {dump.cartID.hex('-')}\n") if dump.flags & DumpFlag.DUMP_ZS_ID_OK: output.write(f" ZS01 ID: {dump.zsID.hex('-')}\n") if dump.flags & DumpFlag.DUMP_CONFIG_OK: output.write(f" Configuration: {dump.config.hex('-')}\n") output.write("\nEEPROM dump:\n") hexdump(dump.data, output) output.write("\n") ## 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("rb"), help = "Read dump (.573 file) or QR string from specified path", 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 binary dump (.573 file) to specified path", metavar = "file" ) group = parser.add_argument_group("Input data") group.add_argument( "data", 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: data: bytes = _file.read() try: dump: Dump = parseDump(data) except: dump: Dump = parseDumpString(data.decode("ascii")) elif args.data: dump: Dump = parseDumpString(args.data) else: parser.error("a dump must be passed on the command line or using -i") if args.log: printDumpInfo(dump, args.log) if args.export: with args.export as _file: _file.write(dump.serialize()) if __name__ == "__main__": main()