145 lines
5.8 KiB
Python
145 lines
5.8 KiB
Python
"""Extract control->column bindings from the SGV executable's embedded Delphi binary DFM forms (READ-ONLY).
|
|
|
|
python scripts/extract_sgv_dfm_bindings.py <SGV.exe> [--needle COD_ORIGEM_MERCADORIA] [--out file.json]
|
|
|
|
Reads the exe bytes (never modifies it), locates every 'TPF0' form stream that contains the needle, parses the
|
|
Delphi binary DFM and reports, for every data-aware control, its DataField/DataSource, Caption/Text-like properties
|
|
and the chain of parent containers (group boxes, tab sheets) — i.e. the on-screen label context.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
VA = dict(NULL=0, LIST=1, INT8=2, INT16=3, INT32=4, EXTENDED=5, STRING=6, IDENT=7, FALSE=8, TRUE=9, BINARY=10, SET=11, LSTRING=12, NIL=13,
|
|
COLLECTION=14, SINGLE=15, CURRENCY=16, DATE=17, WSTRING=18, INT64=19, UTF8=20, DOUBLE=21)
|
|
|
|
|
|
class R:
|
|
def __init__(self, data, pos=0):
|
|
self.d, self.p = data, pos
|
|
|
|
def u8(self):
|
|
v = self.d[self.p]; self.p += 1; return v
|
|
|
|
def take(self, n):
|
|
v = self.d[self.p:self.p + n]
|
|
if len(v) < n:
|
|
raise ValueError("eof")
|
|
self.p += n; return v
|
|
|
|
def sstr(self):
|
|
return self.take(self.u8()).decode("latin-1")
|
|
|
|
def value(self):
|
|
t = self.u8()
|
|
if t == VA["NULL"] or t == VA["NIL"]: return None
|
|
if t == VA["LIST"]:
|
|
out = []
|
|
while self.d[self.p] != 0:
|
|
out.append(self.value())
|
|
self.p += 1; return out
|
|
if t == VA["INT8"]: return struct.unpack("<b", self.take(1))[0]
|
|
if t == VA["INT16"]: return struct.unpack("<h", self.take(2))[0]
|
|
if t == VA["INT32"]: return struct.unpack("<i", self.take(4))[0]
|
|
if t == VA["EXTENDED"]: self.take(10); return "<ext>"
|
|
if t in (VA["STRING"], VA["IDENT"]): return self.sstr()
|
|
if t == VA["FALSE"]: return False
|
|
if t == VA["TRUE"]: return True
|
|
if t == VA["BINARY"]:
|
|
n = struct.unpack("<I", self.take(4))[0]; self.take(n); return "<binary>"
|
|
if t == VA["SET"]:
|
|
out = []
|
|
while True:
|
|
s = self.sstr()
|
|
if not s: break
|
|
out.append(s)
|
|
return out
|
|
if t == VA["LSTRING"] or t == VA["UTF8"]:
|
|
n = struct.unpack("<I", self.take(4))[0]; return self.take(n).decode("utf-8", "replace" if t == VA["UTF8"] else "replace")
|
|
if t == VA["WSTRING"]:
|
|
n = struct.unpack("<I", self.take(4))[0]; return self.take(n * 2).decode("utf-16-le", "replace")
|
|
if t == VA["SINGLE"]: self.take(4); return "<single>"
|
|
if t == VA["CURRENCY"] or t == VA["DATE"] or t == VA["DOUBLE"] or t == VA["INT64"]: self.take(8); return "<8b>"
|
|
if t == VA["COLLECTION"]:
|
|
out = []
|
|
while self.d[self.p] != 0:
|
|
if self.d[self.p] in (2, 3, 4): # optional collection order value
|
|
self.value()
|
|
if self.d[self.p] == 1: # vaList marker that opens every collection item
|
|
self.p += 1
|
|
item = {}
|
|
while self.d[self.p] != 0:
|
|
key = self.sstr() # evaluate the name BEFORE the value (Python evaluates the RHS first)
|
|
item[key] = self.value()
|
|
self.p += 1; out.append(item)
|
|
self.p += 1; return out
|
|
raise ValueError(f"unknown value type {t} at {self.p}")
|
|
|
|
def component(self):
|
|
first = self.u8()
|
|
if first & 0xF0 == 0xF0: # ObjInherit / ObjFlags prefix
|
|
if first & 2: # ffChildPos
|
|
self.value()
|
|
cls = self.sstr()
|
|
else:
|
|
self.p -= 1; cls = self.sstr()
|
|
name = self.sstr()
|
|
props = {}
|
|
while self.d[self.p] != 0:
|
|
pn = self.sstr(); props[pn] = self.value()
|
|
self.p += 1
|
|
kids = []
|
|
while self.d[self.p] != 0:
|
|
kids.append(self.component())
|
|
self.p += 1
|
|
return {"class": cls, "name": name, "props": props, "children": kids}
|
|
|
|
|
|
def walk(node, chain, out):
|
|
cap = node["props"].get("Caption")
|
|
here = chain + ([f"{node['class']}:{node['name']}" + (f"[{cap}]" if isinstance(cap, str) and cap else "")] if node["class"] != "TQuery" else [])
|
|
p = node["props"]
|
|
if "DataField" in p or node["class"].startswith(("TLabel", "TRz", "TGroup", "TTab", "TRadio")):
|
|
out.append({"class": node["class"], "name": node["name"], "DataField": p.get("DataField"), "DataSource": p.get("DataSource"), "Caption": cap,
|
|
"Items": p.get("Items"), "Values": p.get("Values"), "FocusControl": p.get("FocusControl"), "Left": p.get("Left"), "Top": p.get("Top"), "path": [c for c in chain]})
|
|
for k in node["children"]:
|
|
walk(k, here, out)
|
|
|
|
|
|
def find_forms(data, needle):
|
|
forms = []
|
|
for m in re.finditer(b"TPF0", data):
|
|
start = m.start()
|
|
try:
|
|
r = R(data, start + 4)
|
|
tree = r.component()
|
|
except Exception:
|
|
continue
|
|
blob = data[start:r.p]
|
|
if needle in blob:
|
|
forms.append((start, r.p, tree))
|
|
return forms
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("exe"); ap.add_argument("--needle", default="COD_ORIGEM_MERCADORIA"); ap.add_argument("--out")
|
|
a = ap.parse_args(argv)
|
|
data = open(a.exe, "rb").read()
|
|
forms = find_forms(data, a.needle.encode())
|
|
result = []
|
|
for start, end, tree in forms:
|
|
controls = []
|
|
walk(tree, [], controls)
|
|
result.append({"offset": start, "size": end - start, "form_class": tree["class"], "form_name": tree["name"], "controls": controls})
|
|
text = json.dumps(result, ensure_ascii=False, indent=1, default=str)
|
|
if a.out:
|
|
open(a.out, "w", encoding="utf-8").write(text)
|
|
print(f"forms containing {a.needle!r}: {[(f['form_class'], f['size'], len(f['controls'])) for f in result]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|