defmodule ExDBus.Wire.Decoder do @moduledoc """ Decode D-Bus wire protocol binary format to Elixir terms. The inverse of `ExDBus.Wire.Encoder`. Uses binary pattern matching for decoding, consuming correct alignment padding before each value. All decode functions return `{:ok, value, rest, new_offset}` internally, where `rest` is the remaining binary and `new_offset` tracks position for alignment calculations. """ alias ExDBus.Wire.Types @doc """ Decode a single value from a D-Bus wire format binary. Returns `{:ok, value, rest}` or `{:error, reason}`. ## Examples iex> ExDBus.Wire.Decoder.decode(<<42, 0, 0, 0>>, :int32) {:ok, 42, <<>>} iex> ExDBus.Wire.Decoder.decode(<<5, 0, 0, 0, "hello", 0>>, :string) {:ok, "hello", <<>>} """ @spec decode( binary(), ExDBus.Wire.Types.dbus_type() | String.t(), ExDBus.Wire.Types.endianness() ) :: {:ok, term(), binary()} | {:error, term()} def decode(binary, type, endianness \\ :little) do type = normalize_type(type) case decode_impl(binary, type, endianness, 0) do {:ok, value, rest, _offset} -> {:ok, value, rest} {:error, _} = error -> error end end @doc """ Decode a value at a specific offset (for internal use and message decoding). Returns `{:ok, value, rest, new_offset}` or `{:error, reason}`. """ @spec decode_at( binary(), ExDBus.Wire.Types.dbus_type() | String.t(), ExDBus.Wire.Types.endianness(), non_neg_integer() ) :: {:ok, term(), binary(), non_neg_integer()} | {:error, term()} def decode_at(binary, type, endianness, offset) do type = normalize_type(type) decode_impl(binary, type, endianness, offset) end defp normalize_type(type) when is_binary(type) do case Types.parse_signature(type) do {:ok, parsed} -> parsed {:error, _} -> raise ArgumentError, "Invalid type signature: #{type}" end end defp normalize_type(type), do: type # Skip alignment padding bytes defp consume_padding(binary, offset, alignment) do padding = rem(alignment - rem(offset, alignment), alignment) case binary do <<_pad::binary-size(padding), rest::binary>> -> {:ok, rest, offset + padding} _ -> {:error, {:insufficient_data_for_padding, byte_size(binary), padding}} end end # Consume alignment padding before array elements (struct/dict_entry need 8-byte alignment) defp consume_array_element_padding(binary, offset, elem_align) when elem_align > 4 do consume_padding(binary, offset, elem_align) end defp consume_array_element_padding(binary, offset, _elem_align) do {:ok, binary, offset} end # --- Basic types --- defp decode_impl(binary, :byte, _endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 1) do case rest do <> -> {:ok, value, rest2, offset + 1} _ -> {:error, {:insufficient_data, :byte}} end end end defp decode_impl(binary, :boolean, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 4) do case decode_raw_uint32(rest, endianness) do {:ok, 0, rest2} -> {:ok, false, rest2, offset + 4} {:ok, 1, rest2} -> {:ok, true, rest2, offset + 4} {:ok, v, _rest2} -> {:error, {:invalid_boolean, v}} error -> error end end end defp decode_impl(binary, :int16, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 2) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 2} {:big, <>} -> {:ok, value, rest2, offset + 2} _ -> {:error, {:insufficient_data, :int16}} end end end defp decode_impl(binary, :uint16, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 2) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 2} {:big, <>} -> {:ok, value, rest2, offset + 2} _ -> {:error, {:insufficient_data, :uint16}} end end end defp decode_impl(binary, :int32, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 4) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 4} {:big, <>} -> {:ok, value, rest2, offset + 4} _ -> {:error, {:insufficient_data, :int32}} end end end defp decode_impl(binary, :uint32, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 4) do decode_raw_uint32_with_offset(rest, endianness, offset) end end defp decode_impl(binary, :int64, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 8) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 8} {:big, <>} -> {:ok, value, rest2, offset + 8} _ -> {:error, {:insufficient_data, :int64}} end end end defp decode_impl(binary, :uint64, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 8) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 8} {:big, <>} -> {:ok, value, rest2, offset + 8} _ -> {:error, {:insufficient_data, :uint64}} end end end defp decode_impl(binary, :double, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 8) do case {endianness, rest} do {:little, <>} -> {:ok, value, rest2, offset + 8} {:big, <>} -> {:ok, value, rest2, offset + 8} _ -> {:error, {:insufficient_data, :double}} end end end defp decode_impl(binary, :string, endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 4), {:ok, len, rest2} <- decode_raw_uint32(rest, endianness) do offset = offset + 4 case rest2 do <> -> {:ok, str, rest3, offset + len + 1} _ -> {:error, {:insufficient_data, :string}} end end end defp decode_impl(binary, :object_path, endianness, offset) do # Same wire format as string decode_impl(binary, :string, endianness, offset) end defp decode_impl(binary, :signature, _endianness, offset) do with {:ok, rest, offset} <- consume_padding(binary, offset, 1), <> <- rest, offset = offset + 1, <> <- rest2 do {:ok, sig, rest3, offset + len + 1} else {:error, _} = err -> err _ -> {:error, {:insufficient_data, :signature}} end end defp decode_impl(binary, :unix_fd, endianness, offset) do # Same wire format as uint32 with {:ok, rest, offset} <- consume_padding(binary, offset, 4) do decode_raw_uint32_with_offset(rest, endianness, offset) end end # --- Container types --- defp decode_impl(binary, :variant, endianness, offset) do # Decode signature first with {:ok, sig_str, rest, offset} <- decode_impl(binary, :signature, endianness, offset), {:ok, val_type} <- Types.parse_signature(sig_str), {:ok, value, rest2, offset} <- decode_impl(rest, val_type, endianness, offset) do {:ok, {sig_str, value}, rest2, offset} end end defp decode_impl(binary, {:array, elem_type}, endianness, offset) do # Decode array length (uint32) with {:ok, rest, offset} <- consume_padding(binary, offset, 4), {:ok, array_len, rest2} <- decode_raw_uint32(rest, endianness) do offset = offset + 4 # For struct/dict_entry arrays, we need to align to 8 bytes before the first element elem_align = Types.alignment(elem_type) with {:ok, rest3, offset} <- consume_array_element_padding(rest2, offset, elem_align) do # Decode elements within the array_len byte boundary end_offset = offset + array_len decode_array_elements(rest3, elem_type, endianness, offset, end_offset, []) end end end defp decode_impl(binary, {:struct, types}, endianness, offset) do # Struct starts with 8-byte alignment with {:ok, rest, offset} <- consume_padding(binary, offset, 8) do decode_struct_members(rest, types, endianness, offset, []) end end defp decode_impl(binary, {:dict_entry, key_type, val_type}, endianness, offset) do # Dict entry starts with 8-byte alignment with {:ok, rest, offset} <- consume_padding(binary, offset, 8), {:ok, key, rest2, offset} <- decode_impl(rest, key_type, endianness, offset), {:ok, val, rest3, offset} <- decode_impl(rest2, val_type, endianness, offset) do {:ok, {key, val}, rest3, offset} end end # --- Array element helpers --- defp decode_array_elements(binary, _elem_type, _endianness, offset, end_offset, acc) when offset >= end_offset do {:ok, Enum.reverse(acc), binary, offset} end defp decode_array_elements(binary, elem_type, endianness, offset, end_offset, acc) do case decode_impl(binary, elem_type, endianness, offset) do {:ok, value, rest, new_offset} -> decode_array_elements(rest, elem_type, endianness, new_offset, end_offset, [value | acc]) error -> error end end # --- Struct member helpers --- defp decode_struct_members(binary, [], _endianness, offset, acc) do {:ok, List.to_tuple(Enum.reverse(acc)), binary, offset} end defp decode_struct_members(binary, [type | types], endianness, offset, acc) do case decode_impl(binary, type, endianness, offset) do {:ok, value, rest, new_offset} -> decode_struct_members(rest, types, endianness, new_offset, [value | acc]) error -> error end end # --- Raw decode helpers (without offset tracking, for internal use) --- defp decode_raw_uint32(binary, :little) do case binary do <> -> {:ok, value, rest} _ -> {:error, {:insufficient_data, :uint32}} end end defp decode_raw_uint32(binary, :big) do case binary do <> -> {:ok, value, rest} _ -> {:error, {:insufficient_data, :uint32}} end end defp decode_raw_uint32_with_offset(binary, endianness, offset) do case decode_raw_uint32(binary, endianness) do {:ok, value, rest} -> {:ok, value, rest, offset + 4} error -> error end end end