#!/usr/bin/env python3 """Split a Sony PSYLINK library archive (LIB\\x01) into its member LNK objects. Format (derived from PsyQ 4.0 LIBCD.LIB, Phase 7): "LIB\\x01" per member: name[8] space-padded object name (e.g. "CDROM ") date[4] little-endian timestamp obj_off[4] little-endian: bytes from THIS member header to its LNK object (obj_off - 16 bytes: the symbols this object exports) starts with "LNK\\x02", runs to the next member header We don't parse the LNK record stream to find each object's end; instead each member header is located by the invariant u32@(header+12) == (LNK_offset - header) — i.e. obj_off points exactly at that member's "LNK\\x02". An object therefore spans from its "LNK\\x02" to the NEXT member header (or EOF for the last). psyq-obj-parser validates the result downstream (a wrong boundary fails to parse), and the ultimate check is the byte-match of the linked function against the target EXE. Usage: psyq_lib_split.py -> writes /.obj """ import struct, sys, os LNK_MAGIC = b"LNK\x02" def split(lib_path, out_dir): d = open(lib_path, "rb").read() if d[:4] != b"LIB\x01": sys.exit(f"{lib_path}: not a LIB\\x01 archive (magic {d[:4]!r})") # 1) all LNK object starts lnk = [] i = d.find(LNK_MAGIC, 4) while i != -1: lnk.append(i) i = d.find(LNK_MAGIC, i + 4) if not lnk: sys.exit(f"{lib_path}: no LNK objects found") # 2) each LNK's member header: scan back for P with u32@P+12 == lnk-P headers = [] for L in lnk: P = None for cand in range(L - 16, max(L - 0x8000, 0) - 1, -1): if struct.unpack_from(" {sys.argv[2]}") for nm, sz in ms[:8]: print(f" {nm}.obj ({sz} B)") if len(ms) > 8: print(f" … +{len(ms)-8} more")