"""Parser generico delle sezioni a marcatori dei file Creo (.prt/.asm). Reverse engineering. Modello: ogni campo dichiarato per nome (e0 nome\\0) riceve un id progressivo. In modalita' posizionale il campo atteso e' next[ultimo id letto]; f7 lo forza. """ import struct, sys, os class Err(Exception): pass class Node(list): """struct: lista di (nome, valore)""" def get(s, k, d=None): for n, v in s: if n == k: return v return d def all(s, k): return [v for n, v in s if n == k] class Arr(list): pass class Unk(float): """costante a un byte non ancora identificata""" def __new__(cls, code): o = float.__new__(cls, 'nan') o.code = code return o def __repr__(s): return f"" class P: def __init__(s, b, trace=False): s.b = b s.p = 0 s.fields = {} s.nid = 0 s.next = {} s.alias = {} s.stack = [] s.trace = trace s.root = None s.last = None s.lenient = 0 s.lastc = None s.popped = None def u8(s): v = s.b[s.p] s.p += 1 return v def peek(s): return s.b[s.p] if s.p < len(s.b) else None def rint(s): b = s.u8() if b < 0x80: return b if b < 0xc0: return (b & 0x3f) << 8 | s.u8() if b < 0xe0: v = (b & 0x1f) << 16 | s.b[s.p] << 8 | s.b[s.p + 1] s.p += 2 return v if b == 0xe8: return s.u8() if b == 0xe9: v = int.from_bytes(s.b[s.p:s.p + 2], 'big') s.p += 2 return v if b == 0xea: v = int.from_bytes(s.b[s.p:s.p + 3], 'big') s.p += 3 return v if b == 0xeb: v = int.from_bytes(s.b[s.p:s.p + 4], 'big') s.p += 4 return v if b == 0xf5: return s.rint() | 0x80000000 if b == 0xf6: return -1 raise Err(f"int tag {b:02x} @{s.p - 1:x}") CONST = {0x17: -6.283185307179586, 0x14: 4.71238898038469, 0x0d: -0.5, 0x11: 3.141592653589793, 0x15: -3.141592653589793, 0x12: 1.5707963267948966, 0x16: -1.5707963267948966, 0x13: 6.283185307179586, 0x18: 0.0, 0x0f: 1.0, 0x0e: 0.5, 0x10: -1.0, 0x0c: 0.1, 0x0a: 0.01, 0x07: 0.001} FBASE = 0x3f9d FCONST = {0x0b: 1.0, 0x28: -1.0} def rflt(s): t = s.u8() if 0x30 <= t <= 0x4f: T = (0x37 + t - 0x30) if t < 0x40 else (0xb7 + t - 0x40) raw = bytes([T]) + s.b[s.p:s.p + 3] s.p += 3 elif t == 0xea: raw = s.b[s.p:s.p + 3] + bytes(1) s.p += 3 elif t == 0xeb: raw = s.b[s.p:s.p + 4] s.p += 4 elif t == 0xe9: raw = s.b[s.p:s.p + 2] + bytes(2) s.p += 2 elif t == 0xe8: raw = s.b[s.p:s.p + 1] + bytes(3) s.p += 1 elif t == 0x00: return 0.0 elif t in s.FCONST: return s.FCONST[t] elif t < 0x30: return Unk(t) elif 0x50 <= t <= 0xdf: raw = (s.FBASE + t).to_bytes(2, 'big') + s.b[s.p:s.p + 2] s.p += 2 else: raise Err(f"flt tag {t:02x} @{s.p - 1:x}") return struct.unpack('>f', raw)[0] def rbyte(s): b = s.u8() if b == 0xe8: return s.u8() return b # prefissi negativi a 16 bit dei tag a5-df: bfd0..c00d meno tre buchi (bfd2, bfdf, bffe) NEG = [x for x in range(0xbfd0, 0xc00e) if x not in (0xbfd2, 0xbfdf, 0xbffe)] def rdbl(s): t = s.u8() if t in s.CONST: return s.CONST[t] if t <= 0x16 and t > 0x03: return Unk(t) if t <= 0x03: c = s.u8() k = t + 1 pl = s.b[s.p:s.p + k] s.p += k if c not in s.CONST: if 0x04 <= c <= 0x16: return Unk(c) raise Err(f"dbl const {c:02x} after modifier @{s.p - 1:x}") raw = bytearray(struct.pack('>d', s.CONST[c])) raw[8 - k:] = pl return struct.unpack('>d', bytes(raw))[0] if t == 0xed: v = struct.unpack('>d', s.b[s.p:s.p + 8])[0] s.p += 8 return v if t == 0xea: v = struct.unpack('>d', s.b[s.p:s.p + 3] + bytes(5))[0] s.p += 3 return v if t == 0xec: v = struct.unpack('>d', s.b[s.p:s.p + 7] + bytes(1))[0] s.p += 7 return v if 0x19 <= t <= 0x48: if t <= 0x2f: T = 0x3c + (t - 0x17) // 5 m = (t - 0x17) % 5 else: T = 0xbc + (t - 0x30) // 5 m = (t - 0x30) % 5 if m == 4: pl = s.b[s.p:s.p + 2] s.p += 2 raw = bytes([T]) + pl + b'\x00' * 5 elif m == 3: pl = s.b[s.p:s.p + 2] s.p += 2 raw = bytes([T]) + pl + pl[1:] * 5 elif m == 2: pl = s.b[s.p:s.p + 7] s.p += 7 raw = bytes([T]) + pl elif m == 1: pl = s.b[s.p:s.p + 6] s.p += 6 raw = bytes([T]) + pl + b'\x00' else: pl = s.b[s.p:s.p + 3] s.p += 3 raw = bytes([T]) + pl + bytes(4) return struct.unpack('>d', raw)[0] if 0x49 <= t <= 0xdf: hi = (0x3f75 + t) if t <= 0xa4 else s.NEG[t - 0xa5] pl = s.b[s.p:s.p + 6] s.p += 6 return struct.unpack('>d', hi.to_bytes(2, 'big') + pl)[0] raise Err(f"dbl tag {t:02x} @{s.p - 1:x}") def rstr(s): if s.peek() == 0xe1: s.p += 1 return None e = s.b.index(0, s.p) v = s.b[s.p:e].decode('latin1') s.p = e + 1 return v def rarr(s, n, rd): out = [] while len(out) < n: b = s.peek() if 0xe4 <= b <= 0xe6 and out: s.p += 1 out += [out[-1]] * (b - 0xe3) elif b == 0xe7 and out: s.p += 1 k = s.rint() out += [out[-1]] * k else: out.append(rd()) if len(out) != n: raise Err(f"array overrun {len(out)}>{n} @{s.p:x}") return out def scalar(s, f): t = f['t'] if t in (0x0a, 0x03): if s.peek() == 0xf8: s.p += 1 f['dim'] = (s.rint(),) return [s.rstr() for _ in range(f['dim'][0])] if f.get('dim'): out = [] while len(out) < f['dim'][0] and 0x20 <= s.peek() < 0xe0: out.append(s.rstr()) return out return s.rstr() rd = s.rdbl if t == 0x02 else s.rbyte if t in (0x07, 0x04) else s.rflt if t == 0x06 else s.rint b = s.peek() if b == 0xe1: s.p += 1 return None if b == 0xf8: s.p += 1 f['dim'] = (s.rint(),) elif b == 0xf9: s.p += 1 f['dim'] = (s.rint(), s.rint()) elif b == 0xfc: s.p += 1 f['dim'] = (s.rint(),) + tuple(f['dim'][1:]) elif b == 0xfa: s.p += 1 f['dim'] = tuple(s.rint() for _ in range(s.rint())) d = f.get('dim') if d is None: return rd() n = 1 for k in d: n *= k v = s.rarr(n, rd) if len(d) == 2: v = [v[i * d[1]:(i + 1) * d[1]] for i in range(d[0])] return v def sch(s, fid): return s.alias.get(fid, fid) # --- contenitori ------------------------------------------------- def owner_index(s, E, post=False): own = s.fields[E]['own'] for i in range(len(s.stack) - 1, -1, -1): c = s.stack[i] if c['kind'] == 'struct': if c['schema'] in own: if post and s.fields[E].get('post') and i > 0: return i - 1 return i elif c['occ'] == E or ('A', c['occ']) in own: return i return None def default_close(s): """chiusura implicita prima di un campo con nome o di un salto senza marcatore f0-f4: se il campo atteso appartiene al contenitore padre di quello in cui e' stato scritto l'ultimo campo, quest'ultimo contenitore si considera chiuso.""" if s.last is None or not s.stack or s.lastc is None: return E = s.next.get(s.last) if E is None: return p = None for k in range(len(s.stack) - 1, -1, -1): if s.stack[k] is s.lastc: p = k break if not p: return i = s.owner_index(E, True) if i == p - 1: del s.stack[p:] def marker(s): b = s.peek() if b is None or not (0xf0 <= b <= 0xf4): return None s.p += 1 if b == 0xf4: return s.rint() return b - 0xf0 def explicit(s, n): for _ in range(n): if not s.stack: raise Err(f"pop underflow @{s.p:x}") s.popped = s.stack.pop() def trans(s, E): if s.last is not None: s.next[s.last] = E s.last = E def locate(s, E, jump, stay): """porta in cima allo stack il contenitore che riceve il campo E. Ritorna 'elem' o 'field'.""" f = s.fields[E] hdr = s.peek() in (0xf8, 0xf9) top = s.stack[-1] if not hdr and f['t'] == 0: for i in range(len(s.stack) - 1, -1, -1): c = s.stack[i] if c['kind'] == 'array' and c['occ'] == E: del s.stack[i + 1:] return 'elem' par = f['parent'] i = s.owner_index(E, not jump) if i is None and jump and top['kind'] == 'struct' and top['node']: # variante di unione riusata in un altro contenitore: resta dove siamo f['own'].add(top['schema']) s.lenient += 1 return 'field' fresh = top['kind'] == 'struct' and not top['node'] if jump and fresh and i != len(s.stack) - 1: s.alias[top['field']] = par top['schema'] = par return 'field' if i is not None: if jump and i != len(s.stack) - 1: s.lenient += 1 del s.stack[i + 1:] return 'field' if jump and top['kind'] == 'array' and not top['node'] and f['t'] == 0 and not hdr: top['occ'] = E return 'elem' raise Err(f"field {E} {f['name']}: container {par} not on stack @{s.p:x}") def element(s, E): c = s.stack[-1] s.trans(E) s.lastc = c if s.peek() == 0xfb: s.p += 1 b = s.u8() if len(c['node']) >= c['n']: raise Err(f"too many elements in {s.fields[c['field']]['name']} @{s.p:x}") if b == 0xe1: c['node'].append(None) elif b in (0xe2, 0xe3): v = Node() c['node'].append(v) s.stack.append(dict(kind='struct', field=c['field'], schema=s.sch(c['occ']), node=v, elem=True, occ=None, occ0=c['occ'])) else: raise Err(f"array elem tag {b:02x} @{s.p - 1:x} in {s.fields[c['field']]['name']}") def value(s, fid): c = s.stack[-1] if s.stack else None f = s.fields[fid] s.trans(fid) s.lastc = c if f['t'] != 0: v = s.scalar(f) else: b = s.u8() if b == 0xe1: v = None elif b in (0xe2, 0xe3): v = Node() elif b == 0xf8: v = Arr() n = s.rint() elif b == 0xf9: v = Arr() n = s.rint() s.rint() else: raise Err(f"ptr tag {b:02x} @{s.p - 1:x} field {f['name']}") if c is None: s.root = (f['name'], v) elif c['kind'] == 'array': c.setdefault('tail', []).append((f['name'], v)) else: c['node'].append((f['name'], v)) if isinstance(v, Arr): s.stack.append(dict(kind='array', field=fid, occ=fid, n=n, node=v)) elif isinstance(v, Node): s.stack.append(dict(kind='struct', field=fid, schema=s.sch(fid), node=v, occ=None)) def run(s): b = s.b while s.p < len(b): m = s.marker() x = s.peek() if x is None: s.explicit(m or 0) break if x == 0xe0: s.p += 1 t = s.u8() e = b.index(0, s.p) name = b[s.p:e].decode('latin1') s.p = e + 1 m2 = s.marker() if m2 is not None: m = (m or 0) + m2 s.popped = None if m is None: s.default_close() else: s.explicit(m) c = s.stack[-1] if s.stack else None s.nid += 1 fid = s.nid if c is None: par = 0 elif c['kind'] == 'array': par = ('A', c['occ']) else: par = c['schema'] s.fields[fid] = dict(name=name, t=t, parent=par, own={par}) pp = s.popped if pp is not None and c is not None and pp['kind'] == 'struct' and c['kind'] == 'struct' and pp['schema'] == c['schema'] and t == 0: s.fields[fid]['post'] = True if s.trace: print(f"{s.p:6x} {' ' * len(s.stack)}#{fid} {name} t={t}") s.value(fid) continue if m: s.explicit(m) jump = False if x == 0xf7: s.p += 1 E = s.rint() jump = True if m is None: s.default_close() if E not in s.fields: raise Err(f"jump to unknown field {E} @{s.p:x}") else: E = s.next.get(s.last) if E is None: raise Err(f"no expected field @{s.p:x} after {s.last} " f"{s.fields[s.last]['name'] if s.last else ''}") kind = s.locate(E, jump or m is not None, m == 0) if s.trace: print(f"{s.p:6x} {' ' * len(s.stack)}{'>' if jump else '='}{E} {s.fields[E]['name']} {kind}") if kind == 'elem': s.element(E) else: s.value(E) return s.root def short(x, maxarr=40): if isinstance(x, Unk): return repr(x) if isinstance(x, float): return repr(round(x, 9)) if isinstance(x, list): r = ', '.join(short(y) for y in x[:maxarr]) return f"[{r}{', ...' if len(x) > maxarr else ''}] (n={len(x)})" return repr(x) def fmt(v, ind=0, out=None, maxarr=40): pad = ' ' * ind if isinstance(v, Node): for n, x in v: if isinstance(x, (Node, Arr)): out.append(f"{pad}{n}:") fmt(x, ind + 1, out, maxarr) else: out.append(f"{pad}{n} = {short(x, maxarr)}") elif isinstance(v, Arr): for i, x in enumerate(v): out.append(f"{pad}[{i}]") fmt(x, ind + 1, out, maxarr) def parse(b, trace=False): p = P(b, trace) try: root = p.run() return p, root, None except (Err, IndexError, ValueError, KeyError) as e: return p, p.root, e if __name__ == '__main__': sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import prt d, secs, order = prt.load(sys.argv[1]) names = sys.argv[2].split(',') if len(sys.argv) > 2 and sys.argv[2] != '*' else [n for n in order if secs[n][2][2] == '9'] outdir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != '-' else None for nme in names: o, l, f = secs[nme] b = d[o:o + l] p, root, e = parse(b, trace=len(sys.argv) > 4) ok = e is None and not p.stack print(f"{nme:22} len={l:7} parsed={p.p:7} fields={p.nid:5} lenient={p.lenient} {'OK' if ok else 'ERR ' + repr(e) + ' stack=' + str(len(p.stack))}") if e is not None: prt.hexdump(b, max(0, p.p - 64), 128) if outdir and root: out = [root[0] + ':'] fmt(root[1], 1, out) open(f"{outdir}/{nme.replace(':', '_').replace('#', '_')}.txt", 'w', encoding='utf-8').write('\n'.join(out))