from __future__ import annotations
import struct
XCDR2_MAX_ALIGNMENT = 4
XCDR1_MAX_ALIGNMENT = 8
class CdrWriter:
__slots__ = ("buf", "max_alignment", "align_origin", "_bo")
def __init__(
self,
max_alignment: int = XCDR2_MAX_ALIGNMENT,
align_origin: int = 0,
endian: str = "le",
) -> None:
self.buf = bytearray()
self.max_alignment = max_alignment
self.align_origin = align_origin
self._bo = ">" if endian == "be" else "<"
def _align(self, n: int) -> None:
eff = n if n < self.max_alignment else self.max_alignment
pos = len(self.buf) + self.align_origin
pad = (-pos) % eff
if pad:
self.buf.extend(b"\x00" * pad)
def write_bool(self, v: bool) -> None:
self.buf.append(1 if v else 0)
def write_u8(self, v: int) -> None:
self.buf.append(v & 0xFF)
def write_i8(self, v: int) -> None:
self.buf.append(v & 0xFF)
def write_u16(self, v: int) -> None:
self._align(2)
self.buf.extend(struct.pack(self._bo + "H", v & 0xFFFF))
def write_i16(self, v: int) -> None:
self._align(2)
self.buf.extend(struct.pack(self._bo + "h", v))
def write_u32(self, v: int) -> None:
self._align(4)
self.buf.extend(struct.pack(self._bo + "I", v & 0xFFFFFFFF))
def write_i32(self, v: int) -> None:
self._align(4)
self.buf.extend(struct.pack(self._bo + "i", v))
def write_u64(self, v: int) -> None:
self._align(8)
self.buf.extend(struct.pack(self._bo + "Q", v & 0xFFFFFFFFFFFFFFFF))
def write_i64(self, v: int) -> None:
self._align(8)
self.buf.extend(struct.pack(self._bo + "q", v))
def write_f32(self, v: float) -> None:
self._align(4)
self.buf.extend(struct.pack(self._bo + "f", v))
def write_f64(self, v: float) -> None:
self._align(8)
self.buf.extend(struct.pack(self._bo + "d", v))
def write_string(self, s: str) -> None:
encoded = s.encode("utf-8")
self.write_u32(len(encoded) + 1)
self.buf.extend(encoded)
self.buf.append(0)
def write_bytes(self, b: bytes) -> None:
self.write_u32(len(b))
self.buf.extend(b)
def write_fixed_bcd(self, decimal: str, p: int, s: int) -> None:
text = decimal
positive = True
if text.startswith("-"):
positive, text = False, text[1:]
elif text.startswith("+"):
text = text[1:]
int_part, _, frac_part = text.partition(".")
int_needed = p - s
if len(int_part) > int_needed:
raise ValueError(f"fixed: integer part '{int_part}' exceeds P-S={int_needed}")
if len(frac_part) > s:
raise ValueError(f"fixed: fractional part '{frac_part}' exceeds S={s}")
digits = int_part.rjust(int_needed, "0") + frac_part.ljust(s, "0")
nibbles: list[int] = []
if (p + 1) % 2 == 1:
nibbles.append(0)
for c in digits:
if not c.isdigit():
raise ValueError(f"fixed: non-digit '{c}'")
nibbles.append(ord(c) - 48)
nibbles.append(0x0C if positive else 0x0D)
for i in range(0, len(nibbles), 2):
self.buf.append((nibbles[i] << 4) | nibbles[i + 1])
def write_char(self, c: str) -> None:
if not isinstance(c, str) or len(c) != 1:
raise ValueError(f"char must be a single-character str, got {c!r}")
self.buf.append(ord(c) & 0xFF)
def write_wstring(self, s: str) -> None:
encoded = s.encode("utf-16-be" if self._bo == ">" else "utf-16-le")
self.write_u32(len(encoded))
self.buf.extend(encoded)
def position(self) -> int:
return len(self.buf) + self.align_origin
def write_dheader(self, body: bytes) -> None:
self.write_u32(len(body))
self.buf.extend(body)
def write_emheader_lc(
self,
member_id: int,
lc: int,
body: bytes,
must_understand: bool = False,
) -> None:
self._align(4) m_bit = (1 if must_understand else 0) << 31
lc_bits = (lc & 0b111) << 28
emheader = m_bit + lc_bits + (member_id & 0x0FFF_FFFF)
self.write_u32(emheader)
if lc <= 3:
want = (1, 2, 4, 8)[lc]
if len(body) != want:
raise ValueError(
f"LC{lc} requires exactly {want}-byte body, got {len(body)}",
)
elif lc == 4:
self.write_u32(len(body)) else: if len(body) < 4:
raise ValueError(
f"LC{lc} member body must start with a 4-byte length word",
)
self.buf.extend(body)
def write_pl_cdr1_member(self, member_id: int, body: bytes) -> None:
self._align(4)
body_len = len(body)
if member_id >= 0x3F00 or body_len > 0xFFFF:
self.write_u16(0x3F01) self.write_u16(8)
self.write_u32(member_id)
self.write_u32(body_len)
else:
self.write_u16(member_id)
self.write_u16(body_len)
self.buf.extend(body)
pad = (4 - (body_len % 4)) % 4
for _ in range(pad):
self.buf.append(0)
def write_pl_cdr1_sentinel(self) -> None:
self._align(4)
self.write_u16(0x3F02)
self.write_u16(0)
def into_bytes(self) -> bytes:
return bytes(self.buf)
class CdrReader:
__slots__ = ("buf", "pos", "max_alignment", "align_origin", "_bo")
def __init__(
self,
data: bytes,
max_alignment: int = XCDR2_MAX_ALIGNMENT,
align_origin: int = 0,
endian: str = "le",
) -> None:
self.buf = data
self.pos = 0
self.max_alignment = max_alignment
self.align_origin = align_origin
self._bo = ">" if endian == "be" else "<"
def _align(self, n: int) -> None:
eff = n if n < self.max_alignment else self.max_alignment
abs_pos = self.pos + self.align_origin
self.pos += (-abs_pos) % eff
def position(self) -> int:
return self.pos + self.align_origin
def read_dheader(self) -> "CdrReader":
if self.max_alignment != XCDR2_MAX_ALIGNMENT:
return self
length = self.read_u32()
body = self._take(length)
return CdrReader(
bytes(body),
max_alignment=self.max_alignment,
align_origin=self.position() - length,
endian="be" if self._bo == ">" else "le",
)
def _take(self, n: int) -> bytes:
if self.pos + n > len(self.buf):
raise ValueError(
f"CDR underrun: need {n} bytes at pos {self.pos}, have {len(self.buf)}",
)
out = self.buf[self.pos : self.pos + n]
self.pos += n
return out
def read_mutable_member(self) -> "tuple[int, bool, CdrReader] | None":
if self.pos >= len(self.buf):
return None
emheader = self.read_u32()
must_understand = (emheader >> 31) & 1 == 1
lc = (emheader >> 28) & 0b0111
member_id = emheader & 0x0FFF_FFFF
if lc <= 3:
length = (1, 2, 4, 8)[lc]
body = self._take(length)
elif lc == 4:
length = self.read_u32() body = self._take(length)
else:
self._align(4)
word = struct.unpack(self._bo + "I", self.buf[self.pos : self.pos + 4])[0]
if lc == 5:
content = word
elif lc == 6:
content = 4 * word
else: content = 8 * word
body = self._take(4 + content)
return (member_id, must_understand, CdrReader(
bytes(body),
max_alignment=self.max_alignment,
align_origin=0,
endian="be" if self._bo == ">" else "le",
))
def read_pl_cdr1_member(self) -> "tuple[int, CdrReader] | None":
_PID_LIST_END = 0x3F02
_PID_EXTENDED = 0x3F01
self._align(4)
if self.pos + 4 > len(self.buf):
return None
pid = self.read_u16()
len_u16 = self.read_u16()
if pid == _PID_LIST_END:
return None
if pid == _PID_EXTENDED:
member_id = self.read_u32()
body_len = self.read_u32()
else:
member_id, body_len = pid, len_u16
body = self._take(body_len)
pad = (4 - (body_len % 4)) % 4
for _ in range(pad):
if self.pos >= len(self.buf):
break
self.pos += 1
return (member_id, CdrReader(
bytes(body),
max_alignment=self.max_alignment,
align_origin=0,
endian="be" if self._bo == ">" else "le",
))
def read_bool(self) -> bool:
return self._take(1)[0] != 0
def read_u8(self) -> int:
return self._take(1)[0]
def read_i8(self) -> int:
return struct.unpack(self._bo + "b", self._take(1))[0]
def read_u16(self) -> int:
self._align(2)
return struct.unpack(self._bo + "H", self._take(2))[0]
def read_i16(self) -> int:
self._align(2)
return struct.unpack(self._bo + "h", self._take(2))[0]
def read_u32(self) -> int:
self._align(4)
return struct.unpack(self._bo + "I", self._take(4))[0]
def read_i32(self) -> int:
self._align(4)
return struct.unpack(self._bo + "i", self._take(4))[0]
def read_u64(self) -> int:
self._align(8)
return struct.unpack(self._bo + "Q", self._take(8))[0]
def read_i64(self) -> int:
self._align(8)
return struct.unpack(self._bo + "q", self._take(8))[0]
def read_f32(self) -> float:
self._align(4)
return struct.unpack(self._bo + "f", self._take(4))[0]
def read_f64(self) -> float:
self._align(8)
return struct.unpack(self._bo + "d", self._take(8))[0]
def read_string(self) -> str:
length = self.read_u32()
if length == 0:
raise ValueError("CDR string length 0 (missing null terminator)")
raw = self._take(length)
if raw[-1] != 0:
raise ValueError("CDR string missing null terminator")
return raw[:-1].decode("utf-8")
def read_bytes(self) -> bytes:
n = self.read_u32()
return bytes(self._take(n))
def read_fixed_bcd(self, p: int, s: int) -> str:
n = (p + 2) // 2
raw = self._take(n)
chars: list[str] = []
sign = "+"
for i in range(n):
hi = (raw[i] >> 4) & 0x0F
lo = raw[i] & 0x0F
chars.append(chr(48 + (hi % 10)))
if i == n - 1:
sign = "-" if lo == 0x0D else "+"
else:
chars.append(chr(48 + (lo % 10)))
while len(chars) > s + 1 and chars[0] == "0":
chars.pop(0)
out = "-" if sign == "-" else ""
if s > 0:
dot = max(len(chars) - s, 0)
for i, c in enumerate(chars):
if i == dot:
out += "."
out += c
else:
out += "".join(chars)
return out
def read_char(self) -> str:
return chr(self._take(1)[0])
def read_wstring(self) -> str:
octets = self.read_u32()
if octets == 0:
return ""
if octets % 2 != 0:
raise ValueError(f"wstring octet length {octets} is not even")
raw = self._take(octets)
if raw[:2] == b"\xff\xfe":
return raw[2:].decode("utf-16-le")
if raw[:2] == b"\xfe\xff":
return raw[2:].decode("utf-16-be")
return raw.decode("utf-16-be" if self._bo == ">" else "utf-16-le")