Files
pit3000-print-reverse-engin…/scripts/parse_delphi_report_bin.py
thug0bin bde81cdc7e 초기 정리: PIT3000 분석 자료 업로드
- 좌표/SQL/API 분석 문서화
- 재현 스크립트와 예시 응답 추가
- 개인정보/원본 실행파일 제외
2026-06-29 00:27:06 +00:00

170 lines
6.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import json, math, struct, sys
from pathlib import Path
CLASSES=[
b'TfrxMemoView', b'TfrxShapeView', b'TfrxLineView', b'TfrxPictureView',
b'TfrxReport', b'TfrxReportPage', b'TfrxMasterData', b'TfrxDetailData',
b'TfrxHeader', b'TfrxFooter', b'TfrxPageHeader', b'TfrxPageFooter',
b'TfrxDBDataset', b'TfrxUserDataSet', b'TfrxDataPage', b'TfrxDialogPage',
# QuickReport / QRDesign objects used by older PIT A4 forms
b'TQuickRep', b'TQRBand', b'TQRChildBand', b'TQRLabel', b'TQRDBText', b'TQRShape',
b'TQRRichText', b'TQRExpr', b'TQRImage', b'TQRDBImage', b'TQRCompositeReport',
]
# Keep essentially all useful report-layout/data-binding properties. Large binary payloads
# are still summarized by read_value() as {'binary_len': N}.
PROP_KEEP={
'Left','Top','Width','Height','Font.Height','Font.Name','Font.Style','Font.Charset','Font.Color',
'HAlign','VAlign','Memo.UTF8W','Memo.Text','Text','Caption','Frame.Typ','Frame.Width','Frame.Color',
'Color','DataSet','DataSetName','DataField','DataSource','FieldName','Master','Detail','Filter',
'Format','FormatStr','DisplayFormat','ExpressionDelimiters','Name','Tag','Visible','Printable',
'StretchMode','WordWrap','AutoWidth','CharSpacing','LineSpacing','GapX','GapY','Rotation','Align',
'Band','ParentFont','Transparent','BrushStyle','Frame.Style','Frame.ShadowWidth','Frame.ShadowColor',
'Restrictions','Page','PaperWidth','PaperHeight','PaperSize','Orientation','Margins.Left','Margins.Top',
'Margins.Right','Margins.Bottom','Columns','ColumnWidth','ColumnPositions','RowCount','Version',
}
# Delphi DFM binary value kinds (subset)
VK={0:'Null',1:'List',2:'Int8',3:'Int16',4:'Int32',5:'Extended',6:'String',7:'Ident',8:'False',9:'True',10:'Binary',11:'Set',12:'LString',13:'Nil',14:'Collection',15:'Single',16:'Currency',17:'Date',18:'WString',19:'Int64',20:'UTF8String'}
def ext80(b: bytes) -> float:
if len(b)!=10: return math.nan
lo=int.from_bytes(b[:8],'little')
se=int.from_bytes(b[8:10],'little')
sign=-1 if (se & 0x8000) else 1
exp=se & 0x7fff
if exp==0 and lo==0: return 0.0
if exp==0x7fff: return math.inf*sign
# 80-bit extended has explicit integer bit, value = lo/2^63 * 2^(exp-bias)
return sign * (lo/(1<<63)) * (2**(exp-16383))
def read_lp(data,i):
if i>=len(data): raise EOFError
n=data[i]; i+=1
if i+n>len(data): raise EOFError
try: s=data[i:i+n].decode('utf-8')
except UnicodeDecodeError:
try: s=data[i:i+n].decode('cp949')
except Exception: s=data[i:i+n].decode('latin1','replace')
return s,i+n
def read_value(data,i):
if i>=len(data): raise EOFError
tag=data[i]; i+=1
if tag==0: return None,i
if tag==1:
vals=[]
# Delphi DFM vaList: nested values terminated by vaNull (0).
while i < len(data):
if data[i] == 0:
i += 1
break
v,i = read_value(data,i)
vals.append(v)
return vals,i
if tag==2:
v=struct.unpack_from('<b',data,i)[0]; return v,i+1
if tag==3:
v=struct.unpack_from('<h',data,i)[0]; return v,i+2
if tag==4:
v=struct.unpack_from('<i',data,i)[0]; return v,i+4
if tag==5:
return ext80(data[i:i+10]), i+10
if tag==6 or tag==7:
return read_lp(data,i)
if tag==8: return False,i
if tag==9: return True,i
if tag==10:
# binary blob: int32 length + bytes
if i+4>len(data): raise EOFError
n=struct.unpack_from('<I',data,i)[0]; i+=4
return {'binary_len':n}, min(len(data),i+n)
if tag==11:
vals=[]
while i < len(data) and data[i]!=0:
s,i=read_lp(data,i); vals.append(s)
if i<len(data) and data[i]==0: i+=1
return vals,i
if tag==12:
if i+4>len(data): raise EOFError
n=struct.unpack_from('<I',data,i)[0]; i+=4
raw=data[i:i+n]; i+=n
return raw.decode('cp949','replace'),i
if tag==15:
return struct.unpack_from('<f',data,i)[0],i+4
if tag==18:
if i+4>len(data): raise EOFError
n=struct.unpack_from('<I',data,i)[0]; i+=4
raw=data[i:i+n*2]; i+=n*2
return raw.decode('utf-16le','replace'),i
if tag==19:
return struct.unpack_from('<q',data,i)[0],i+8
if tag==20:
if i+4>len(data): raise EOFError
n=struct.unpack_from('<I',data,i)[0]; i+=4
raw=data[i:i+n]; i+=n
return raw.decode('utf-8','replace'),i
# unknown; fail to let caller resync
raise ValueError(f'unknown tag {tag} at {i-1:x}')
def find_objects(data):
starts=[]
for cls in CLASSES:
pos=0
while True:
j=data.find(cls,pos)
if j<0: break
if j>0 and data[j-1]==len(cls):
starts.append(j-1)
pos=j+1
return sorted(set(starts))
def parse_object_at(data,start,next_start=None):
i=start
cls,i=read_lp(data,i)
name,i=read_lp(data,i)
props={}
limit=next_start if next_start else min(len(data), start+5000)
# Parse until next object start; binary blobs can skip far, so limit may be before i after Picture.Data.
while i < len(data) and i < limit:
if data[i]==0:
i+=1; continue
# Avoid eating next object header as property if at a known start.
if i in OBJECT_STARTS and i!=start:
break
try:
prop,i2=read_lp(data,i)
except Exception:
break
if not prop or len(prop)>80 or any(ord(ch)<32 for ch in prop):
break
try:
val,i3=read_value(data,i2)
except Exception:
# Some extracted chunks are not aligned; advance one byte and try to keep scanning.
i=i+1
continue
if prop in PROP_KEEP or prop in {'Tag','Name'}:
props[prop]=val
i=i3
return {'class':cls,'name':name,'offset':start,'props':props}
def main(path):
global OBJECT_STARTS
data=Path(path).read_bytes()
OBJECT_STARTS=find_objects(data)
objs=[]
for idx,s in enumerate(OBJECT_STARTS):
ns=OBJECT_STARTS[idx+1] if idx+1<len(OBJECT_STARTS) else None
try:
o=parse_object_at(data,s,ns)
if o['class'] in [c.decode() for c in CLASSES]:
objs.append(o)
except Exception:
pass
print(json.dumps({'file':Path(path).name,'objects':objs}, ensure_ascii=False, indent=2))
if __name__=='__main__':
main(sys.argv[1])