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>
119 lines
4.5 KiB
Python
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)
|