defmodule Tortoise.Package.Connect do @moduledoc false @opcode 1 alias Tortoise.Package @opaque t :: %__MODULE__{ __META__: Package.Meta.t(), protocol: binary(), protocol_version: non_neg_integer(), user_name: binary() | nil, password: binary() | nil, clean_session: boolean(), keep_alive: non_neg_integer(), client_id: Tortoise.client_id(), will: Package.Publish.t() | nil } @enforce_keys [:client_id] defstruct __META__: %Package.Meta{opcode: @opcode}, protocol: "MQTT", protocol_version: 0b00000100, user_name: nil, password: nil, clean_session: true, keep_alive: 60, client_id: nil, will: nil @spec decode(binary()) :: t def decode(<<@opcode::4, 0::4, variable::binary>>) do <<4::big-integer-size(16), "MQTT", 4::8, user_name::1, password::1, will_retain::1, will_qos::2, will::1, clean_session::1, 0::1, keep_alive::big-integer-size(16), package::binary>> = drop_length(variable) options = [ client_id: 1, will_topic: will, will_payload: will, user_name: user_name, password: password ] |> Enum.filter(fn {_, present} -> present == 1 end) |> Enum.map(fn {value, 1} -> value end) |> Enum.zip(decode_length_prefixed(package)) %__MODULE__{ client_id: options[:client_id], user_name: options[:user_name], password: options[:password], will: if will == 1 do %Package.Publish{ topic: options[:will_topic], payload: nullify(options[:will_payload]), qos: will_qos, retain: will_retain == 1 } end, clean_session: clean_session == 1, keep_alive: keep_alive } end defp nullify(""), do: nil defp nullify(payload), do: payload defp drop_length(payload) do case payload do <<0::1, _::7, r::binary>> -> r <<1::1, _::7, 0::1, _::7, r::binary>> -> r <<1::1, _::7, 1::1, _::7, 0::1, _::7, r::binary>> -> r <<1::1, _::7, 1::1, _::7, 1::1, _::7, 0::1, _::7, r::binary>> -> r end end defp decode_length_prefixed(<<>>), do: [] defp decode_length_prefixed(<>) do <> = payload [item] ++ decode_length_prefixed(rest) end defimpl Tortoise.Encodable do def encode(%Package.Connect{client_id: client_id} = t) when is_binary(client_id) do [ Package.Meta.encode(t.__META__), Package.variable_length_encode([ protocol_header(t), connection_flags(t), keep_alive(t), payload(t) ]) ] end def encode(%Package.Connect{client_id: client_id} = t) when is_atom(client_id) do encode(%Package.Connect{t | client_id: Atom.to_string(client_id)}) end defp protocol_header(%{protocol: protocol, protocol_version: version}) do [Package.length_encode(protocol), version] end defp connection_flags(%{will: nil} = f) do << flag(f.user_name)::integer-size(1), flag(f.password)::integer-size(1), # will retain flag(0)::integer-size(1), # will qos 0::integer-size(2), # will flag flag(0)::integer-size(1), flag(f.clean_session)::integer-size(1), # reserved bit 0::1 >> end defp connection_flags(%{will: %Package.Publish{}} = f) do << flag(f.user_name)::integer-size(1), flag(f.password)::integer-size(1), flag(f.will.retain)::integer-size(1), f.will.qos::integer-size(2), flag(f.will.topic)::integer-size(1), flag(f.clean_session)::integer-size(1), # reserved bit 0::1 >> end defp keep_alive(f) do <> end defp payload(%{will: nil} = f) do [f.client_id, f.user_name, f.password] |> Enum.filter(&is_binary/1) |> Enum.map(&Package.length_encode/1) end defp payload(f) do will_payload = encode_payload(f.will.payload) [f.client_id, f.will.topic, will_payload, f.user_name, f.password] |> Enum.filter(&is_binary/1) |> Enum.map(&Package.length_encode/1) end defp encode_payload(nil), do: "" defp encode_payload(payload), do: payload defp flag(f) when f in [0, nil, false], do: 0 defp flag(_), do: 1 end end