Files
CydandClaude Fable 5 deaade4f72 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>
2026-07-07 22:43:32 -05:00

119 lines
4.5 KiB
Python

#!/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)