Track the 410console hand-off docs; ignore the raw Mac archive
The BattleTech port spec, the Console 4.10 decompilation notes, the golden reference eggs, and the PEF/rsrc analysis tools come into the repo. The .sit archive and its extraction (Mac binaries, __MACOSX/.finf metadata) stay untracked for now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pefparse.py -- Minimal parser/disassembler for classic Mac OS CFM
|
||||
Preferred Executable Format (PEF) PowerPC containers ('Joy!peff pwpc').
|
||||
|
||||
Written for the VWE Tesla restoration project to decompile "Console 4.10".
|
||||
Big-endian PowerPC. Pure-python; only external dep is capstone (optional,
|
||||
for the `dis` command).
|
||||
|
||||
PEF reference: Apple "Mac OS Runtime Architectures", ch. Preferred
|
||||
Executable Format. Container header (40 bytes), then N section headers
|
||||
(28 bytes each), then the section name table, then section bodies.
|
||||
|
||||
Usage:
|
||||
py -3.13 pefparse.py <file> header # container + section headers
|
||||
py -3.13 pefparse.py <file> loader # loader section: imports/exports/entry
|
||||
py -3.13 pefparse.py <file> strings # printable strings in code+data
|
||||
py -3.13 pefparse.py <file> dis [secidx] [start] [count] # disassemble
|
||||
py -3.13 pefparse.py <file> dump <secidx> <off> <len> # hex dump section body
|
||||
"""
|
||||
import sys, struct, string
|
||||
|
||||
def u32(b, o): return struct.unpack_from('>I', b, o)[0]
|
||||
def u16(b, o): return struct.unpack_from('>H', b, o)[0]
|
||||
def s32(b, o): return struct.unpack_from('>i', b, o)[0]
|
||||
def u8(b, o): return b[o]
|
||||
|
||||
SECTION_KINDS = {
|
||||
0: 'Code', 1: 'UnpackedData', 2: 'PatternInitData', 3: 'Constant',
|
||||
4: 'Loader', 5: 'Debug', 6: 'ExecutableData', 7: 'Exception', 8: 'Traceback',
|
||||
}
|
||||
|
||||
class Section:
|
||||
def __init__(self, idx, hdr, container):
|
||||
(self.name_off, self.default_addr, self.total_size, self.unpacked_size,
|
||||
self.packed_size, self.container_off, self.kind, self.share,
|
||||
self.align, self.rsvd) = hdr
|
||||
self.idx = idx
|
||||
self.name = None
|
||||
self._container = container
|
||||
|
||||
def raw(self):
|
||||
"""Raw (possibly packed) bytes of the section body as stored."""
|
||||
return self._container[self.container_off:self.container_off + self.packed_size]
|
||||
|
||||
def data(self):
|
||||
"""Unpacked section body. PatternInitData(2) is RLE-decompressed."""
|
||||
raw = self.raw()
|
||||
if self.kind == 2:
|
||||
return unpack_pidata(raw, self.unpacked_size)
|
||||
return raw
|
||||
|
||||
class PEF:
|
||||
def __init__(self, blob):
|
||||
self.blob = blob
|
||||
assert blob[0:4] == b'Joy!', "not a PEF (missing Joy!)"
|
||||
assert blob[4:8] == b'peff', "not a PEF (missing peff)"
|
||||
self.arch = blob[8:12]
|
||||
self.format_version = u32(blob, 12)
|
||||
self.timestamp = u32(blob, 16)
|
||||
self.old_def_ver = u32(blob, 20)
|
||||
self.old_imp_ver = u32(blob, 24)
|
||||
self.current_ver = u32(blob, 28)
|
||||
self.section_count = u16(blob, 32)
|
||||
self.inst_section_count = u16(blob, 34)
|
||||
self.sections = []
|
||||
o = 40
|
||||
for i in range(self.section_count):
|
||||
hdr = struct.unpack_from('>iIIIIIBBBB', blob, o)
|
||||
self.sections.append(Section(i, hdr, blob))
|
||||
o += 28
|
||||
# section name table follows the section headers
|
||||
name_tbl_off = o
|
||||
for s in self.sections:
|
||||
if s.name_off >= 0:
|
||||
s.name = read_cstr(blob, name_tbl_off + s.name_off)
|
||||
|
||||
def loader(self):
|
||||
for s in self.sections:
|
||||
if s.kind == 4:
|
||||
return s
|
||||
return None
|
||||
|
||||
def read_cstr(b, o):
|
||||
e = o
|
||||
while e < len(b) and b[e] != 0:
|
||||
e += 1
|
||||
return b[o:e].decode('mac_roman', 'replace')
|
||||
|
||||
def unpack_pidata(raw, unpacked_size):
|
||||
"""Decompress PEF pattern-initialized data (RLE opcode stream)."""
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(raw)
|
||||
def argcount():
|
||||
nonlocal i
|
||||
val = 0
|
||||
while True:
|
||||
b = raw[i]; i += 1
|
||||
val = (val << 7) | (b & 0x7f)
|
||||
if not (b & 0x80):
|
||||
break
|
||||
return val
|
||||
while i < n:
|
||||
op = raw[i]; i += 1
|
||||
opcode = op >> 5
|
||||
count5 = op & 0x1f
|
||||
cnt = count5 if count5 else argcount()
|
||||
if opcode == 0: # Zero
|
||||
out.extend(b'\x00' * cnt)
|
||||
elif opcode == 1: # blockCopy
|
||||
out.extend(raw[i:i+cnt]); i += cnt
|
||||
elif opcode == 2: # repeatedBlock
|
||||
rpt = argcount()
|
||||
block = raw[i:i+cnt]; i += cnt
|
||||
for _ in range(rpt + 1):
|
||||
out.extend(block)
|
||||
elif opcode == 3: # interleaveRepeatBlockWithBlockCopy
|
||||
cs = argcount() # custom size
|
||||
rc = argcount() # repeat count
|
||||
common = raw[i:i+cnt]; i += cnt
|
||||
for _ in range(rc):
|
||||
out.extend(common)
|
||||
out.extend(raw[i:i+cs]); i += cs
|
||||
out.extend(common)
|
||||
elif opcode == 4: # interleaveRepeatBlockWithZero
|
||||
cs = argcount()
|
||||
rc = argcount()
|
||||
for _ in range(rc):
|
||||
out.extend(b'\x00' * cnt)
|
||||
out.extend(raw[i:i+cs]); i += cs
|
||||
out.extend(b'\x00' * cnt)
|
||||
else:
|
||||
raise ValueError("bad PIDATA opcode %d at %d" % (opcode, i))
|
||||
return bytes(out[:unpacked_size])
|
||||
|
||||
# ---- Loader section parsing -------------------------------------------------
|
||||
|
||||
def parse_loader(sec):
|
||||
b = sec.data()
|
||||
(main_section, main_offset, init_section, init_offset,
|
||||
term_section, term_offset, imported_lib_count, total_imported_sym_count,
|
||||
reloc_section_count, reloc_instr_offset, loader_strings_offset,
|
||||
export_hash_offset, export_hash_table_power, exported_symbol_count) = \
|
||||
struct.unpack_from('>iIiIiIIIIIIIII', b, 0)
|
||||
info = dict(main_section=main_section, main_offset=main_offset,
|
||||
init_section=init_section, init_offset=init_offset,
|
||||
term_section=term_section, term_offset=term_offset,
|
||||
imported_lib_count=imported_lib_count,
|
||||
total_imported_sym_count=total_imported_sym_count,
|
||||
reloc_section_count=reloc_section_count,
|
||||
reloc_instr_offset=reloc_instr_offset,
|
||||
loader_strings_offset=loader_strings_offset,
|
||||
export_hash_offset=export_hash_offset,
|
||||
export_hash_table_power=export_hash_table_power,
|
||||
exported_symbol_count=exported_symbol_count)
|
||||
# Imported library table: starts at offset 56
|
||||
libs = []
|
||||
o = 56
|
||||
for _ in range(imported_lib_count):
|
||||
(name_off, old_impl_ver, curr_ver, imp_sym_count,
|
||||
first_imp_sym, options, rsvdA, rsvdB) = struct.unpack_from('>IIIIIBBH', b, o)
|
||||
libs.append(dict(name_off=name_off, imp_sym_count=imp_sym_count,
|
||||
first_imp_sym=first_imp_sym, options=options))
|
||||
o += 24
|
||||
# Imported symbol table follows lib table
|
||||
imp_syms = []
|
||||
for _ in range(total_imported_sym_count):
|
||||
val = u32(b, o); o += 4
|
||||
sym_class = val >> 24
|
||||
name_off = val & 0x00FFFFFF
|
||||
imp_syms.append((sym_class, name_off))
|
||||
strtab = info['loader_strings_offset']
|
||||
for lib in libs:
|
||||
lib['name'] = read_cstr(b, strtab + lib['name_off'])
|
||||
syms = []
|
||||
for k in range(lib['first_imp_sym'], lib['first_imp_sym'] + lib['imp_sym_count']):
|
||||
cls, noff = imp_syms[k]
|
||||
syms.append((cls, read_cstr(b, strtab + noff)))
|
||||
lib['symbols'] = syms
|
||||
# Exported symbols (loader export hash). Layout:
|
||||
# export hash table (2^power u32 slots) at export_hash_offset
|
||||
# then key table (u32 per exported symbol)
|
||||
# then exported symbol table (10 bytes each: class/name u32, value u32, sect u16)
|
||||
exports = []
|
||||
hash_slots = 1 << export_hash_table_power
|
||||
key_tab_off = export_hash_offset + hash_slots * 4
|
||||
sym_tab_off = key_tab_off + exported_symbol_count * 4
|
||||
for k in range(exported_symbol_count):
|
||||
eo = sym_tab_off + k * 10
|
||||
clsname = u32(b, eo)
|
||||
value = u32(b, eo + 4)
|
||||
sect = u16(b, eo + 8)
|
||||
sym_class = clsname >> 24
|
||||
name_off = clsname & 0x00FFFFFF
|
||||
name = read_cstr(b, strtab + name_off)
|
||||
exports.append(dict(name=name, sym_class=sym_class, value=value, section=sect))
|
||||
return info, libs, exports
|
||||
|
||||
SYM_CLASS = {0: 'code(TVect)', 1: 'data', 2: 'TVector', 3: 'TOC/Glue',
|
||||
4: 'linker/glue', 0x40: 'weak?'}
|
||||
|
||||
# ---- commands ---------------------------------------------------------------
|
||||
|
||||
def cmd_header(pef):
|
||||
print("PEF container: arch=%s formatVersion=%d timestamp=0x%08x currentVer=0x%08x"
|
||||
% (pef.arch.decode('ascii','replace'), pef.format_version, pef.timestamp, pef.current_ver))
|
||||
print("sectionCount=%d instantiatedSectionCount=%d" % (pef.section_count, pef.inst_section_count))
|
||||
print()
|
||||
print("idx name kind default_addr total unpacked packed fileoff align")
|
||||
for s in pef.sections:
|
||||
print("%3d %-14s %-19s 0x%08x %-9d %-9d %-9d 0x%06x %d" % (
|
||||
s.idx, (s.name or '-'), "%d:%s" % (s.kind, SECTION_KINDS.get(s.kind, '?')),
|
||||
s.default_addr, s.total_size, s.unpacked_size, s.packed_size,
|
||||
s.container_off, s.align))
|
||||
|
||||
def cmd_loader(pef):
|
||||
lsec = pef.loader()
|
||||
if not lsec:
|
||||
print("no loader section"); return
|
||||
info, libs, exports = parse_loader(lsec)
|
||||
print("=== Loader header ===")
|
||||
for k, v in info.items():
|
||||
if isinstance(v, int) and v > 9 and k.endswith(('offset','section','count')) is False:
|
||||
print(" %-28s 0x%x (%d)" % (k, v, v))
|
||||
else:
|
||||
print(" %-28s %s" % (k, v))
|
||||
ms = info['main_section']
|
||||
print(" ENTRY main: section %d offset 0x%x" % (info['main_section'], info['main_offset']) if ms>=0 else " (no main)")
|
||||
print(" ENTRY init: section %d offset 0x%x" % (info['init_section'], info['init_offset']))
|
||||
print(" ENTRY term: section %d offset 0x%x" % (info['term_section'], info['term_offset']))
|
||||
print()
|
||||
print("=== Imported libraries (%d) ===" % len(libs))
|
||||
for lib in libs:
|
||||
print("\n-- %s (%d symbols) --" % (lib['name'], lib['imp_sym_count']))
|
||||
for j,(cls,name) in enumerate(lib['symbols']):
|
||||
print(" [%4d] cls=%s %s" % (lib['first_imp_sym']+j, SYM_CLASS.get(cls, hex(cls)), name))
|
||||
print()
|
||||
print("=== Exported symbols (%d) ===" % len(exports))
|
||||
for e in exports:
|
||||
print(" %-40s cls=%s value=0x%x sect=%d" % (e['name'], SYM_CLASS.get(e['sym_class'], hex(e['sym_class'])), e['value'], e['section']))
|
||||
|
||||
def iter_strings(data, minlen=4):
|
||||
cur = bytearray()
|
||||
start = 0
|
||||
printable = set(bytes(string.printable, 'ascii')) - set(b'\t\n\r\x0b\x0c')
|
||||
printable |= set(b' ')
|
||||
for i, ch in enumerate(data):
|
||||
if ch in printable:
|
||||
if not cur:
|
||||
start = i
|
||||
cur.append(ch)
|
||||
else:
|
||||
if len(cur) >= minlen:
|
||||
yield start, cur.decode('ascii', 'replace')
|
||||
cur = bytearray()
|
||||
if len(cur) >= minlen:
|
||||
yield start, cur.decode('ascii', 'replace')
|
||||
|
||||
def cmd_strings(pef, minlen=4):
|
||||
for s in pef.sections:
|
||||
if s.kind in (0,1,2,3):
|
||||
data = s.data()
|
||||
print("### section %d (%s) base=0x%x ###" % (s.idx, SECTION_KINDS.get(s.kind), s.default_addr))
|
||||
for off, st in iter_strings(data, minlen):
|
||||
print("0x%08x %s" % (s.default_addr + off, st))
|
||||
|
||||
def cmd_dump(pef, secidx, off, length):
|
||||
data = pef.sections[secidx].data()
|
||||
chunk = data[off:off+length]
|
||||
for i in range(0, len(chunk), 16):
|
||||
row = chunk[i:i+16]
|
||||
hexs = ' '.join('%02x' % c for c in row)
|
||||
asci = ''.join(chr(c) if 32 <= c < 127 else '.' for c in row)
|
||||
print('%08x %-48s %s' % (off+i, hexs, asci))
|
||||
|
||||
def cmd_dis(pef, secidx=0, start=0, count=200):
|
||||
import capstone
|
||||
sec = pef.sections[secidx]
|
||||
data = sec.data()
|
||||
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||
md.detail = False
|
||||
base = sec.default_addr
|
||||
for insn in md.disasm(data[start:start+count*4], base+start):
|
||||
print("0x%08x %-8s %s" % (insn.address, insn.mnemonic, insn.op_str))
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
cmd = sys.argv[2] if len(sys.argv) > 2 else 'header'
|
||||
with open(path, 'rb') as f:
|
||||
blob = f.read()
|
||||
pef = PEF(blob)
|
||||
if cmd == 'header': cmd_header(pef)
|
||||
elif cmd == 'loader': cmd_loader(pef)
|
||||
elif cmd == 'strings':
|
||||
minlen = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
||||
cmd_strings(pef, minlen)
|
||||
elif cmd == 'dump':
|
||||
cmd_dump(pef, int(sys.argv[3]), int(sys.argv[4],0), int(sys.argv[5],0))
|
||||
elif cmd == 'dis':
|
||||
secidx = int(sys.argv[3]) if len(sys.argv) > 3 else 0
|
||||
start = int(sys.argv[4],0) if len(sys.argv) > 4 else 0
|
||||
count = int(sys.argv[5],0) if len(sys.argv) > 5 else 200
|
||||
cmd_dis(pef, secidx, start, count)
|
||||
else:
|
||||
print("unknown cmd", cmd)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ppcxref.py -- TOC-aware PPC annotator for the Console 4.10 PEF.
|
||||
|
||||
Resolves r2(TOC)-relative loads to data addresses and, when those point at
|
||||
C strings, annotates the disassembly. Also builds a string cross-reference:
|
||||
which code addresses reference which strings.
|
||||
|
||||
r2 (TOC base) = data section offset 0x8000 (from the main transition vector).
|
||||
|
||||
Usage:
|
||||
py -3.13 ppcxref.py annotate <start> <count> # annotated disasm
|
||||
py -3.13 ppcxref.py xref <substring> # find code refs to strings matching substring
|
||||
py -3.13 ppcxref.py toc <hexoff> # what does TOC[-off(r2)] point to
|
||||
"""
|
||||
import sys, struct, capstone
|
||||
sys.path.insert(0, __import__('os').path.dirname(__file__))
|
||||
from pefparse import PEF
|
||||
|
||||
BIN = r"C:\VWE\TeslaRel410\410console\4_10-console-extracted\Console 4.10"
|
||||
TOC_BASE = 0x8000 # r2 value = data section offset
|
||||
|
||||
pef = PEF(open(BIN,'rb').read())
|
||||
CODE = pef.sections[0].data()
|
||||
DATA = pef.sections[1].data()
|
||||
|
||||
def d32(off):
|
||||
if off < 0 or off+4 > len(DATA): return None
|
||||
return struct.unpack_from('>I', DATA, off)[0]
|
||||
|
||||
def cstr_at(off, maxlen=80):
|
||||
if off < 0 or off >= len(DATA): return None
|
||||
e = off
|
||||
while e < len(DATA) and DATA[e] != 0 and (e-off) < maxlen:
|
||||
c = DATA[e]
|
||||
if c < 0x09 or (c > 0x0d and c < 0x20) or c > 0x7e:
|
||||
# allow only printable; stop otherwise
|
||||
if c != 0xc9: # ellipsis
|
||||
break
|
||||
e += 1
|
||||
s = DATA[off:e]
|
||||
try:
|
||||
txt = s.decode('mac_roman')
|
||||
except Exception:
|
||||
return None
|
||||
# require mostly printable and length>=3
|
||||
printable = sum(1 for c in s if 0x20 <= c <= 0x7e)
|
||||
if len(s) >= 3 and printable >= max(3, int(len(s)*0.8)):
|
||||
return txt
|
||||
return None
|
||||
|
||||
def toc_target(disp):
|
||||
"""disp is signed offset from r2. Returns (data_off, pointed_value, string_or_None)."""
|
||||
off = TOC_BASE + disp
|
||||
val = d32(off)
|
||||
s = None
|
||||
if val is not None:
|
||||
s = cstr_at(val)
|
||||
return off, val, s
|
||||
|
||||
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
||||
md.detail = True
|
||||
|
||||
def annotate(start, count):
|
||||
base = 0
|
||||
for insn in md.disasm(CODE[start:start+count*4], base+start):
|
||||
note = ''
|
||||
# detect r2-relative: op_str contains "r2)" with a displacement
|
||||
ops = insn.op_str
|
||||
# capstone PPC prints e.g. "r4, -0x6c9c(r2)" or "r3, r2, -0x6848"
|
||||
disp = None
|
||||
if '(r2)' in ops:
|
||||
# form: rX, disp(r2)
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
d = d[:d.index('(r2)')]
|
||||
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||
except Exception:
|
||||
disp = None
|
||||
if disp is not None:
|
||||
o, val, s = toc_target(disp)
|
||||
if val is not None:
|
||||
note = "; TOC[0x%x]=*0x%x=0x%x" % (o, o, val)
|
||||
if s: note += ' "%s"' % s
|
||||
elif 'r2,' in ops and insn.mnemonic in ('addi','addic','subi'):
|
||||
# form: rX, r2, disp -> address = TOC_BASE+disp (pointer to data directly)
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
disp = int(d, 16) if d.startswith(('0x','-0x')) else int(d)
|
||||
except Exception:
|
||||
disp = None
|
||||
if disp is not None:
|
||||
addr = TOC_BASE + disp
|
||||
s = cstr_at(addr)
|
||||
note = "; &data[0x%x]" % addr
|
||||
if s: note += ' "%s"' % s
|
||||
elif insn.mnemonic == 'lis':
|
||||
# high-half load, often followed by addi to form an absolute data ptr
|
||||
note = ''
|
||||
print("0x%08x %-8s %-28s %s" % (insn.address, insn.mnemonic, insn.op_str, note))
|
||||
|
||||
def build_string_table():
|
||||
"""All C strings in data with >=4 printable chars -> {addr: text}."""
|
||||
tbl = {}
|
||||
i = 0
|
||||
n = len(DATA)
|
||||
while i < n:
|
||||
if 0x20 <= DATA[i] <= 0x7e:
|
||||
j = i
|
||||
while j < n and (0x20 <= DATA[j] <= 0x7e):
|
||||
j += 1
|
||||
if j - i >= 4:
|
||||
tbl[i] = DATA[i:j].decode('ascii','replace')
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
return tbl
|
||||
|
||||
def xref(substr):
|
||||
# find data offsets of strings containing substr
|
||||
strtbl = build_string_table()
|
||||
targets = {a:t for a,t in strtbl.items() if substr.lower() in t.lower()}
|
||||
if not targets:
|
||||
print("no strings match", substr); return
|
||||
print("=== target strings ===")
|
||||
for a,t in sorted(targets.items()):
|
||||
print(" 0x%06x %s" % (a, t))
|
||||
# Build TOC reverse map: which TOC slots hold pointers into these strings
|
||||
# (pointer may target start OR interior). Map value->slot.
|
||||
tocslots = {}
|
||||
for slot in range(0, len(DATA), 4):
|
||||
v = d32(slot)
|
||||
if v in targets:
|
||||
tocslots.setdefault(v, []).append(slot)
|
||||
# Also direct addi r2 references use TOC_BASE+disp == string addr directly
|
||||
print("\n=== code references ===")
|
||||
# Disassemble whole code, look for r2-relative producing an address in targets
|
||||
for insn in md.disasm(CODE, 0):
|
||||
ops = insn.op_str
|
||||
hit = None
|
||||
if '(r2)' in ops:
|
||||
try:
|
||||
d = ops.split(',')[-1].strip(); d = d[:d.index('(r2)')]
|
||||
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||
o = TOC_BASE+disp; v = d32(o)
|
||||
if v in targets: hit = (v, targets[v], 'TOC')
|
||||
except Exception: pass
|
||||
elif 'r2,' in ops and insn.mnemonic in ('addi','subi'):
|
||||
try:
|
||||
d = ops.split(',')[-1].strip()
|
||||
disp = int(d,16) if d.startswith(('0x','-0x')) else int(d)
|
||||
addr = TOC_BASE+disp
|
||||
if addr in targets: hit = (addr, targets[addr], 'direct')
|
||||
except Exception: pass
|
||||
if hit:
|
||||
print(" 0x%08x %-8s %-24s -> %s \"%s\"" % (insn.address, insn.mnemonic, ops, hit[2], hit[1]))
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'annotate':
|
||||
annotate(int(sys.argv[2],0), int(sys.argv[3],0))
|
||||
elif cmd == 'xref':
|
||||
xref(sys.argv[2])
|
||||
elif cmd == 'toc':
|
||||
o,v,s = toc_target(int(sys.argv[2],0))
|
||||
print("TOC off=0x%x val=0x%x str=%s" % (o, v if v else 0, s))
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
rsrcparse.py -- classic Mac OS resource-fork parser.
|
||||
Reads a resource fork (raw, or embedded in an AppleDouble ._ sidecar) and
|
||||
lists resource types/IDs/names. Optionally dumps a given type.
|
||||
|
||||
Usage:
|
||||
py -3.13 rsrcparse.py <._file> [--base 0x52] map
|
||||
py -3.13 rsrcparse.py <._file> [--base 0x52] dump <TYPE> [id]
|
||||
"""
|
||||
import sys, struct
|
||||
|
||||
def u32(b,o): return struct.unpack_from('>I',b,o)[0]
|
||||
def u16(b,o): return struct.unpack_from('>H',b,o)[0]
|
||||
|
||||
def appledouble_rfork_base(blob):
|
||||
# AppleDouble: magic 0x00051607; entries describe forks. Entry id 2 = resource fork.
|
||||
if u32(blob,0) != 0x00051607:
|
||||
return 0, len(blob)
|
||||
n = u16(blob, 24)
|
||||
o = 26
|
||||
for _ in range(n):
|
||||
eid, off, length = struct.unpack_from('>III', blob, o); o += 12
|
||||
if eid == 2:
|
||||
return off, length
|
||||
return 0, len(blob)
|
||||
|
||||
def parse(blob, base):
|
||||
rf = blob[base:]
|
||||
data_off, map_off, data_len, map_len = struct.unpack_from('>IIII', rf, 0)
|
||||
m = map_off
|
||||
type_list_off = u16(rf, m+24) # from map start
|
||||
name_list_off = u16(rf, m+26)
|
||||
tlo = m + type_list_off
|
||||
ntypes = u16(rf, tlo) + 1
|
||||
types = []
|
||||
for i in range(ntypes):
|
||||
eo = tlo + 2 + i*8
|
||||
typ = rf[eo:eo+4].decode('mac_roman','replace')
|
||||
count = u16(rf, eo+4) + 1
|
||||
ref_off = u16(rf, eo+6)
|
||||
refs = []
|
||||
rlo = tlo + ref_off
|
||||
for j in range(count):
|
||||
ro = rlo + j*12
|
||||
rid = struct.unpack_from('>h', rf, ro)[0]
|
||||
noff = struct.unpack_from('>h', rf, ro+2)[0]
|
||||
attr_dataoff = u32(rf, ro+4)
|
||||
attrs = attr_dataoff >> 24
|
||||
dataoff = attr_dataoff & 0x00FFFFFF
|
||||
name = None
|
||||
if noff != -1:
|
||||
nlo = m + name_list_off + noff
|
||||
ln = rf[nlo]
|
||||
name = rf[nlo+1:nlo+1+ln].decode('mac_roman','replace')
|
||||
# resource data: at data_off + dataoff, first 4 bytes = length
|
||||
dstart = data_off + dataoff
|
||||
dlen = u32(rf, dstart)
|
||||
refs.append(dict(id=rid, name=name, attrs=attrs, data_start=base+dstart+4, dlen=dlen))
|
||||
types.append((typ, refs))
|
||||
return types
|
||||
|
||||
def cmd_map(blob, base):
|
||||
types = parse(blob, base)
|
||||
total = sum(len(r) for _,r in types)
|
||||
print("resource types: %d, total resources: %d" % (len(types), total))
|
||||
for typ, refs in sorted(types, key=lambda x:x[0]):
|
||||
ids = ', '.join('%d%s' % (r['id'], ('("%s")'%r['name']) if r['name'] else '') for r in refs[:12])
|
||||
more = '' if len(refs)<=12 else ' ...(%d total)'%len(refs)
|
||||
print(" '%s' x%-4d : %s%s" % (typ, len(refs), ids, more))
|
||||
|
||||
def cmd_dump(blob, base, typ, wantid=None):
|
||||
types = dict(parse(blob, base))
|
||||
if typ not in types:
|
||||
print("no type", typ); return
|
||||
for r in types[typ]:
|
||||
if wantid is not None and r['id'] != wantid: continue
|
||||
data = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||
print("=== '%s' id=%d name=%s len=%d ===" % (typ, r['id'], r['name'], r['dlen']))
|
||||
# hex + ascii
|
||||
for i in range(0, min(len(data), 4096), 16):
|
||||
row = data[i:i+16]
|
||||
hexs=' '.join('%02x'%c for c in row)
|
||||
asci=''.join(chr(c) if 32<=c<127 else '.' for c in row)
|
||||
print(' %04x %-48s %s' % (i, hexs, asci))
|
||||
|
||||
def cmd_names(blob, base, typ):
|
||||
types = dict(parse(blob, base))
|
||||
if typ not in types:
|
||||
print("no type", typ); return
|
||||
for r in types[typ]:
|
||||
print(" id=%-6d %s" % (r['id'], r['name'] or ''))
|
||||
|
||||
def cmd_strn(blob, base, wantid=None):
|
||||
"""Decode STR# (list of Pascal strings)."""
|
||||
types = dict(parse(blob, base))
|
||||
for r in types.get('STR#', []):
|
||||
if wantid is not None and r['id'] != wantid: continue
|
||||
d = blob[r['data_start']:r['data_start']+r['dlen']]
|
||||
n = u16(d,0); o=2; items=[]
|
||||
for _ in range(n):
|
||||
ln=d[o]; o+=1; items.append(d[o:o+ln].decode('mac_roman','replace')); o+=ln
|
||||
print("STR# %d (%s): %s" % (r['id'], r['name'], ' | '.join(items)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
path = args[0]; args = args[1:]
|
||||
base = None
|
||||
if args and args[0] == '--base':
|
||||
base = int(args[1],0); args = args[2:]
|
||||
blob = open(path,'rb').read()
|
||||
if base is None:
|
||||
base, _ = appledouble_rfork_base(blob)
|
||||
cmd = args[0] if args else 'map'
|
||||
if cmd == 'map': cmd_map(blob, base)
|
||||
elif cmd == 'dump': cmd_dump(blob, base, args[1], int(args[2]) if len(args)>2 else None)
|
||||
elif cmd == 'names': cmd_names(blob, base, args[1])
|
||||
elif cmd == 'strn': cmd_strn(blob, base, int(args[1]) if len(args)>1 else None)
|
||||
Reference in New Issue
Block a user