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