defmodule SnmpKit.SnmpLib.PDU.Decoder do @moduledoc """ ASN.1 BER decoding functions for SNMP PDUs and messages. This module handles the conversion of binary ASN.1 BER format to Elixir data structures for SNMP protocol communication. """ import Bitwise alias SnmpKit.SnmpLib.PDU.Constants alias SnmpKit.SnmpLib.PDU.V3Encoder @type message :: Constants.message() @type pdu :: Constants.pdu() # Import constants for decoding @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 """ Decodes an SNMP message from binary format. """ @spec decode_message(binary()) :: {:ok, message()} | {:error, atom()} def decode_message(data) when is_binary(data) do # Check if this is a SNMPv3 message by looking at version case peek_version(data) do {:ok, 3} -> # Delegate to SNMPv3 decoder V3Encoder.decode_message(data, nil) {:ok, _version} -> # Use standard v1/v2c decoder decode_snmp_message_comprehensive(data) {:error, reason} -> {:error, reason} end rescue error -> {:error, {:decoding_error, error}} catch error -> {:error, {:decoding_error, error}} end def decode_message(_), do: {:error, :invalid_input} @doc """ Decodes an SNMP message with security user (SNMPv3). """ @spec decode_message(binary(), map() | nil) :: {:ok, message()} | {:error, atom()} def decode_message(data, user) when is_binary(data) do case peek_version(data) do {:ok, 3} -> V3Encoder.decode_message(data, user) {:ok, _version} -> # v1/v2c messages don't use security users decode_message(data) {:error, reason} -> {:error, reason} end end @doc """ Decodes a PDU from binary format. """ @spec decode_pdu(binary()) :: {:ok, pdu()} | {:error, atom()} def decode_pdu(data) when is_binary(data) do case parse_pdu_comprehensive(data) do {:ok, pdu} -> {:ok, pdu} {:error, reason} -> {:error, reason} end rescue error -> {:error, {:decoding_error, error}} catch error -> {:error, {:decoding_error, error}} end @doc """ Decodes an SNMP message from binary format (alias for decode_message/1). """ @spec decode(binary()) :: {:ok, message()} | {:error, atom()} def decode(data) when is_binary(data) do decode_message(data) end # Private helper to peek at version without full decoding defp peek_version(<<0x30, _length, 0x02, _version_length, version, _rest::binary>>) do {:ok, version} end defp peek_version(data) when is_binary(data) do case parse_sequence(data) do {:ok, {content, _remaining}} -> case parse_integer(content) do {:ok, {version, _rest}} -> {:ok, version} {:error, reason} -> {:error, reason} end {:error, reason} -> {:error, reason} end end defp peek_version(_), do: {:error, :invalid_data} @doc """ Alias for decode/1. """ @spec decode_snmp_packet(binary()) :: {:ok, message()} | {:error, atom()} def decode_snmp_packet(data), do: decode(data) ## Private Implementation # Comprehensive decoding implementation (from SnmpSim) defp decode_snmp_message_comprehensive(<<0x30, rest::binary>>) do case parse_ber_length(rest) do {:ok, {_content_length, content}} -> case parse_snmp_message_fields(content) do {:ok, {version, community, pdu_data}} -> case parse_pdu_comprehensive(pdu_data) do {:ok, pdu} -> {:ok, %{ version: version, community: community, pdu: pdu }} {:error, reason} -> {:error, {:pdu_parse_error, reason}} end {:error, reason} -> {:error, {:message_parse_error, reason}} end {:error, reason} -> {:error, {:message_parse_error, reason}} end end defp decode_snmp_message_comprehensive(_), do: {:error, :invalid_message_format} defp parse_ber_length(<>) when length < 128 do if byte_size(rest) >= length do content = binary_part(rest, 0, length) {:ok, {length, content}} else {:error, :insufficient_data} end end defp parse_ber_length(<>) when length_of_length >= 128 do num_length_bytes = length_of_length - 128 if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do <> = rest actual_length = :binary.decode_unsigned(length_bytes, :big) if byte_size(remaining) >= actual_length do content = binary_part(remaining, 0, actual_length) {:ok, {actual_length, content}} else {:error, :insufficient_data} end else {:error, :invalid_length_encoding} end end defp parse_ber_length(_), do: {:error, :invalid_length_format} defp parse_snmp_message_fields(data) do with {:ok, {version, rest1}} <- parse_integer(data), {:ok, {community, rest2}} <- parse_octet_string(rest1), {:ok, pdu_data} <- {:ok, rest2} do {:ok, {version, community, pdu_data}} end end defp parse_integer(<<@integer, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_length, value_bytes, remaining}} -> if byte_size(value_bytes) > 0 do value = decode_integer_value(value_bytes) {:ok, {value, remaining}} else {:error, :invalid_integer_length} end {:error, reason} -> {:error, reason} end end defp parse_integer(_), do: {:error, :invalid_integer} defp parse_octet_string(<<@octet_string, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_length, value_bytes, remaining}} -> {:ok, {value_bytes, remaining}} {:error, reason} -> {:error, reason} end end defp parse_octet_string(_), do: {:error, :invalid_octet_string} defp parse_ber_length_and_remaining(<>) when length < 128 do if byte_size(rest) >= length do content = binary_part(rest, 0, length) remaining = binary_part(rest, length, byte_size(rest) - length) {:ok, {length, content, remaining}} else {:error, :insufficient_data} end end defp parse_ber_length_and_remaining(<>) when length_of_length >= 128 do num_length_bytes = length_of_length - 128 if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do <> = rest actual_length = :binary.decode_unsigned(length_bytes, :big) if byte_size(remaining_with_content) >= actual_length do content = binary_part(remaining_with_content, 0, actual_length) remaining = binary_part( remaining_with_content, actual_length, byte_size(remaining_with_content) - actual_length ) {:ok, {actual_length, content, remaining}} else {:error, :insufficient_data} end else {:error, :invalid_length_encoding} end end defp parse_ber_length_and_remaining(_), do: {:error, :invalid_length_format} defp parse_pdu_comprehensive(<>) when tag in [0xA0, 0xA1, 0xA2, 0xA3, 0xA5] do pdu_type = case tag do 0xA0 -> :get_request 0xA1 -> :get_next_request 0xA2 -> :get_response 0xA3 -> :set_request 0xA5 -> :get_bulk_request end case parse_ber_length_and_remaining(rest) do {:ok, {_length, pdu_content, _remaining}} -> case pdu_type do :get_bulk_request -> case parse_bulk_pdu_fields(pdu_content) do {:ok, {request_id, non_repeaters, max_repetitions, varbinds}} -> {:ok, %{ type: pdu_type, request_id: request_id, non_repeaters: non_repeaters, max_repetitions: max_repetitions, varbinds: varbinds }} {:error, _reason} -> {:ok, %{type: pdu_type, varbinds: [], non_repeaters: 0, max_repetitions: 0}} end _ -> case parse_standard_pdu_fields(pdu_content) do {:ok, {request_id, error_status, error_index, varbinds}} -> {:ok, %{ type: pdu_type, request_id: request_id, error_status: error_status, error_index: error_index, varbinds: varbinds }} {:error, _reason} -> {:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}} end end {:error, _reason} -> case pdu_type do :get_bulk_request -> {:ok, %{type: pdu_type, varbinds: [], non_repeaters: 0, max_repetitions: 0}} _ -> {:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}} end end end defp parse_pdu_comprehensive(_), do: {:error, :invalid_pdu} defp parse_standard_pdu_fields(data) do with {:ok, {request_id, rest1}} <- parse_integer(data), {:ok, {error_status, rest2}} <- parse_integer(rest1), {:ok, {error_index, rest3}} <- parse_integer(rest2), {:ok, varbinds} <- parse_varbinds(rest3) do {:ok, {request_id, error_status, error_index, varbinds}} end end defp parse_bulk_pdu_fields(data) do with {:ok, {request_id, rest1}} <- parse_integer(data), {:ok, {non_repeaters, rest2}} <- parse_integer(rest1), {:ok, {max_repetitions, rest3}} <- parse_integer(rest2), {:ok, varbinds} <- parse_varbinds(rest3) do {:ok, {request_id, non_repeaters, max_repetitions, varbinds}} end end defp parse_varbinds(data) do case parse_sequence(data) do {:ok, {varbind_data, _rest}} -> parse_varbind_list(varbind_data, []) {:error, _} -> {:ok, []} end end defp parse_sequence(<<0x30, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_length, data, remaining}} -> {:ok, {data, remaining}} {:error, reason} -> {:error, reason} end end defp parse_sequence(_), do: {:error, :not_sequence} defp parse_varbind_list(<<>>, acc), do: {:ok, Enum.reverse(acc)} defp parse_varbind_list(data, acc) do case parse_sequence(data) do {:ok, {varbind_data, rest}} -> case parse_single_varbind(varbind_data) do {:ok, varbind} -> parse_varbind_list(rest, [varbind | acc]) {:error, _} -> parse_varbind_list(rest, acc) end {:error, _} -> {:ok, Enum.reverse(acc)} end end defp parse_single_varbind(data) do with {:ok, {oid, rest1}} <- parse_oid(data), {:ok, {type, value, _rest2}} <- parse_value_with_type(rest1) do {:ok, {oid, type, value}} else _ -> {:error, :invalid_varbind} end end defp parse_oid(<<@object_identifier, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, oid_data, rest2}} -> case decode_oid_data(oid_data) do {:ok, oid} -> {:ok, {oid, rest2}} error -> error end {:error, reason} -> {:error, reason} end end defp parse_oid(_), do: {:error, :invalid_oid} defp decode_oid_data(<>) do first_subid = div(first, 40) second_subid = rem(first, 40) case decode_oid_subids(rest, [second_subid, first_subid]) do {:ok, subids} -> {:ok, Enum.reverse(subids)} error -> error end end defp decode_oid_data(_), do: {:error, :invalid_oid_data} defp decode_oid_subids(<<>>, acc), do: {:ok, acc} defp decode_oid_subids(data, acc) do case decode_oid_subid(data, 0) do {:ok, {subid, rest}} -> decode_oid_subids(rest, [subid | acc]) error -> error end end defp decode_oid_subid(<>, acc) do new_acc = (acc <<< 7) + (byte &&& 0x7F) if (byte &&& 0x80) == 0 do {:ok, {new_acc, rest}} else decode_oid_subid(rest, new_acc) end end defp decode_oid_subid(<<>>, _), do: {:error, :incomplete_oid} defp parse_value_with_type(<<@octet_string, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:octet_string, value, rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@integer, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value_bytes, rest2}} -> {:ok, {:integer, decode_integer_value(value_bytes), rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@null, 0, rest::binary>>) do {:ok, {:null, :null, rest}} end defp parse_value_with_type(<<@object_identifier, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, oid_data, rest2}} -> case decode_oid_data(oid_data) do {:ok, oid_list} -> {:ok, {:object_identifier, oid_list, rest2}} {:error, _} -> {:error, :invalid_oid} end {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@counter32, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:counter32, decode_unsigned_integer(value), rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@gauge32, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:gauge32, decode_unsigned_integer(value), rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@timeticks, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:timeticks, decode_unsigned_integer(value), rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@counter64, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:counter64, decode_counter64(value), rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@ip_address, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:ip_address, value, rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@opaque_type, rest::binary>>) do case parse_ber_length_and_remaining(rest) do {:ok, {_len, value, rest2}} -> {:ok, {:opaque, value, rest2}} {:error, reason} -> {:error, reason} end end defp parse_value_with_type(<<@no_such_object, 0, rest::binary>>) do {:ok, {:no_such_object, nil, rest}} end defp parse_value_with_type(<<@no_such_instance, 0, rest::binary>>) do {:ok, {:no_such_instance, nil, rest}} end defp parse_value_with_type(<<@end_of_mib_view, 0, rest::binary>>) do {:ok, {:end_of_mib_view, nil, rest}} end defp parse_value_with_type(_), do: {:error, :invalid_value} defp decode_integer_value(<>) when byte < 128, do: byte defp decode_integer_value(<>) when byte >= 128, do: byte - 256 defp decode_integer_value(data) do value = :binary.decode_unsigned(data, :big) bit_size = byte_size(data) * 8 if value >= 1 <<< (bit_size - 1) do value - (1 <<< bit_size) else value end end defp decode_unsigned_integer(data) when byte_size(data) <= 4 do :binary.decode_unsigned(data, :big) end defp decode_unsigned_integer(data) when byte_size(data) == 5 do # Handle 5-byte case for large 32-bit unsigned values that require leading zero padding case data do <<0, rest::binary-size(4)>> -> # Leading zero byte for unsigned representation, decode the remaining 4 bytes :binary.decode_unsigned(rest, :big) _ -> # If first byte is not zero, this exceeds 32-bit range 0 end end defp decode_unsigned_integer(_), do: 0 defp decode_counter64(data) when byte_size(data) <= 8 and byte_size(data) > 0 do :binary.decode_unsigned(data, :big) end defp decode_counter64(_), do: 0 end