79 خطوط
2.8 KiB
Python
79 خطوط
2.8 KiB
Python
"""Bounds-checked property record reader; keeps physical indexes including binary records."""
|
|
import re
|
|
import base64
|
|
import struct
|
|
from dataclasses import dataclass
|
|
|
|
MAX_RECORDS = 100_000
|
|
MAX_RECORD_BYTES = 4 * 1024 * 1024
|
|
|
|
class ParseError(ValueError):
|
|
pass
|
|
|
|
@dataclass
|
|
class Record:
|
|
index: int
|
|
stream: str
|
|
properties: dict[str,str]
|
|
binary: bool = False
|
|
@property
|
|
def id(self):
|
|
return f'{self.stream}:{self.index}'
|
|
|
|
|
|
def properties(payload: bytes):
|
|
result = {}
|
|
# UTF8 companion fields take precedence irrespective of source order.
|
|
utf8 = {}
|
|
for item in payload.rstrip(b'\x00\r\n').split(b'|'):
|
|
if b'=' not in item:
|
|
continue
|
|
k,v = item.split(b'=',1)
|
|
key = k.decode('ascii',errors='replace').upper()
|
|
if key.startswith('%UTF8%'):
|
|
utf8[key[6:]] = v.decode('utf-8',errors='replace')
|
|
else:
|
|
result[key] = v.decode('cp1252',errors='replace')
|
|
result.update(utf8)
|
|
return result
|
|
|
|
|
|
def binary_records(data: bytes, stream: str):
|
|
offset=0
|
|
records=[]
|
|
while offset < len(data):
|
|
if len(records)>MAX_RECORDS:
|
|
raise ParseError('Record count limit exceeded')
|
|
if len(data)-offset<4:
|
|
if not any(data[offset:]): break
|
|
raise ParseError(f'{stream}: truncated length at byte {offset}')
|
|
word=struct.unpack_from('<I',data,offset)[0]
|
|
length, kind=word&0xffffff, word>>24
|
|
if not 0 < length <= MAX_RECORD_BYTES or length>len(data)-offset-4:
|
|
raise ParseError(f'{stream}: invalid record length {length} at byte {offset}')
|
|
payload=data[offset+4:offset+4+length]
|
|
# Header is outside OWNERINDEX numbering; first object is index zero.
|
|
records.append(Record(len(records)-1,stream,properties(payload) if kind==0 else {'BINARY_KIND':str(kind),'BINARY_BASE64':base64.b64encode(payload).decode('ascii')},kind!=0))
|
|
offset+=4+length
|
|
return records
|
|
|
|
|
|
def ascii_records(data: bytes):
|
|
lines=data.lstrip(b'\xef\xbb\xbf').splitlines()
|
|
records=[]
|
|
for line in lines:
|
|
if not line.strip(): continue
|
|
if len(line)>MAX_RECORD_BYTES: raise ParseError('Text record size limit exceeded')
|
|
if len(records)>MAX_RECORDS: raise ParseError('Record count limit exceeded')
|
|
p=properties(line)
|
|
if not p or not ('HEADER' in p or 'RECORD' in p):
|
|
raise ParseError('Malformed ASCII schematic record')
|
|
records.append(Record(len(records)-1,'FileHeader',p))
|
|
return records
|
|
|
|
|
|
def detect_format(data):
|
|
if data.startswith(b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1'): return 'ole'
|
|
if re.match(br'\|HEADER=',data.lstrip(b'\xef\xbb\xbf \r\n'),re.I): return 'ascii'
|
|
raise ParseError('Not a supported SchDoc: expected OLE container or |HEADER= ASCII schematic')
|