"""Checked Altium PCB framing. No embedded content is decompressed or executed.""" import math import struct from .record_decoder import ParseError, properties MAX_ITEMS = 100_000 MAX_BLOCK = 16 * 1024 * 1024 class Reader: def __init__(self, data, label="PCB"): self.data = memoryview(data) self.offset = 0 self.label = label @property def remaining(self): return len(self.data) - self.offset def take(self, length): if length < 0 or length > self.remaining: raise ParseError( f"{self.label}: truncated data at byte {self.offset}, need {length}" ) start = self.offset self.offset += length return self.data[start : self.offset] def read(self, fmt): result = struct.unpack("<" + fmt, self.take(struct.calcsize("<" + fmt))) if any(isinstance(v, float) and not math.isfinite(v) for v in result): raise ParseError(f"{self.label}: non-finite binary number") return result[0] if len(result) == 1 else result def block(self): length = self.read("I") if length > MAX_BLOCK: raise ParseError(f"{self.label}: subrecord exceeds {MAX_BLOCK} bytes") return self.take(length) def pascal(self): return bytes(self.take(self.read("B"))).decode("cp1252", errors="replace") def text_records(data, name): reader = Reader(data, name) result = [] while reader.remaining: if len(result) >= MAX_ITEMS: raise ParseError(f"{name}: excessive record count") result.append(properties(bytes(reader.block()))) return result def binary_records(data, name, tag, blocks=1): reader = Reader(data, name) index = 0 while reader.remaining: if index >= MAX_ITEMS: raise ParseError(f"{name}: excessive record count") actual = reader.read("B") if actual != tag: raise ParseError( f"{name}: unsupported tag {actual} at record {index}, expected {tag}" ) yield index, [bytes(reader.block()) for _ in range(blocks)] index += 1 def wide_strings(data): reader = Reader(data, "WideStrings6") result = {} while reader.remaining: if len(result) >= MAX_ITEMS: raise ParseError("Excessive wide string count") index = reader.read("I") length = reader.read("I") # Altium writes empty strings with length=2 but no payload in some exports. if ( length == 2 and bytes(reader.data[reader.offset : reader.offset + 2]) != b"\0\0" ): payload = b"" else: if length > MAX_BLOCK: raise ParseError("Wide string exceeds size limit") payload = reader.take(length) if len(payload) % 2: raise ParseError("Odd UTF-16 string length") result[index] = ( bytes(payload).decode("utf-16le", errors="replace").rstrip("\x00") ) return result def at(data, offset, fmt, default=None): size = struct.calcsize("<" + fmt) if offset < 0 or offset + size > len(data): if default is not None: return default raise ParseError(f"Truncated PCB field at offset {offset}, need {size} bytes") values = struct.unpack_from("<" + fmt, data, offset) if any(isinstance(v, float) and not math.isfinite(v) for v in values): raise ParseError("Non-finite PCB field") return values[0] if len(values) == 1 else values