defmodule ExDBus.Message do @moduledoc """ D-Bus message struct and marshalling. A D-Bus message consists of a fixed header, header fields array, and body. ## Header format byte endianness ('l' = little, 'B' = big) byte message_type (1=method_call, 2=method_return, 3=error, 4=signal) byte flags (0x1=no_reply_expected, 0x2=no_auto_start) byte protocol_version (1) uint32 body_length uint32 serial array header_fields (typed key-value pairs) padding align to 8 bytes ## Header field codes 1 = PATH (object_path, required for method_call/signal) 2 = INTERFACE (string) 3 = MEMBER (string, required for method_call/signal) 4 = ERROR_NAME (string, required for error) 5 = REPLY_SERIAL (uint32, required for method_return/error) 6 = DESTINATION (string) 7 = SENDER (string) 8 = SIGNATURE (signature, body type signature) 9 = UNIX_FDS (uint32) """ alias ExDBus.Wire.{Decoder, Encoder, Types} @protocol_version 1 # Message types @type_method_call 1 @type_method_return 2 @type_error 3 @type_signal 4 # Header field codes @field_path 1 @field_interface 2 @field_member 3 @field_error_name 4 @field_reply_serial 5 @field_destination 6 @field_sender 7 @field_signature 8 @field_unix_fds 9 defstruct [ :type, :serial, flags: 0, path: nil, interface: nil, member: nil, error_name: nil, reply_serial: nil, destination: nil, sender: nil, signature: nil, unix_fds: nil, fds: [], body: [] ] @type message_type :: :method_call | :method_return | :error | :signal @type t :: %__MODULE__{ type: message_type(), serial: non_neg_integer(), flags: non_neg_integer(), path: String.t() | nil, interface: String.t() | nil, member: String.t() | nil, error_name: String.t() | nil, reply_serial: non_neg_integer() | nil, destination: String.t() | nil, sender: String.t() | nil, signature: String.t() | nil, unix_fds: non_neg_integer() | nil, fds: [non_neg_integer()], body: list() } @doc """ Create a method_call message. """ @spec method_call(String.t(), String.t() | nil, String.t(), keyword()) :: t() def method_call(path, interface, member, opts \\ []) do %__MODULE__{ type: :method_call, serial: Keyword.get(opts, :serial, 0), flags: Keyword.get(opts, :flags, 0), path: path, interface: interface, member: member, destination: Keyword.get(opts, :destination), signature: Keyword.get(opts, :signature), body: Keyword.get(opts, :body, []) } end @doc """ Create a method_return message. """ @spec method_return(non_neg_integer(), keyword()) :: t() def method_return(reply_serial, opts \\ []) do %__MODULE__{ type: :method_return, serial: Keyword.get(opts, :serial, 0), flags: Keyword.get(opts, :flags, 0), reply_serial: reply_serial, destination: Keyword.get(opts, :destination), sender: Keyword.get(opts, :sender), signature: Keyword.get(opts, :signature), body: Keyword.get(opts, :body, []) } end @doc """ Create an error message. """ @spec error(String.t(), non_neg_integer(), keyword()) :: t() def error(error_name, reply_serial, opts \\ []) do %__MODULE__{ type: :error, serial: Keyword.get(opts, :serial, 0), flags: Keyword.get(opts, :flags, 0), error_name: error_name, reply_serial: reply_serial, destination: Keyword.get(opts, :destination), sender: Keyword.get(opts, :sender), signature: Keyword.get(opts, :signature), body: Keyword.get(opts, :body, []) } end @doc """ Create a signal message. """ @spec signal(String.t(), String.t(), String.t(), keyword()) :: t() def signal(path, interface, member, opts \\ []) do %__MODULE__{ type: :signal, serial: Keyword.get(opts, :serial, 0), flags: Keyword.get(opts, :flags, 0), path: path, interface: interface, member: member, destination: Keyword.get(opts, :destination), signature: Keyword.get(opts, :signature), body: Keyword.get(opts, :body, []) } end @doc """ Encode a message struct to D-Bus wire format binary. Returns iodata suitable for sending over a transport. """ @spec encode_message(t(), ExDBus.Wire.Types.endianness()) :: binary() def encode_message(%__MODULE__{} = msg, endianness \\ :little) do endian_byte = endianness_to_byte(endianness) type_byte = type_to_byte(msg.type) # Encode body first to know body_length (body is at offset 0 relative to body start) body_binary = encode_body(msg.body, msg.signature, endianness) body_length = byte_size(body_binary) # Fixed header: endianness(1) + type(1) + flags(1) + version(1) + body_len(4) + serial(4) = 12 bytes fixed_header = [ <>, <>, <>, <<@protocol_version::8>>, encode_uint32(body_length, endianness), encode_uint32(msg.serial, endianness) ] # Encode header fields array at offset 12 (after fixed header) # This ensures alignment is calculated relative to message start fields = build_header_fields(msg) {fields_iodata, offset_after_fields} = Encoder.encode_at(fields, {:array, {:struct, [:byte, :variant]}}, endianness, 12) fields_binary = IO.iodata_to_binary(fields_iodata) # Align body to 8-byte boundary body_padding_len = rem(8 - rem(offset_after_fields, 8), 8) body_padding = <<0::size(body_padding_len)-unit(8)>> IO.iodata_to_binary([fixed_header, fields_binary, body_padding, body_binary]) end @doc """ Decode a D-Bus message from binary data. Returns `{:ok, message, rest}` or `{:error, reason}`. """ @spec decode_message(binary()) :: {:ok, t(), binary()} | {:error, term()} def decode_message(binary) when byte_size(binary) < 16 do {:error, :insufficient_data} end def decode_message(binary) do # Read first byte to determine endianness <> = binary endianness = case endian_byte do ?l -> :little ?B -> :big _ -> :invalid end if endianness == :invalid do {:error, {:invalid_endianness, endian_byte}} else decode_message_with_endianness(rest, endianness) end end # --- Private encode helpers --- defp encode_body([], _signature, _endianness), do: <<>> defp encode_body(body, signature, endianness) when is_binary(signature) do {:ok, types} = Types.parse_types(signature) encode_body_values(body, types, endianness, 0) |> IO.iodata_to_binary() end defp encode_body(_body, nil, _endianness), do: <<>> defp encode_body_values([], [], _endianness, _offset), do: [] defp encode_body_values([value | values], [type | types], endianness, offset) do {encoded, new_offset} = Encoder.encode_at(value, type, endianness, offset) [encoded | encode_body_values(values, types, endianness, new_offset)] end defp build_header_fields(%__MODULE__{} = msg) do [] |> maybe_add_field(msg.path, @field_path, "o") |> maybe_add_field(msg.interface, @field_interface, "s") |> maybe_add_field(msg.member, @field_member, "s") |> maybe_add_field(msg.error_name, @field_error_name, "s") |> maybe_add_field(msg.reply_serial, @field_reply_serial, "u") |> maybe_add_field(msg.destination, @field_destination, "s") |> maybe_add_field(msg.sender, @field_sender, "s") |> maybe_add_field(msg.signature, @field_signature, "g") |> maybe_add_field(msg.unix_fds, @field_unix_fds, "u") |> Enum.reverse() end defp maybe_add_field(acc, nil, _code, _sig), do: acc defp maybe_add_field(acc, value, code, sig), do: [{code, {sig, value}} | acc] # --- Private decode helpers --- defp decode_message_with_endianness(binary, endianness) do <> = binary with {:ok, msg_type} <- byte_to_type(type_byte), {:ok, body_length, rest2} <- decode_raw_uint32(rest, endianness), {:ok, serial, rest3} <- decode_raw_uint32(rest2, endianness), {:ok, fields_list, rest4, offset} <- Decoder.decode_at(rest3, {:array, {:struct, [:byte, :variant]}}, endianness, 12), body_padding = rem(8 - rem(offset, 8), 8), <<_pad::binary-size(body_padding), body_rest::binary>> <- rest4, sig = extract_field_value(fields_list, @field_signature), {body, rest5} <- decode_body(body_rest, sig, body_length, endianness, offset + body_padding) do msg = %__MODULE__{ type: msg_type, serial: serial, flags: flags, path: extract_field_value(fields_list, @field_path), interface: extract_field_value(fields_list, @field_interface), member: extract_field_value(fields_list, @field_member), error_name: extract_field_value(fields_list, @field_error_name), reply_serial: extract_field_value(fields_list, @field_reply_serial), destination: extract_field_value(fields_list, @field_destination), sender: extract_field_value(fields_list, @field_sender), signature: sig, unix_fds: extract_field_value(fields_list, @field_unix_fds), body: body } {:ok, msg, rest5} else :insufficient_data -> {:error, :insufficient_data} {:error, _} = err -> err _ -> {:error, :insufficient_data_for_body_padding} end end defp decode_body(binary, nil, _body_length, _endianness, _offset), do: {[], binary} defp decode_body(binary, "", _body_length, _endianness, _offset), do: {[], binary} defp decode_body(binary, signature, body_length, endianness, _offset) do if byte_size(binary) < body_length do :insufficient_data else <> = binary {:ok, types} = Types.parse_types(signature) # Body alignment is relative to the body start (offset 0) case decode_body_values(body_data, types, endianness, 0) do {:ok, values, _rest, _offset} -> {values, rest} {:error, _reason} -> {[], rest} end end end defp decode_body_values(binary, [], _endianness, offset), do: {:ok, [], binary, offset} defp decode_body_values(binary, [type | types], endianness, offset) do case Decoder.decode_at(binary, type, endianness, offset) do {:ok, value, rest, new_offset} -> case decode_body_values(rest, types, endianness, new_offset) do {:ok, values, rest2, final_offset} -> {:ok, [value | values], rest2, final_offset} error -> error end error -> error end end defp extract_field_value(fields_list, code) do case Enum.find(fields_list, fn {c, _v} -> c == code end) do {_, {_sig, value}} -> value nil -> nil end end 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 endianness_to_byte(:little), do: ?l defp endianness_to_byte(:big), do: ?B defp type_to_byte(:method_call), do: @type_method_call defp type_to_byte(:method_return), do: @type_method_return defp type_to_byte(:error), do: @type_error defp type_to_byte(:signal), do: @type_signal defp byte_to_type(@type_method_call), do: {:ok, :method_call} defp byte_to_type(@type_method_return), do: {:ok, :method_return} defp byte_to_type(@type_error), do: {:ok, :error} defp byte_to_type(@type_signal), do: {:ok, :signal} defp byte_to_type(byte), do: {:error, {:invalid_message_type, byte}} defp encode_uint32(value, :little), do: <> defp encode_uint32(value, :big), do: <> end