blob: 9131dfd0057304e53be2f8977d8f904265f42f99 [file]
# 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
def _align_to_8(offset: int) -> int:
"""Rounds an offset up to the next 8-byte boundary.
According to the FIDL wire format specification (RFC-0027/RFC-0120), all
out-of-line secondary objects (such as strings, vectors, out-of-line envelopes,
and out-of-line structs) must be aligned to 8-byte boundaries.
"""
pad_mod = offset % 8
return offset + (8 - pad_mod) if pad_mod != 0 else offset
class Decoder:
def __init__(self, buf: bytes, handles: list[typing.Any]) -> None:
self.buf = buf
self.handles = handles
self.ool_offset = 0
self.handle_index = 0
def _align_ool_offset(self) -> None:
"""Aligns the out-of-line offset to the next 8-byte boundary."""
self.ool_offset = _align_to_8(self.ool_offset)
def read_handle(self, offset: int, nullable: bool = False) -> typing.Any:
presence = self._read_primitive("<I", offset)
if presence == 0:
if nullable:
return None
else:
raise ValueError("Absent non-nullable handle")
if self.handle_index >= len(self.handles):
raise ValueError("Not enough handles in decoder")
hdl = self.handles[self.handle_index]
self.handle_index += 1
# Handles in `self.handles` can either be handle wrappers (e.g.,
# `fuchsia_controller_py.Handle` or FDomain handles providing `.as_int()`)
# or raw integer handle values (e.g., in unit tests or GIDL runners).
if hasattr(hdl, "as_int"):
return hdl.as_int()
return int(hdl)
def read_bool(self, offset: int) -> bool:
return self.buf[offset] != 0
def _read_primitive(self, fmt: str, offset: int) -> typing.Any:
return struct.unpack_from(fmt, self.buf, offset)[0]
def read_int8(self, offset: int) -> int:
return self._read_primitive("<b", offset)
def read_uint8(self, offset: int) -> int:
return self._read_primitive("<B", offset)
def read_int16(self, offset: int) -> int:
return self._read_primitive("<h", offset)
def read_uint16(self, offset: int) -> int:
return self._read_primitive("<H", offset)
def read_int32(self, offset: int) -> int:
return self._read_primitive("<i", offset)
def read_uint32(self, offset: int) -> int:
return self._read_primitive("<I", offset)
def read_int64(self, offset: int) -> int:
return self._read_primitive("<q", offset)
def read_uint64(self, offset: int) -> int:
return self._read_primitive("<Q", offset)
def read_float32(self, offset: int) -> float:
return self._read_primitive("<f", offset)
def read_float64(self, offset: int) -> float:
return self._read_primitive("<d", offset)
def read_string(self, offset: int, nullable: bool = False) -> typing.Any:
length = struct.unpack_from("<Q", self.buf, offset)[0]
presence = struct.unpack_from("<Q", self.buf, offset + 8)[0]
if presence == 0:
if nullable:
return None
else:
raise ValueError("Absent non-nullable string")
self._align_ool_offset()
res = self.buf[self.ool_offset : self.ool_offset + length].decode(
"utf-8"
)
self.ool_offset += length
return res
def read_vector(
self,
offset: int,
element_decoder_fn: typing.Callable[["Decoder", int], typing.Any],
element_size: int,
nullable: bool = False,
) -> typing.Any:
count = struct.unpack_from("<Q", self.buf, offset)[0]
presence = struct.unpack_from("<Q", self.buf, offset + 8)[0]
if presence == 0:
if nullable:
return None
else:
raise ValueError("Absent non-nullable vector")
self._align_ool_offset()
start_offset = self.ool_offset
self.ool_offset += element_size * count
res = []
for i in range(count):
res.append(
element_decoder_fn(self, start_offset + i * element_size)
)
return res
def read_array(
self,
offset: int,
element_decoder_fn: typing.Callable[["Decoder", int], typing.Any],
element_size: int,
count: int,
) -> list[typing.Any]:
res = []
for i in range(count):
res.append(element_decoder_fn(self, offset + i * element_size))
return res
def read_optional_struct(
self,
offset: int,
decode_fn: typing.Callable[["Decoder", int], typing.Any],
inline_size: int,
) -> typing.Any:
presence = struct.unpack_from("<Q", self.buf, offset)[0]
if presence == 0:
return None
self._align_ool_offset()
start_offset = self.ool_offset
self.ool_offset += inline_size
return decode_fn(self, start_offset)
def read_envelope(
self,
env_offset: int,
decode_fn: typing.Callable[["Decoder", int], typing.Any],
inline_size: int,
) -> typing.Any:
is_empty = self.buf[env_offset : env_offset + 8] == b"\x00" * 8
if is_empty:
return None
size_or_value = self.buf[env_offset : env_offset + 4]
struct.unpack_from("<H", self.buf, env_offset + 4)[0]
flags = struct.unpack_from("<H", self.buf, env_offset + 6)[0]
inlined = (flags & 1) != 0
if inlined:
temp_decoder = Decoder(
size_or_value, self.handles[self.handle_index :]
)
val = decode_fn(temp_decoder, 0)
self.handle_index += temp_decoder.handle_index
return val
else:
self._align_ool_offset()
num_bytes = struct.unpack_from("<I", self.buf, env_offset)[0]
member_offset = self.ool_offset
inline_size_aligned = _align_to_8(inline_size)
self.ool_offset = member_offset + inline_size_aligned
val = decode_fn(self, member_offset)
self.ool_offset = _align_to_8(member_offset + num_bytes)
return val
def skip_envelope(self, env_offset: int) -> None:
is_empty = self.buf[env_offset : env_offset + 8] == b"\x00" * 8
if is_empty:
return
num_handles = struct.unpack_from("<H", self.buf, env_offset + 4)[0]
flags = struct.unpack_from("<H", self.buf, env_offset + 6)[0]
inlined = (flags & 1) != 0
self.handle_index += num_handles
if not inlined:
num_bytes = struct.unpack_from("<I", self.buf, env_offset)[0]
self._align_ool_offset()
self.ool_offset = _align_to_8(self.ool_offset + num_bytes)
def decode_struct(
bytes: bytes,
handles: list[typing.Any],
decode_fn: typing.Callable[[Decoder, int], typing.Any],
inline_size: int,
) -> typing.Any:
decoder = Decoder(bytes, handles)
decoder.ool_offset = _align_to_8(inline_size)
return decode_fn(decoder, 0)