main
John Lauer Component record for ltc2945cms-pbf: files batch 3 16c3b96 23h ago
#!/usr/bin/env python3
"""Read the pin records straight out of an Altium .SchLib (OLE2/CFB container, binary pin records), independent of
adom-lbr's own reader, so a writer bug cannot hide behind a matching reader. Prints, per pin: designator, name,
electrical type and the conglomerate flags (hidden, name shown, designator shown).
Pin record layout per the public format notes (AltiumSharp / pyAltiumLib): int32 record=2, byte, int16 owner part,
byte display mode, 4 symbol bytes, pascal description, byte formal type, byte electrical, byte conglomerate,
int16 length, int16 x, int16 y, int32 colour, pascal name, pascal designator.
Conglomerate bits: 0x01/0x02 orientation, 0x04 hide, 0x08 name visible, 0x10 designator visible.
usage: schlib_pins.py file.SchLib [out.json]"""
import struct, sys, json
ET = {0: "input", 1: "io", 2: "output", 3: "open_collector", 4: "passive", 5: "hiz", 6: "open_emitter", 7: "power"}

def cfb_streams(path):
    b = open(path, "rb").read()
    assert b[:8] == bytes.fromhex("d0cf11e0a1b11ae1"), "not an OLE2 file"
    ss = 1 << struct.unpack_from("<H", b, 30)[0]; mss = 1 << struct.unpack_from("<H", b, 32)[0]
    nfat, dir0, _, mini_cut, minifat0, nminifat, difat0, ndifat = struct.unpack_from("<IIIIIIII", b, 44)
    hdr = max(512, ss)  # the header fills a whole sector: 512 bytes in CFB v3, 4096 in v4
    sec = lambda i: b[hdr + i * ss: hdr + (i + 1) * ss]
    difat = list(struct.unpack_from("<109I", b, 76)); d = difat0
    for _ in range(ndifat):
        s = sec(d); difat += list(struct.unpack_from("<%dI" % (ss // 4 - 1), s)); d = struct.unpack_from("<I", s, ss - 4)[0]
    fat = []
    for f in difat[:nfat]: fat += list(struct.unpack_from("<%dI" % (ss // 4), sec(f)))
    def chain(i):
        # a sector chain visits each sector once; a cycle or an out-of-range index means a bad file or a misread
        # header, so stop instead of growing the list until the machine runs out of memory
        out = []; seen = set()
        while i < 0xFFFFFFFA:
            if i in seen or i >= len(fat): raise ValueError(f"bad FAT chain at sector {i} after {len(out)} sectors")
            seen.add(i); out.append(i); i = fat[i]
        return out
    read = lambda i: b"".join(sec(j) for j in chain(i))
    dirs = read(dir0); ents = []
    for k in range(len(dirs) // 128):
        e = dirs[k * 128:(k + 1) * 128]; nl = struct.unpack_from("<H", e, 64)[0]
        left, right, child = struct.unpack_from("<III", e, 68)
        ents.append(dict(name=e[:max(nl - 2, 0)].decode("utf-16-le"), type=e[66], left=left, right=right, child=child,
                         start=struct.unpack_from("<I", e, 116)[0], size=struct.unpack_from("<Q", e, 120)[0]))
    root = ents[0]; ministream = read(root["start"]) if root["start"] < 0xFFFFFFFA else b""
    minifat = []
    if nminifat:
        mf = read(minifat0); minifat = list(struct.unpack_from("<%dI" % (len(mf) // 4), mf))
    def data(e):
        if e["size"] < mini_cut:
            out = []; i = e["start"]
            while i < 0xFFFFFFFA: out.append(ministream[i * mss:(i + 1) * mss]); i = minifat[i]
            return b"".join(out)[:e["size"]]
        return read(e["start"])[:e["size"]]
    paths = {}
    def walk(idx, prefix):
        if idx == 0xFFFFFFFF: return
        e = ents[idx]; walk(e["left"], prefix); walk(e["right"], prefix)
        p = prefix + "/" + e["name"]
        if e["type"] == 2: paths[p] = data(e)
        if e["type"] == 1: walk(e["child"], p)
    walk(root["child"], "")
    return paths

def pascal(b, o):
    n = b[o]; return b[o + 1:o + 1 + n].decode("latin-1"), o + 1 + n

def pins(stream):
    o = 0; out = []
    while o + 4 <= len(stream):
        hdr = struct.unpack_from("<I", stream, o)[0]; ln = hdr & 0xFFFFFF; kind = hdr >> 24; rec = stream[o + 4:o + 4 + ln]; o += 4 + ln
        if kind == 1 and len(rec) > 4 and struct.unpack_from("<i", rec, 0)[0] == 2:
            p = 4 + 1 + 2 + 1 + 4
            desc, p = pascal(rec, p)
            formal, elec, cong = rec[p], rec[p + 1], rec[p + 2]; p += 3
            length, x, y = struct.unpack_from("<hhh", rec, p); p += 6 + 4
            name, p = pascal(rec, p); des, p = pascal(rec, p)
            out.append(dict(designator=des, name=name, electrical=ET.get(elec, elec), electrical_code=elec,
                            hidden=bool(cong & 0x04), name_visible=bool(cong & 0x08), designator_visible=bool(cong & 0x10),
                            conglomerate=cong, length=length, x=x, y=y))
    return out

res = {}
for path, blob in cfb_streams(sys.argv[1]).items():
    if path.endswith("/Data") and path.count("/") == 2:
        ps = pins(blob)
        if ps: res[path.split("/")[1]] = ps
print(json.dumps(res, indent=1))
if len(sys.argv) > 2: json.dump(res, open(sys.argv[2], "w"), indent=1)