diff --git a/README.md b/README.md index e999932..c85021e 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,6 @@ original build process. ROM images predating before the "SuperMario" ROM (Quadra 660AV/840AV) are not supported, excluding most 68k Mac ROMs. -The resource fork of a NewWorld ROM image is ignored, despite -containting a System Enabler that is paired with the data fork contents. -Simply copying the resource fork back will cause a crash, because the -'cfrg' resource contains offests to some PowerPC binaries at the end of -the data fork. - The `tbxi dump` format is likely to change. If you keep a collection of dumped ROM images to peruse, re-dump them regularly. diff --git a/setup.py b/setup.py index 9c49484..5014cb4 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ setup_args = dict( ], zip_safe=True, packages=['tbxi'], + install_requires=['macresources'], entry_points=dict(console_scripts=['tbxi = tbxi.__main__:main']), ext_modules=[Extension('tbxi.fast_lzss', ['speedups/fast_lzss.c'])], ) diff --git a/tbxi/__main__.py b/tbxi/__main__.py index 1fe2bac..a139afb 100644 --- a/tbxi/__main__.py +++ b/tbxi/__main__.py @@ -6,6 +6,7 @@ import sys import os from os import path import shutil +import macresources from .slow_lzss import decompress @@ -56,7 +57,34 @@ def main(args=None): except FileNotFoundError: pass - dispatcher.dump(f.read(), args.output, toplevel=True) + base, ext = path.splitext(args.file) + if ext.lower() == '.hqx': + import binhex + hb = binhex.HexBin(f) + data = hb.read() + rsrc = list(macresources.parse_file(hb.read_rsrc())) + + else: + data = f.read() + rsrc = [] + + if not rsrc: + try: + with open(args.file + '.rdump', 'rb') as f: + rsrc = list(macresources.parse_rez_code(f.read())) + except FileNotFoundError: + pass + + if not rsrc: + try: + with open(args.file + '/..namedfork/rsrc', 'rb') as f: + rsrc = list(macresources.parse_file(f.read())) + except FileNotFoundError: + pass + + tpl = (data, rsrc) + + dispatcher.dump(tpl, args.output, toplevel=True) elif command == 'build': parser.add_argument('dir', metavar='', help='source directory') @@ -67,7 +95,8 @@ def main(args=None): data = dispatcher.build(args.dir) - if data.startswith(b''): + if isinstance(data, tuple): + data, rsrc = data # unpack the resource list from the data fork base, ext = path.splitext(args.output) if ext.lower() == '.hqx': import binhex @@ -77,14 +106,29 @@ def main(args=None): finfo.Type = b'tbxi' finfo.Flags = 0 - bh = binhex.BinHex(('Mac OS ROM', finfo, len(data), 0), args.output) + # Special-casing for no-resource-fork + rsrc = macresources.make_file(rsrc) if rsrc else b'' + + bh = binhex.BinHex(('Mac OS ROM', finfo, len(data), len(rsrc)), args.output) bh.write(data) - bh.write_rsrc(b'') + bh.write_rsrc(rsrc) bh.close() return # do not write the usual way else: + rsrc = macresources.make_rez_code(rsrc, ascii_clean=True) + + # Special-casing for no-resource-fork + if rsrc: + with open(args.output + '.rdump', 'wb') as f: + f.write(rsrc) + else: + try: + os.remove(args.output + '.rdump') + except FileNotFoundError: + pass + with open(args.output + '.idump', 'wb') as f: f.write(b'tbxichrp') diff --git a/tbxi/bootinfo_build.py b/tbxi/bootinfo_build.py index 25a84ba..c32410c 100644 --- a/tbxi/bootinfo_build.py +++ b/tbxi/bootinfo_build.py @@ -2,6 +2,7 @@ from os import path import re import zlib import sys +import macresources try: from .fast_lzss import compress @@ -9,6 +10,7 @@ except ImportError: from .slow_lzss import compress from . import dispatcher +from . import cfrg_rsrc def append_checksum(binary): @@ -100,4 +102,22 @@ def build(src): if has_checksum: append_checksum(booter) - return bytes(booter) + # Add a System Enabler (or even just 'vers' information) + rsrcfork = [] + try: + datafork = open(path.join(src, 'SysEnabler'), 'rb').read() + rsrcfork = list(macresources.parse_rez_code(open(path.join(src, 'SysEnabler.rdump'), 'rb').read())) + + while len(booter) % 16: booter.append(0) + delta = len(booter) + booter.extend(datafork) + if len(datafork) > 0 and has_checksum: append_checksum(booter) + + for r in rsrcfork: + if r.type == b'cfrg': + r.data = cfrg_rsrc.adjust_dfrkoffset_fields(r.data, delta) + + except FileNotFoundError: + pass + + return bytes(booter), rsrcfork diff --git a/tbxi/bootinfo_dump.py b/tbxi/bootinfo_dump.py index 8a79f5c..1ab9a0e 100644 --- a/tbxi/bootinfo_dump.py +++ b/tbxi/bootinfo_dump.py @@ -1,13 +1,19 @@ import os from os import path import re +import sys +import macresources from .slow_lzss import decompress from . import dispatcher +from . import cfrg_rsrc +# Special case: expects a (data, resource_list) tuple def dump(binary, dest_dir): + if not isinstance(binary, tuple): raise dispatcher.WrongFormat + binary, rsrc = binary if not binary.startswith(b''): raise dispatcher.WrongFormat os.makedirs(dest_dir, exist_ok=True) @@ -48,3 +54,24 @@ def dump(binary, dest_dir): parcels = decompress(parcels) dispatcher.dump(parcels, path.join(dest_dir, filename)) + + # Lastly, dump the System Enabler (if present and rsrc fork not stripped) + if rsrc: + cfrgs = [r for r in rsrc if r.type == b'cfrg'] + + start, stop = cfrg_rsrc.get_dfrk_range([c.data for c in cfrgs], len(binary)) + + for c in cfrgs: + c.data = cfrg_rsrc.adjust_dfrkoffset_fields(c.data, -start) + + with open(path.join(dest_dir, 'SysEnabler'), 'wb') as f: + f.write(binary[start:stop]) + + with open(path.join(dest_dir, 'SysEnabler.rdump'), 'wb') as f: + f.write(macresources.make_rez_code(rsrc, ascii_clean=True)) + + with open(path.join(dest_dir, 'SysEnabler.idump'), 'wb') as f: + f.write(b'gblyMACS') + + elif b'Joy!' in binary[other_offset+other_size:]: + print('Resource fork missing, ignoring orphaned data fork PEFs', file=sys.stderr) diff --git a/tbxi/cfrg_rsrc.py b/tbxi/cfrg_rsrc.py new file mode 100644 index 0000000..b426a77 --- /dev/null +++ b/tbxi/cfrg_rsrc.py @@ -0,0 +1,60 @@ +# Routines to fiddle with 'cfrg' (Code Fragment) metadata resources + +# These are required only because the code fragments +# referenced by a cfrg are usually at specific offsets +# in a data fork, rather than in a resource. When we +# manipulate a data fork, any corresponding cfrg must be +# adjusted accordingly. This un-Mac-like scheme originated +# as a way to allow code fragments to be memory-mapped, +# which is impossible in a frequently-repacked resource fork. + + +import struct + + +# Internal: where are the fields that must be edited? +def get_dfrkoffset_field_positions(cfrg): + # old-style cfrg only, seems to work fine... + entry_cnt, = struct.unpack_from('>L', cfrg, 28) + + ctr = 32 + + for i in range(entry_cnt): + if len(cfrg) < ctr + 43: break + + if cfrg[ctr + 23] == 1: # kDataForkCFragLocator + yield ctr + 24 + + ctr += 42 + 1 + cfrg[ctr + 42] + while ctr % 4: ctr += 1 + + +# Tell this resource that you moved the PEFs that it references in the data fork +def adjust_dfrkoffset_fields(cfrg, delta): + cfrg = bytearray(cfrg) + + for field in get_dfrkoffset_field_positions(cfrg): + ofs, = struct.unpack_from('>L', cfrg, field) + ofs += delta + struct.pack_into('>L', cfrg, field, ofs) + + return bytes(cfrg) + + +# Get the (start, stop) offset range of PEFs in the data fork +def get_dfrk_range(cfrg_list, dfrk_len): + left = dfrk_len + right = 0 + + for cfrg in cfrg_list: + for field in get_dfrkoffset_field_positions(cfrg): + my_left, = struct.unpack_from('>L', cfrg, field) + left = min(my_left, left) + + my_right, = struct.unpack_from('>L', cfrg, field + 4) + if my_right == 0: + right = dfrk_len + else: + right = max(right, my_left + my_right) + + return left, right diff --git a/tbxi/dispatcher.py b/tbxi/dispatcher.py index 09e9ca6..b28d2dc 100644 --- a/tbxi/dispatcher.py +++ b/tbxi/dispatcher.py @@ -60,6 +60,8 @@ def dump(binary, dest_path, toplevel=False): for fmt in FORMATS: mod = importlib.import_module('..%s_dump' % fmt, __name__) try: + arg = binary + if isinstance(arg, tuple) and fmt != 'bootinfo': arg = arg[0] # strip found resource fork mod.dump(binary, dest_path) print(fmt) break