초기 정리: PIT3000 분석 자료 업로드
- 좌표/SQL/API 분석 문서화 - 재현 스크립트와 예시 응답 추가 - 개인정보/원본 실행파일 제외
This commit is contained in:
169
scripts/parse_delphi_report_bin.py
Normal file
169
scripts/parse_delphi_report_bin.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/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])
|
||||
Reference in New Issue
Block a user