diff --git a/tbxi/namedtuplestruct.py b/tbxi/namedtuplestruct.py index 9b1991d..ebb7d3b 100644 --- a/tbxi/namedtuplestruct.py +++ b/tbxi/namedtuplestruct.py @@ -6,6 +6,7 @@ class NamedTupleStruct(struct.Struct): def __init__(self, *args, name=None, fields=None, **kwargs): self.__namedtuple = namedtuple(name, fields) + self._fields = self.__namedtuple._fields super().__init__(*args, **kwargs) def __tuplify(self, *args, **kwargs): diff --git a/tbxi/powerpc_build.py b/tbxi/powerpc_build.py index 92c5275..8fe4c52 100644 --- a/tbxi/powerpc_build.py +++ b/tbxi/powerpc_build.py @@ -1,5 +1,269 @@ +from os import path +import shlex +import ast +import re +import struct +import glob + +from .lowlevel import ConfigInfo + from . import dispatcher +MAPNAMES = ['sup', 'usr', 'cpu', 'ovl'] +BATNAMES = ['ibat0', 'ibat1', 'ibat2', 'ibat3', 'dbat0', 'dbat1', 'dbat2', 'dbat3'] + + +class CodeLine(dict): + def __getattr__(self, attrname): + return self[attrname] + + def __setattr__(self, attrname, attrval): + self[attrname] = attrval + + +def iter_configinfo_names(): + yield 'Configfile' + + n = 1 + while True: + yield 'Configfile-%d' % n + n += 1 + + +def is_safe(expr): + chexpr = re.sub(r'\b(0x[0-9a-f]+|0b[01]+|\b[1-9][0-9]*|\b0\b)', '', expr.lower()) + for char in chexpr: + if char not in '+-|&()': + return False + + return True + + +def sub_constants(expr): + K = dict(PMDT_InvalidAddress=0xA00, PMDT_Available=0xA01) + + for k, v in K.items(): + expr = re.sub(r'\b%s\b' % k, '(%s)' % hex(v), expr) + + return expr + + +def parse_configinfo(src_path): + linelist = [] + chunks = {'': linelist} # must sort as first + + with open(src_path) as f: + for line in f: + words = shlex.split(line, comments=True, posix=True) + if len(words) == 0: continue + + if len(words) == 1 and words[0].startswith('[') and words[0].endswith(']'): + linelist = [] + chunks[words[0][1:-1]] = linelist + continue + + worddict = CodeLine() + linelist.append(worddict) + for word in words: + k, sep, v = word.partition('=') + if sep: worddict[k] = v + + # do some cleanup: replace all instances of BASE with ROMImageBaseOffset + base = '-0x30C000' # bad fallback, don't skip ROMImageBaseOffset + lines = chunks[''] + for words in lines: + for k, v in words.items(): + if k == 'ROMImageBaseOffset': + base = v + + base = '(%s)' % base + + for header, lines in chunks.items(): + for words in lines: + for k, v in list(words.items()): + if k == 'BootstrapVersion': + words[k] = v.encode('mac_roman') + continue + + if k == 'brpn': + v2 = re.sub(r'\bBASE\b', base, v) + is_relative = (v != v2) + if is_safe(v2): + words[k] = is_relative, eval(v2) + continue + + v2 = re.sub(r'\bBASE\b', base, v) + v2 = sub_constants(v2) + if is_safe(v2): + words[k] = eval(v2) + + return chunks + + +def insert_and_assert(binary, insertee, offset): + existing = binary[offset:offset+len(insertee)] + if existing != insertee and any(existing): + raise ValueError('inserting over something else') + + binary[offset:offset+len(insertee)] = insertee + + +def checksum_image(binary, ofs): + # ugly, but iterating the right was is painfully slow + + byte_lanes = [sum(binary[i::8]) for i in range(8)] + + zeroed_byte_lanes = list(byte_lanes) + for j in range(ofs, ofs+40): + zeroed_byte_lanes[j % 8] -= binary[j] + + sum32 = [lane % (1<<32) for lane in zeroed_byte_lanes] + + sum64 = sum(lane << (k * 8) for (k, lane) in enumerate(reversed(zeroed_byte_lanes))) + sum64 %= 1 << 64 + + allsums = b''.join(x.to_bytes(4, byteorder='big') for x in sum32) + allsums += sum64.to_bytes(8, byteorder='big') + + return allsums + + def build(src): - raise dispatcher.WrongFormat + if not path.exists(path.join(src, 'Configfile')) or path.exists(path.join(src, 'Configfile-1')): raise dispatcher.WrongFormat + + cilist = [] + for ciname in iter_configinfo_names(): + try: + cilist.append(parse_configinfo(path.join(src, ciname))) + except (FileNotFoundError, NotADirectoryError): + break + + if len(cilist) == 0: raise dispatcher.WrongFormat + + # This will typically contain the emulator, which I can't reliably extract + try: + with open(path.join(src, 'EverythingElse'), 'rb') as f: + rom = bytearray(f.read()) + except FileNotFoundError: + rom = bytearray(0x400000) + + # Now we go through every configinfo and insert it (oh hell) + for ci in reversed(cilist): + fields = {key: 0 for key in ConfigInfo._fields} + lowmem = bytearray() + pagemap = bytearray() + segptrs = [bytearray(128) for _ in MAPNAMES] + batmap = bytearray() # will be padded to 128 + batptrs = [0 for _ in MAPNAMES] + + for header, lines in ci.items(): + if header == '': + for words in lines: + for k, v in words.items(): + if k in fields: + fields[k] = v + + elif header == 'LowMemory': + for words in lines: + lowmem.extend(struct.pack('>LL', words.address, words.value)) + + elif header == 'PageMappingInfo': + for words in lines: + if 'segment_ptr_here' in words: + mapidx = MAPNAMES.index(words.map.lower()) + struct.pack_into('>LL', segptrs[mapidx], 8 * words.segment_ptr_here, len(pagemap), words.segment_register) + + elif 'special_pmdt' in words: + key = 'PageMap%sOffset' % words.special_pmdt.upper() + fields[key] = len(pagemap) + + elif 'pmdt_page_offset' in words: + long2 = words.phys_page << 12 | words.attr + pagemap.extend(struct.pack('>HHL', words.pmdt_page_offset, words.pages_minus_1, long2)) + + elif header == 'BatMappingInfo': + for words in lines: + if 'bat_ptr_here' in words: + batidx = BATNAMES.index(words.bat_ptr_here.lower()) + mapidx = MAPNAMES.index(words.map.lower()) + fourbits = len(batmap) // 8 + shift = 4 * (7 - batidx) + batptrs[mapidx] |= fourbits << shift + + elif 'bepi' in words: + ubat = lbat = 0 + + ubat |= words.bepi & 0xFFFE0000 # trailing zeroes hopefully + ubat |= words.bl_128k << 2 + ubat |= words.vs << 1 + ubat |= words.vp + + lbat |= words.unk23 << 8 + lbat |= words.wim << 4 + lbat |= words.ks << 3 + lbat |= words.ku << 2 + lbat |= words.pp + + is_relative, brpn = words.brpn # special case in parse_configinfo + if is_relative: lbat |= 0x200 + lbat = (lbat + brpn) & 0xFFFFFFFF + + batmap.extend(struct.pack('>LL', ubat, lbat)) + + # Get the awkward array data into the struct + fields['SegMap32SupInit'] = segptrs[0] + fields['SegMap32UsrInit'] = segptrs[1] + fields['SegMap32CPUInit'] = segptrs[2] + fields['SegMap32OvlInit'] = segptrs[3] + fields['BatMap32SupInit'] = batptrs[0] + fields['BatMap32UsrInit'] = batptrs[1] + fields['BatMap32CPUInit'] = batptrs[2] + fields['BatMap32OvlInit'] = batptrs[3] + fields['BATRangeInit'] = batmap + + # Great, now we'll neaten up fields and blat it out + lowmem.extend(b'\0\0\0\0') + + flat = bytearray(0x1000) + ptr = len(flat) + + ptr -= len(lowmem) + insert_and_assert(flat, lowmem, ptr) + fields['MacLowMemInitOffset'] = ptr + + ptr -= len(pagemap) + insert_and_assert(flat, pagemap, ptr) + fields['PageMapInitOffset'] = ptr + fields['PageMapInitSize'] = len(pagemap) + + insert_and_assert(flat, ConfigInfo.pack(**fields), 0) + + # Insert the ConfigInfo struct! + configinfo_offset = -fields['ROMImageBaseOffset'] # this var used below + insert_and_assert(rom, flat, configinfo_offset) + + # Now insert other things as directed by the struct + described = [ + ('Mac68KROM', 'Mac68KROM'), + ('ExceptionTable', 'ExceptionTable'), + ('HWInitCode', 'HWInit'), + ('KernelCode', 'NanoKernel'), + ('OpenFWBundle', 'OpenFW'), + ] + + for basename, filename in described: + blob_offset = fields[basename + 'Offset'] + if blob_offset == 0: continue + + matches = glob.glob(glob.escape(path.join(src, filename)) + '*') + if matches: + match = min(matches) # try for * before *.src + blob = dispatcher.build_path(match) + insert_and_assert(rom, blob, configinfo_offset + blob_offset) + + # let's do a cheeky checksum! + cksum = checksum_image(rom, configinfo_offset) + insert_and_assert(rom, cksum, configinfo_offset) # overwrites start of ConfigInfo + + return bytes(rom) diff --git a/tbxi/powerpc_dump.py b/tbxi/powerpc_dump.py index 7451716..dbf041f 100644 --- a/tbxi/powerpc_dump.py +++ b/tbxi/powerpc_dump.py @@ -1,7 +1,7 @@ from .lowlevel import SuperMarioHeader, ConfigInfo import struct - +import shlex import os from os import path @@ -22,7 +22,7 @@ Mac68KROMSize= # [38] Number of bytes in Macintosh 68K ROM ExceptionTableOffset= # [3C] Offset of base of PowerPC Exception Table Code ExceptionTableSize= # [40] Number of bytes in PowerPC Exception Table Code -HWInitCodeOffset= # Offset of base of Hardware Init Code +HWInitCodeOffset= # [44] Offset of base of Hardware Init Code HWInitCodeSize= # [48] Number of bytes in Hardware Init Code KernelCodeOffset= # [4C] Offset of base of NanoKernel Code @@ -124,7 +124,7 @@ def dump_configinfo(binary, offset, push_line): if key == 'InterruptHandlerKind': value = '0x%02X' % raw_value elif key == 'BootstrapVersion': - value = repr(raw_value)[1:] + value = shlex.quote(raw_value.decode('mac_roman')) elif key.endswith('Offset') and key.startswith(('Mac68KROM', 'ExceptionTable', 'HWInitCode', 'KernelCode', 'EmulatorCode', 'OpcodeTable', 'OpenFWBundle')): if getattr(s, key.replace('Offset', 'Size')) == 0: value = '0x00000000' @@ -201,6 +201,7 @@ def dump_configinfo(binary, offset, push_line): attr_s = 'PMDT_InvalidAddress' elif attr == 0xA01: attr_s = 'PMDT_Available' + # elif attr & else: attr_s = '0x%03X' % attr @@ -214,7 +215,7 @@ def dump_configinfo(binary, offset, push_line): if i == s.PageMapKDPOffset: push_line('special_pmdt=kdp') if i == s.PageMapEDPOffset: push_line('special_pmdt=edp') - push_line('pmdt_page_offset=0x%04X pages_minus_1=0x%04X phys_page=%s attr=%s' % (pgidx, pgcnt, paddr_s, attr_s)) + push_line('\tpmdt_page_offset=0x%04X pages_minus_1=0x%04X phys_page=%s attr=%s' % (pgidx, pgcnt, paddr_s, attr_s)) push_line('') @@ -237,17 +238,22 @@ def dump_configinfo(binary, offset, push_line): vp = u & 1 brpn = l >> 17 - wimg = [(l > 6) & 1, (l > 5) & 1, (l > 4) & 1, (l > 3) & 1] - pp = [(l > 1) & 1, l & 1] + unk23 = (l >> 8) & 1 + wim = (l >> 4) & 0x7 + ks = (l >> 3) & 1 + ku = (l >> 2) & 1 + pp = l & 0x3 - bl_s = '0b' + bin(bl)[2:].zfill(11) + bl_s = '0b' + bin(bl)[2:].zfill(6) + wim_s = bin(wim)[2:].zfill(3) + pp_s = bin(pp)[2:].zfill(2) if is_relative: brpn_s = 'BASE+0x%06X' % (brpn << 17) else: brpn_s = '0x%08X' % (brpn << 17) - push_line('bepi=0x%08X bl=%s vs=%s vp=%d brpn=%s wimg=0b%d%d%d%d pp=0b%d%d' % (bepi << 17, bl_s, vs, vp, brpn_s, *wimg, *pp)) + push_line('\tbepi=0x%08X bl_128k=%s vs=%d vp=%d brpn=%s unk23=%d wim=0b%s ks=%d ku=%d pp=0b%s' % (bepi << 17, bl_s, vs, vp, brpn_s, unk23, wim_s, ks, ku, pp_s)) push_line('')