| # Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import struct | |
| import typing | |
| class Encoder: | |
| def __init__(self) -> None: | |
| self.buf = bytearray() | |
| self.handles: list[typing.Any] = [] | |
| self.ool_tasks: list[typing.Callable[[], None]] = [] | |
| def write_inline(self, offset: int, data: bytes) -> None: | |
| needed = offset + len(data) | |
| if len(self.buf) < needed: | |
| self.buf.extend(b"\x00" * (needed - len(self.buf))) | |
| self.buf[offset : offset + len(data)] = data | |
| def write_bool(self, val: bool, offset: int) -> None: | |
| if not isinstance(val, bool): | |
| raise TypeError(f"Expected bool, got {type(val)}") | |
| self.write_inline(offset, b"\x01" if val else b"\x00") | |
| def _write_primitive( | |
| self, fmt: str, val: typing.Any, offset: int, subtype: str | |
| ) -> None: | |
| if val is None: | |
| raise TypeError( | |
| f"None value not allowed for primitive subtype {subtype}" | |
| ) | |
| if "int" in subtype and not isinstance(val, int): | |
| raise TypeError(f"Expected int, got {type(val)}") | |
| if "float" in subtype and not isinstance(val, (int, float)): | |
| raise TypeError(f"Expected float, got {type(val)}") | |
| try: | |
| self.write_inline(offset, struct.pack(fmt, val)) | |
| except (struct.error, OverflowError) as e: | |
| raise OverflowError( | |
| f"Value {val} out of range for {subtype}" | |
| ) from e | |
| def write_int8(self, val: int, offset: int) -> None: | |
| self._write_primitive("<b", val, offset, "int8") | |
| def write_uint8(self, val: int, offset: int) -> None: | |
| self._write_primitive("<B", val, offset, "uint8") | |
| def write_int16(self, val: int, offset: int) -> None: | |
| self._write_primitive("<h", val, offset, "int16") | |
| def write_uint16(self, val: int, offset: int) -> None: | |
| self._write_primitive("<H", val, offset, "uint16") | |
| def write_int32(self, val: int, offset: int) -> None: | |
| self._write_primitive("<i", val, offset, "int32") | |
| def write_uint32(self, val: int, offset: int) -> None: | |
| self._write_primitive("<I", val, offset, "uint32") | |
| def write_int64(self, val: int, offset: int) -> None: | |
| self._write_primitive("<q", val, offset, "int64") | |
| def write_uint64(self, val: int, offset: int) -> None: | |
| self._write_primitive("<Q", val, offset, "uint64") | |
| def write_float32(self, val: float, offset: int) -> None: | |
| self._write_primitive("<f", val, offset, "float32") | |
| def write_float64(self, val: float, offset: int) -> None: | |
| self._write_primitive("<d", val, offset, "float64") | |
| def write_handle( | |
| self, | |
| val: int | None, | |
| offset: int, | |
| obj_type: int = 0, | |
| rights: int = 0x80000000, | |
| ) -> None: | |
| if val is None or val == 0: | |
| self.write_inline(offset, struct.pack("<I", 0)) | |
| else: | |
| hdl_val = int(val) | |
| if hdl_val < 0 or hdl_val > 0xFFFFFFFF: | |
| raise OverflowError(f"Handle value {hdl_val} out of range") | |
| self.write_inline(offset, struct.pack("<I", 0xFFFFFFFF)) | |
| self.handles.append((0, hdl_val, obj_type, rights, 0)) | |
| def write_string(self, val: str | None, offset: int) -> None: | |
| if val is None: | |
| self.write_inline(offset, struct.pack("<Q", 0)) | |
| self.write_inline(offset + 8, struct.pack("<Q", 0)) | |
| else: | |
| encoded = val.encode("utf-8") | |
| self.write_inline(offset, struct.pack("<Q", len(encoded))) | |
| self.write_inline(offset + 8, struct.pack("<Q", 0xFFFFFFFFFFFFFFFF)) | |
| self.ool_tasks.append(lambda: self.write_ool_bytes(encoded)) | |
| def write_ool_bytes(self, data: bytes) -> None: | |
| pad_mod = len(data) % 8 | |
| if pad_mod != 0: | |
| data = data + b"\x00" * (8 - pad_mod) | |
| self.buf.extend(data) | |
| def write_vector( | |
| self, | |
| val: typing.Sequence[typing.Any] | None, | |
| offset: int, | |
| element_encoder_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| element_size: int, | |
| ) -> None: | |
| if val is None: | |
| self.write_inline(offset, struct.pack("<Q", 0)) | |
| self.write_inline(offset + 8, struct.pack("<Q", 0)) | |
| else: | |
| self.write_inline(offset, struct.pack("<Q", len(val))) | |
| self.write_inline(offset + 8, struct.pack("<Q", 0xFFFFFFFFFFFFFFFF)) | |
| self.ool_tasks.append( | |
| lambda: self.write_ool_vector( | |
| val, element_encoder_fn, element_size | |
| ) | |
| ) | |
| def write_ool_vector( | |
| self, | |
| val: typing.Sequence[typing.Any], | |
| element_encoder_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| element_size: int, | |
| ) -> None: | |
| parent_tasks = self.ool_tasks | |
| self.ool_tasks = [] | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| start_offset = len(self.buf) | |
| self.buf.extend(b"\x00" * (element_size * len(val))) | |
| for i, item in enumerate(val): | |
| element_encoder_fn(item, self, start_offset + i * element_size) | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| i = 0 | |
| while i < len(self.ool_tasks): | |
| self.ool_tasks[i]() | |
| i += 1 | |
| self.ool_tasks = parent_tasks | |
| def write_array( | |
| self, | |
| val: typing.Sequence[typing.Any], | |
| offset: int, | |
| element_encoder_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| element_size: int, | |
| count: int, | |
| ) -> None: | |
| if len(val) != count: | |
| raise ValueError( | |
| f"Array length {len(val)} does not match expected count {count}" | |
| ) | |
| for i in range(count): | |
| element_encoder_fn(val[i], self, offset + i * element_size) | |
| def write_optional_struct( | |
| self, | |
| val: typing.Any, | |
| offset: int, | |
| encode_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| inline_size: int, | |
| ) -> None: | |
| if val is None: | |
| self.write_inline(offset, struct.pack("<Q", 0)) | |
| else: | |
| self.write_inline(offset, struct.pack("<Q", 0xFFFFFFFFFFFFFFFF)) | |
| self.ool_tasks.append( | |
| lambda: self.write_ool_struct(val, encode_fn, inline_size) | |
| ) | |
| def write_ool_struct( | |
| self, | |
| val: typing.Any, | |
| encode_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| inline_size: int, | |
| ) -> None: | |
| parent_tasks = self.ool_tasks | |
| self.ool_tasks = [] | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| start_offset = len(self.buf) | |
| self.buf.extend(b"\x00" * inline_size) | |
| encode_fn(val, self, start_offset) | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| i = 0 | |
| while i < len(self.ool_tasks): | |
| self.ool_tasks[i]() | |
| i += 1 | |
| self.ool_tasks = parent_tasks | |
| def write_envelope( | |
| self, | |
| val: typing.Any, | |
| env_offset: int, | |
| encode_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| inline_size: int, | |
| is_inline: bool, | |
| ) -> None: | |
| if val is None: | |
| self.write_inline(env_offset, b"\x00" * 8) | |
| return | |
| if is_inline: | |
| temp_encoder = Encoder() | |
| encode_fn(val, temp_encoder, 0) | |
| temp_bytes = bytes(temp_encoder.buf) | |
| if len(temp_bytes) < 4: | |
| temp_bytes = temp_bytes + b"\x00" * (4 - len(temp_bytes)) | |
| elif len(temp_bytes) > 4: | |
| raise RuntimeError("Inlined value exceeds 4 bytes") | |
| self.write_inline(env_offset, temp_bytes) | |
| self.handles.extend(temp_encoder.handles) | |
| self.write_inline( | |
| env_offset + 4, struct.pack("<H", len(temp_encoder.handles)) | |
| ) | |
| self.write_inline(env_offset + 6, struct.pack("<H", 1)) | |
| else: | |
| self.write_inline(env_offset, struct.pack("<I", 0)) | |
| self.write_inline(env_offset + 4, struct.pack("<H", 0)) | |
| self.write_inline(env_offset + 6, struct.pack("<H", 0)) | |
| self.ool_tasks.append( | |
| lambda: self.write_ool_envelope_member( | |
| val, encode_fn, inline_size, env_offset | |
| ) | |
| ) | |
| def write_ool_envelope_member( | |
| self, | |
| val: typing.Any, | |
| encode_fn: typing.Callable[[typing.Any, "Encoder", int], None], | |
| inline_size: int, | |
| env_offset: int, | |
| ) -> None: | |
| parent_tasks = self.ool_tasks | |
| self.ool_tasks = [] | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| start_offset = len(self.buf) | |
| self.buf.extend(b"\x00" * inline_size) | |
| start_handles = len(self.handles) | |
| encode_fn(val, self, start_offset) | |
| pad_mod = len(self.buf) % 8 | |
| if pad_mod != 0: | |
| self.buf.extend(b"\x00" * (8 - pad_mod)) | |
| i = 0 | |
| while i < len(self.ool_tasks): | |
| self.ool_tasks[i]() | |
| i += 1 | |
| total_size = len(self.buf) - start_offset | |
| total_handles = len(self.handles) - start_handles | |
| self.write_inline(env_offset, struct.pack("<I", total_size)) | |
| self.write_inline(env_offset + 4, struct.pack("<H", total_handles)) | |
| self.ool_tasks = parent_tasks | |
| def encode_struct( | |
| obj: typing.Any, | |
| encode_fn: typing.Callable[[typing.Any, Encoder, int], None], | |
| inline_size: int, | |
| ) -> tuple[bytes, list[typing.Any]]: | |
| encoder = Encoder() | |
| encoder.buf.extend(b"\x00" * inline_size) | |
| encode_fn(obj, encoder, 0) | |
| i = 0 | |
| while i < len(encoder.ool_tasks): | |
| encoder.ool_tasks[i]() | |
| i += 1 | |
| # Pad final buffer to 8 bytes alignment | |
| pad_mod = len(encoder.buf) % 8 | |
| if pad_mod != 0: | |
| encoder.buf.extend(b"\x00" * (8 - pad_mod)) | |
| return bytes(encoder.buf), encoder.handles |