"""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('>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')