defmodule DBux.Protocol do require Logger @type endianness :: :little_endian | :big_endian @debug !is_nil(System.get_env("DBUX_DEBUG")) @doc """ Unmarshalls given bitstring while interpreting data using given endianness and signature (where signature comes in D-Bus format). It returns `{:ok, {list_of_values, rest}}` on success. If `unwrap_values` is `false`, `list_of_values` will contain a nested list of `DBux.Value` structs, otherwise it will contain plain values. For example, signature "i(ss)" will map to the following result: `[%DBux.Value{type: :int32, value: 123}, %DBux.Value{type: :struct, value: [ %DBux.Value{type: :string, value: "sample1"}, %DBux.Value{type: :string, value: "sample2"}]]` if unwrap_values is set to `false`, but it will map to: `[123, {"sample1", "sample2"}]` if it is set to `true`. `rest` will contain remaining part of the bitstring. It returns `{:error, reason}` in case of failure. Specifically, it returns `{:error, :bitstring_too_short}` if given bitstring was not long enough. """ @spec unmarshall_bitstring(Bitstring, endianness, String.t, boolean) :: {:ok, DBux.Value.list_of_values, Bitstring} | {:error, any} def unmarshall_bitstring(bitstring, endianness, signature, unwrap_values) when is_binary(bitstring) and is_atom(endianness) and is_binary(signature) do case DBux.Type.type_from_signature(signature) do {:ok, signature_as_list} -> unmarshall_bitstring_step(bitstring, endianness, signature_as_list, unwrap_values, [], 0) {:error, reason} -> {:error, reason} end end @doc """ Unmarshalls given bitstring while interpreting data using given endianness and signature (where signature comes in as nested list of types, such as one generated by `DBux.Type.type_from_signature/1`). It handles padding between values if they require some alignment. It returns `{:ok, {list_of_values, rest}}` on success. If `unwrap_values` is `false`, `list_of_values` will contain a nested list of `DBux.Value` structs, otherwise it will contain plain values. For example, signature "i(ss)" will map to the following result: `[%DBux.Value{type: :int32, value: 123}, %DBux.Value{type: :struct, value: [ %DBux.Value{type: :string, value: "sample1"}, %DBux.Value{type: :string, value: "sample2"}]]` if unwrap_values is set to `false`, but it will map to: `[123, {"sample1", "sample2"}]` if it is set to `true`. `rest` will contain remaining part of the bitstring. It returns `{:error, reason}` in case of failure. Specifically, it returns `{:error, :bitstring_too_short}` if given bitstring was not long enough. """ @spec unmarshall_bitstring(Bitstring, endianness, DBux.Type.list_of_types, boolean) :: {:ok, DBux.Value.list_of_values, Bitstring} | {:error, any} def unmarshall_bitstring(bitstring, endianness, signature, unwrap_values) when is_binary(bitstring) and is_atom(endianness) and is_list(signature) do unmarshall_bitstring_step(bitstring, endianness, signature, unwrap_values, [], 0) end @doc """ Marshalls given list of values using given endianness. It returns `{:ok, {bitstring, signature}}`. It applies padding between values if they require some alignment. """ @spec marshall_bitstring(DBux.Value.list_of_values, endianness) :: {:ok, {Bitstring, String.t}} def marshall_bitstring(values, endianness) when is_list(values) and is_atom(endianness) do marshall_bitstring_step(values, endianness, << >>, << >>) end defp marshall_bitstring_step([], _endianness, bitstring_acc, signature_acc) do {:ok, {bitstring_acc, signature_acc}} end defp marshall_bitstring_step([head|tail], endianness, bitstring_acc, signature_acc) do {:ok, {bitstring_with_padding, _padding}} = bitstring_acc |> DBux.Value.align(head.type) case DBux.Value.marshall(head, endianness) do {:ok, {bitstring_value, _padding}} -> marshall_bitstring_step(tail, endianness, bitstring_with_padding <> bitstring_value, signature_acc <> DBux.Type.signature(head)) end end defp unmarshall_bitstring_step(bitstring, _endianness, [], _unwrap_values, values_acc, _position_acc) do {:ok, {values_acc, bitstring}} end defp unmarshall_bitstring_step(<< >>, _endianness, [_signature_head|_signature_rest], _unwrap_values, _values_acc, _position_acc) do {:error, :bitstring_too_short} end defp unmarshall_bitstring_step(bitstring, endianness, [signature_head|signature_rest], unwrap_values, values_acc, position_acc) do if @debug, do: Logger.debug("unmarshall_bitstring_step: bitstring = #{inspect(bitstring)}, signature_head = #{inspect(signature_head)}, values_acc = #{inspect(values_acc)}, position_acc = #{inspect(position_acc)}") case signature_head do {subtype_major, subtype_minor} -> padding_size = DBux.Type.compute_padding_size(position_acc, subtype_major) << padding :: binary-size(padding_size), rest_after_padding :: binary >> = bitstring if @debug, do: Logger.debug("unmarshall_bitstring_step: padding = #{inspect(padding)}, rest_after_padding = #{inspect(rest_after_padding)}") case DBux.Value.unmarshall(rest_after_padding, endianness, subtype_major, subtype_minor, unwrap_values, 0) do {:ok, {value, rest_after_parse}} -> unmarshall_bitstring_step(rest_after_parse, endianness, signature_rest, unwrap_values, values_acc ++ [value], position_acc + (byte_size(bitstring) - byte_size(rest_after_parse))) {:error, reason} -> {:error, reason} end _ -> padding_size = DBux.Type.compute_padding_size(position_acc, signature_head) << padding :: binary-size(padding_size), rest_after_padding :: binary >> = bitstring if @debug, do: Logger.debug("unmarshall_bitstring_step: padding = #{inspect(padding)}, rest_after_padding = #{inspect(rest_after_padding)}") case DBux.Value.unmarshall(rest_after_padding, endianness, signature_head, nil, unwrap_values, 0) do {:ok, {value, rest_after_parse}} -> unmarshall_bitstring_step(rest_after_parse, endianness, signature_rest, unwrap_values, values_acc ++ [value], position_acc + (byte_size(bitstring) - byte_size(rest_after_parse))) {:error, reason} -> {:error, reason} end end end end