defmodule SnmpKit.SnmpLib.PDU.Encoder do @moduledoc """ ASN.1 BER encoding functions for SNMP PDUs and messages. This module handles the conversion of Elixir data structures to binary ASN.1 BER format for SNMP protocol communication. """ import Bitwise alias SnmpKit.SnmpLib.OID alias SnmpKit.SnmpLib.PDU.Constants alias SnmpKit.SnmpLib.PDU.V3Encoder @type message :: Constants.message() @type pdu :: Constants.pdu() # Import constants for encoding @get_request Constants.get_request() @getnext_request Constants.getnext_request() @get_response Constants.get_response() @set_request Constants.set_request() @getbulk_request Constants.getbulk_request() @integer Constants.integer() @octet_string Constants.octet_string() @null Constants.null() @object_identifier Constants.object_identifier() @counter32 Constants.counter32() @gauge32 Constants.gauge32() @timeticks Constants.timeticks() @counter64 Constants.counter64() @ip_address Constants.ip_address() @opaque_type Constants.opaque_type() @no_such_object Constants.no_such_object() @no_such_instance Constants.no_such_instance() @end_of_mib_view Constants.end_of_mib_view() @doc """ Encodes an SNMP message to binary format. """ @spec encode_message(message()) :: {:ok, binary()} | {:error, atom()} def encode_message(%{version: 3} = message) do # Delegate SNMPv3 messages to specialized encoder V3Encoder.encode_message(message, nil) end def encode_message(%{version: version, community: community, pdu: pdu}) do encode_snmp_message_fast(version, community, pdu) rescue error -> {:error, {:encoding_error, error}} catch error -> {:error, {:encoding_error, error}} end def encode_message(_), do: {:error, :invalid_message_format} @doc """ Encodes an SNMP message with security user (SNMPv3). """ @spec encode_message(message(), map() | nil) :: {:ok, binary()} | {:error, atom()} def encode_message(%{version: 3} = message, user) do V3Encoder.encode_message(message, user) end def encode_message(message, _user) do # Fall back to regular encoding for v1/v2c encode_message(message) end @doc """ Encodes a PDU to binary format. """ @spec encode_pdu(pdu()) :: {:ok, binary()} | {:error, atom()} def encode_pdu(pdu) when is_map(pdu) do case encode_pdu_fast(pdu) do {:ok, result} when is_binary(result) -> {:ok, result} {:error, reason} -> {:error, reason} end rescue error -> {:error, {:encoding_error, error}} catch error -> {:error, {:encoding_error, error}} end @doc """ Encodes an SNMP message to binary format (alias for encode_message/1). """ @spec encode(message()) :: {:ok, binary()} | {:error, atom()} def encode(message) when is_map(message) do encode_message(message) end @doc """ Alias for encode/1. """ @spec encode_snmp_packet(message()) :: {:ok, binary()} | {:error, atom()} def encode_snmp_packet(message), do: encode(message) ## Private Implementation defp encode_snmp_message_fast(version, community, pdu) when is_integer(version) and is_binary(community) and is_map(pdu) do case encode_pdu_fast(pdu) do {:ok, pdu_encoded} -> iodata = [ encode_integer_fast(version), encode_octet_string_fast(community), pdu_encoded ] content = :erlang.iolist_to_binary(iodata) {:ok, encode_sequence_ber(content)} {:error, reason} -> {:error, reason} end end defp encode_snmp_message_fast(_, _, _), do: {:error, :invalid_message_format} defp encode_pdu_fast(%{type: :get_request} = pdu), do: encode_standard_pdu_fast(pdu, @get_request) defp encode_pdu_fast(%{type: :get_next_request} = pdu), do: encode_standard_pdu_fast(pdu, @getnext_request) defp encode_pdu_fast(%{type: :get_response} = pdu), do: encode_standard_pdu_fast(pdu, @get_response) defp encode_pdu_fast(%{type: :set_request} = pdu), do: encode_standard_pdu_fast(pdu, @set_request) defp encode_pdu_fast(%{type: :get_bulk_request} = pdu), do: encode_bulk_pdu_fast(pdu) defp encode_pdu_fast(_), do: {:error, :unsupported_pdu_type} defp encode_standard_pdu_fast(pdu, tag) do %{ request_id: request_id, error_status: error_status, error_index: error_index, varbinds: varbinds } = pdu case encode_varbinds_fast(varbinds) do {:ok, varbinds_encoded} -> iodata = [ encode_integer_fast(request_id), encode_integer_fast(error_status), encode_integer_fast(error_index), varbinds_encoded ] content = :erlang.iolist_to_binary(iodata) {:ok, encode_tag_length_value(tag, byte_size(content), content)} {:error, reason} -> {:error, reason} end end defp encode_bulk_pdu_fast(pdu) do %{ request_id: request_id, non_repeaters: non_repeaters, max_repetitions: max_repetitions, varbinds: varbinds } = pdu case encode_varbinds_fast(varbinds) do {:ok, varbinds_encoded} -> iodata = [ encode_integer_fast(request_id), encode_integer_fast(non_repeaters), encode_integer_fast(max_repetitions), varbinds_encoded ] content = :erlang.iolist_to_binary(iodata) {:ok, encode_tag_length_value(@getbulk_request, byte_size(content), content)} {:error, reason} -> {:error, reason} end end defp encode_varbinds_fast(varbinds) when is_list(varbinds) do case encode_varbinds_acc(varbinds, []) do {:ok, iodata} -> content = :erlang.iolist_to_binary(iodata) {:ok, encode_sequence_ber(content)} error -> error end end defp encode_varbinds_acc([], acc), do: {:ok, Enum.reverse(acc)} defp encode_varbinds_acc([varbind | rest], acc) do case encode_varbind_fast(varbind) do {:ok, encoded} -> encode_varbinds_acc(rest, [encoded | acc]) error -> error end end defp encode_varbind_fast({oid, type, value}) when is_list(oid) do case encode_oid_fast(oid) do {:ok, oid_encoded} -> value_encoded = encode_snmp_value_fast(type, value) content = :erlang.iolist_to_binary([oid_encoded, value_encoded]) {:ok, encode_sequence_ber(content)} error -> error end end defp encode_varbind_fast({oid, value}) when is_list(oid) do encode_varbind_fast({oid, :auto, value}) end defp encode_varbind_fast(_), do: {:error, :invalid_varbind_format} # Fast integer encoder defp encode_integer_fast(0), do: <<@integer, 0x01, 0x00>> defp encode_integer_fast(value) when value > 0 and value < 128 do <<@integer, 0x01, value>> end defp encode_integer_fast(value) when is_integer(value) do encode_integer_ber(value) end defp encode_octet_string_fast(value) when is_binary(value) do length = byte_size(value) length_bytes = encode_length_ber(length) [<<@octet_string>>, length_bytes, value] end defp encode_snmp_value_fast(:null, _), do: <<@null, 0x00>> defp encode_snmp_value_fast(:auto, nil), do: <<@null, 0x00>> defp encode_snmp_value_fast(:auto, :null), do: <<@null, 0x00>> defp encode_snmp_value_fast(:integer, value) when is_integer(value), do: encode_integer_fast(value) defp encode_snmp_value_fast(:string, value) when is_binary(value), do: encode_octet_string_fast(value) defp encode_snmp_value_fast(:octet_string, value) when is_binary(value), do: encode_octet_string_fast(value) defp encode_snmp_value_fast(:counter32, value) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@counter32, value) end defp encode_snmp_value_fast(:gauge32, value) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@gauge32, value) end defp encode_snmp_value_fast(:timeticks, value) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@timeticks, value) end defp encode_snmp_value_fast(:counter64, value) when is_integer(value) and value >= 0 and value <= 18_446_744_073_709_551_615 do encode_counter64(@counter64, value) end defp encode_snmp_value_fast(:ip_address, value) when is_binary(value) and byte_size(value) == 4 do encode_tag_length_value(@ip_address, 4, value) end defp encode_snmp_value_fast(:opaque, value) when is_binary(value) do length = byte_size(value) encode_tag_length_value(@opaque_type, length, value) end defp encode_snmp_value_fast(:object_identifier, value) when is_list(value) do case encode_oid_fast(value) do {:ok, encoded} -> encoded {:error, _} -> raise ArgumentError, "Invalid OID list: #{inspect(value)}" end end defp encode_snmp_value_fast(:object_identifier, value) when is_binary(value) do case OID.string_to_list(value) do {:ok, oid_list} -> case encode_oid_fast(oid_list) do {:ok, encoded} -> encoded {:error, _} -> raise ArgumentError, "Invalid OID string: #{inspect(value)}" end {:error, _} -> raise ArgumentError, "Invalid OID string format: #{inspect(value)}" end end defp encode_snmp_value_fast(:auto, {:object_identifier, value}) when is_list(value) do case encode_oid_fast(value) do {:ok, encoded} -> encoded {:error, _} -> <<@null, 0x00>> end end defp encode_snmp_value_fast(:auto, {:object_identifier, value}) when is_binary(value) do case OID.string_to_list(value) do {:ok, oid_list} -> case encode_oid_fast(oid_list) do {:ok, encoded} -> encoded {:error, _} -> <<@null, 0x00>> end {:error, _} -> <<@null, 0x00>> end end defp encode_snmp_value_fast(:auto, {:no_such_object, _}), do: <<@no_such_object, 0x00>> defp encode_snmp_value_fast(:auto, {:no_such_instance, _}), do: <<@no_such_instance, 0x00>> defp encode_snmp_value_fast(:auto, {:end_of_mib_view, _}), do: <<@end_of_mib_view, 0x00>> defp encode_snmp_value_fast(:auto, {:opaque, value}) when is_binary(value) do length = byte_size(value) encode_tag_length_value(@opaque_type, length, value) end defp encode_snmp_value_fast(:auto, {:opaque, _value}), do: <<@null, 0x00>> defp encode_snmp_value_fast(:auto, {:counter32, value}) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@counter32, value) end defp encode_snmp_value_fast(:auto, {:counter32, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, {:gauge32, value}) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@gauge32, value) end defp encode_snmp_value_fast(:auto, {:gauge32, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, {:timeticks, value}) when is_integer(value) and value >= 0 and value <= 4_294_967_295 do encode_unsigned_integer(@timeticks, value) end defp encode_snmp_value_fast(:auto, {:timeticks, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, {:counter64, value}) when is_integer(value) and value >= 0 and value <= 18_446_744_073_709_551_615 do encode_counter64(@counter64, value) end defp encode_snmp_value_fast(:auto, {:counter64, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, {:ip_address, value}) when is_binary(value) and byte_size(value) == 4 do encode_tag_length_value(@ip_address, 4, value) end defp encode_snmp_value_fast(:auto, {:ip_address, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, {_type, _value}) do <<@null, 0x00>> end defp encode_snmp_value_fast(:auto, value) when is_integer(value), do: encode_integer_fast(value) defp encode_snmp_value_fast(:auto, value) when is_binary(value) do # Try to parse as OID string first, fallback to octet string case OID.string_to_list(value) do {:ok, oid_list} -> case encode_oid_fast(oid_list) do {:ok, encoded} -> encoded {:error, _} -> encode_octet_string_fast(value) end {:error, _} -> encode_octet_string_fast(value) end end defp encode_snmp_value_fast(:auto, value) when is_list(value) do # Assume it's an OID if it's a list of non-negative integers if Enum.all?(value, &(is_integer(&1) and &1 >= 0)) do case encode_oid_fast(value) do {:ok, encoded} -> encoded {:error, _} -> raise ArgumentError, "Invalid OID list: #{inspect(value)}" end else raise ArgumentError, "Invalid value for :auto type: #{inspect(value)}" end end defp encode_snmp_value_fast(:end_of_mib_view, nil), do: <<@end_of_mib_view, 0x00>> defp encode_snmp_value_fast(:no_such_object, _), do: <<@no_such_object, 0x00>> defp encode_snmp_value_fast(:no_such_instance, _), do: <<@no_such_instance, 0x00>> defp encode_snmp_value_fast(type, value) do raise ArgumentError, """ Invalid SNMP value encoding. Unsupported type/value combination: Type: #{inspect(type)} Value: #{inspect(value)} Supported types: :integer, :octet_string, :null, :object_identifier, :counter32, :gauge32, :timeticks, :counter64, :ip_address, :opaque, :no_such_object, :no_such_instance, :end_of_mib_view """ end # ASN.1 BER encoding helpers defp encode_integer_ber(value) when is_integer(value) do bytes = integer_to_bytes(value) length = byte_size(bytes) encode_tag_length_value(@integer, length, bytes) end defp integer_to_bytes(0), do: <<0>> defp integer_to_bytes(value) when value > 0 do bytes = :binary.encode_unsigned(value, :big) case bytes do <> when bit == 1 -> <<0>> <> bytes _ -> bytes end end defp integer_to_bytes(value) when value < 0 do positive = abs(value) bit_length = bit_length_for_integer(positive) + 1 byte_length = div(bit_length + 7, 8) max_value = 1 <<< (byte_length * 8) twos_comp = max_value + value <> end @spec bit_length_for_integer(pos_integer()) :: pos_integer() defp bit_length_for_integer(n) when n > 0 do n |> :math.log2() |> :math.ceil() |> trunc() end defp encode_sequence_ber(content) when is_binary(content) do length = byte_size(content) encode_tag_length_value(0x30, length, content) end defp encode_tag_length_value(tag, length, content) do length_bytes = encode_length_ber(length) <> <> length_bytes <> content end defp encode_length_ber(length) when length < 128 do <> end defp encode_length_ber(length) when length < 256 do <<0x81, length>> end defp encode_length_ber(length) when length < 65_536 do <<0x82, length::16>> end defp encode_length_ber(length) when length < 16_777_216 do <<0x83, length::24>> end defp encode_length_ber(length) do <<0x84, length::32>> end # Helper functions for encoding unsigned integers and counter64 defp encode_unsigned_integer(tag, value) when is_integer(value) and value >= 0 do bytes = encode_unsigned_bytes(value) length = byte_size(bytes) encode_tag_length_value(tag, length, bytes) end defp encode_counter64(tag, value) when is_integer(value) and value >= 0 do bytes = <> length = byte_size(bytes) encode_tag_length_value(tag, length, bytes) end defp encode_unsigned_bytes(0), do: <<0>> defp encode_unsigned_bytes(value) when value > 0 do bytes = :binary.encode_unsigned(value, :big) # Ensure the most significant bit is 0 for unsigned integers case bytes do <> when bit == 1 -> <<0>> <> bytes _ -> bytes end end defp encode_oid_fast([first]) when first >= 0 and first < 3 do # Single component OID - encode directly case encode_oid_subids_fast([first], []) do {:ok, content} -> {:ok, encode_tag_length_value(@object_identifier, byte_size(content), content)} error -> error end end defp encode_oid_fast(oid_list) when is_list(oid_list) and length(oid_list) >= 2 do [first, second | rest] = oid_list if first >= 0 and first < 3 and second >= 0 and second < 40 do first_encoded = first * 40 + second case encode_oid_subids_fast([first_encoded | rest], []) do {:ok, content} -> {:ok, encode_tag_length_value(@object_identifier, byte_size(content), content)} error -> error end else {:error, :invalid_oid_format} end end defp encode_oid_fast(_), do: {:error, :invalid_oid_format} defp encode_oid_subids_fast([], acc), do: {:ok, :erlang.iolist_to_binary(Enum.reverse(acc))} defp encode_oid_subids_fast([subid | rest], acc) when subid >= 0 and subid < 128 do encode_oid_subids_fast(rest, [<> | acc]) end defp encode_oid_subids_fast([subid | rest], acc) when subid >= 128 do bytes = encode_subid_multibyte(subid, []) encode_oid_subids_fast(rest, [bytes | acc]) end defp encode_oid_subids_fast(_, _), do: {:error, :invalid_subidentifier} # Encode a subidentifier using ASN.1 BER multibyte encoding defp encode_subid_multibyte(subid, _acc) do encode_subid_multibyte_correct(subid) end # Correct implementation: build bytes from most significant to least significant defp encode_subid_multibyte_correct(subid) when subid < 128 do <> end defp encode_subid_multibyte_correct(subid) do # Build list of 7-bit groups from least to most significant bytes = build_multibyte_list(subid, []) # Convert to binary with high bits set correctly bytes_with_high_bits = set_high_bits(bytes) :erlang.iolist_to_binary(bytes_with_high_bits) end # Build list of 7-bit values from least to most significant defp build_multibyte_list(subid, acc) when subid < 128 do # Most significant byte (no more bits) [subid | acc] end defp build_multibyte_list(subid, acc) do lower_7_bits = subid &&& 0x7F build_multibyte_list(subid >>> 7, [lower_7_bits | acc]) end # Set high bits: all bytes except the last one get the high bit set # Last byte has no high bit defp set_high_bits([last]), do: [last] defp set_high_bits([first | rest]) do # Set high bit on all but last [first ||| 0x80 | set_high_bits(rest)] end end