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,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