@@ -0,0 +1,78 @@
|
||||
"""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')
|
||||
مرجع در شماره جدید
مسدود کردن کاربر