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