defmodule SnmpKit.SnmpLib.PDU.Builder do @moduledoc """ High-level PDU and message building functions for SNMP operations. This module provides functions to build various types of SNMP PDUs and messages, including GET, GETNEXT, SET, GETBULK requests, and responses. """ alias SnmpKit.SnmpLib.PDU.Constants alias SnmpKit.SnmpLib.PDU.Decoder @type snmp_version :: Constants.snmp_version() @type pdu_type :: Constants.pdu_type() @type error_status :: Constants.error_status() @type oid :: Constants.oid() @type snmp_value :: Constants.snmp_value() @type varbind :: Constants.varbind() @type pdu :: Constants.pdu() @type message :: Constants.message() @doc """ Builds a GET request PDU. """ @spec build_get_request(oid(), non_neg_integer()) :: pdu() def build_get_request(oid_list, request_id) do validate_request_id!(request_id) normalized_oid = Constants.normalize_oid(oid_list) %{ type: :get_request, request_id: request_id, error_status: Constants.no_error(), error_index: 0, varbinds: [{normalized_oid, :null, :null}] } end @doc """ Builds a GET request PDU with multiple varbinds. """ @spec build_get_request_multi([varbind()], non_neg_integer()) :: pdu() def build_get_request_multi(varbinds, request_id) do validate_request_id!(request_id) case validate_varbinds_format(varbinds) do :ok -> %{ type: :get_request, request_id: request_id, error_status: Constants.no_error(), error_index: 0, varbinds: varbinds } error -> error end end @doc """ Builds a GETNEXT request PDU. """ @spec build_get_next_request(oid(), non_neg_integer()) :: pdu() def build_get_next_request(oid_list, request_id) do validate_request_id!(request_id) normalized_oid = Constants.normalize_oid(oid_list) %{ type: :get_next_request, request_id: request_id, error_status: Constants.no_error(), error_index: 0, varbinds: [{normalized_oid, :null, :null}] } end @doc """ Builds a SET request PDU. """ @spec build_set_request(oid(), {atom(), any()}, non_neg_integer()) :: pdu() def build_set_request(oid_list, {type, value}, request_id) do validate_request_id!(request_id) normalized_oid = Constants.normalize_oid(oid_list) %{ type: :set_request, request_id: request_id, error_status: Constants.no_error(), error_index: 0, varbinds: [{normalized_oid, type, value}] } end @doc """ Builds a GETBULK request PDU for SNMPv2c. ## Parameters - `oid_list`: Starting OID - `request_id`: Request identifier - `non_repeaters`: Number of non-repeating variables (default: 0) - `max_repetitions`: Maximum repetitions (default: 10) """ @spec build_get_bulk_request(oid(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: pdu() def build_get_bulk_request(oid_list, request_id, non_repeaters \\ 0, max_repetitions \\ 10) do validate_request_id!(request_id) validate_bulk_params!(non_repeaters, max_repetitions) normalized_oid = Constants.normalize_oid(oid_list) %{ type: :get_bulk_request, request_id: request_id, error_status: Constants.no_error(), error_index: 0, non_repeaters: non_repeaters, max_repetitions: max_repetitions, varbinds: [{normalized_oid, :null, :null}] } end @doc """ Builds a response PDU. """ @spec build_response(non_neg_integer(), error_status(), non_neg_integer(), [varbind()]) :: pdu() def build_response(request_id, error_status, error_index, varbinds \\ []) do validate_request_id!(request_id) %{ type: :get_response, request_id: request_id, error_status: error_status, error_index: error_index, varbinds: varbinds } end @doc """ Builds an SNMP message structure. ## Parameters - `pdu`: The PDU to include in the message - `community`: Community string - `version`: SNMP version (:v1, :v2c, etc.) """ @spec build_message(pdu(), binary(), snmp_version()) :: message() def build_message(pdu, community, version \\ :v1) do validate_community!(community) validate_bulk_version!(pdu, version) version_number = Constants.normalize_version(version) %{ version: version_number, community: community, pdu: pdu } end @doc """ Creates an error response PDU from a request PDU. ## Examples error_pdu = SnmpKit.SnmpLib.PDU.Builder.create_error_response(request_pdu, 2, 1) """ @spec create_error_response(pdu(), error_status(), non_neg_integer()) :: pdu() def create_error_response(request_pdu, error_status, error_index \\ 0) do # Handle PDU map format - all PDUs are maps with :type field case request_pdu do %{type: _type, request_id: request_id, varbinds: varbinds} -> %{ type: :get_response, request_id: request_id, error_status: error_status, error_index: error_index, varbinds: varbinds } _ -> # Legacy map format for backward compatibility %{ type: :get_response, request_id: Map.get(request_pdu, :request_id, 1), error_status: error_status, error_index: error_index, varbinds: Map.get(request_pdu, :varbinds, []) } end end @doc """ Validates a PDU structure. """ @spec validate(pdu()) :: {:ok, pdu()} | {:error, atom()} def validate(pdu) when is_map(pdu) do # First check if we have a type field case Map.get(pdu, :type) do nil -> {:error, :missing_required_fields} type -> # Validate the type first case validate_pdu_type_only(type) do :ok -> # Now check required fields based on type basic_fields = [:request_id, :varbinds] if Enum.all?(basic_fields, &Map.has_key?(pdu, &1)) do case type do :get_bulk_request -> bulk_fields = [:non_repeaters, :max_repetitions] if Enum.all?(bulk_fields, &Map.has_key?(pdu, &1)) do {:ok, pdu} else {:error, :missing_bulk_fields} end _ -> # Standard PDUs need error_status and error_index standard_fields = [:error_status, :error_index] if Enum.all?(standard_fields, &Map.has_key?(pdu, &1)) do {:ok, pdu} else {:error, :missing_required_fields} end end else {:error, :missing_required_fields} end :error -> {:error, :invalid_pdu_type} end end end def validate(_), do: {:error, :invalid_pdu_format} @doc """ Validates a community string against an encoded SNMP message. """ @spec validate_community(binary(), binary()) :: :ok | {:error, atom()} def validate_community(encoded_message, expected_community) when is_binary(encoded_message) and is_binary(expected_community) do case Decoder.decode_message(encoded_message) do {:ok, %{community: community}} when community == expected_community -> :ok {:ok, %{community: _other}} -> {:error, :invalid_community} {:error, _reason} -> {:error, :decode_failed} end end def validate_community(_encoded, _community), do: {:error, :invalid_parameters} ## Private Implementation # Validation helpers defp validate_request_id!(request_id) do if !(is_integer(request_id) and request_id >= 0 and request_id <= 2_147_483_647) do raise ArgumentError, "Request ID must be a valid integer (0-2147483647), got: #{inspect(request_id)}" end end defp validate_bulk_params!(non_repeaters, max_repetitions) do if !(is_integer(non_repeaters) and non_repeaters >= 0) do raise ArgumentError, "non_repeaters must be a non-negative integer, got: #{inspect(non_repeaters)}" end if !(is_integer(max_repetitions) and max_repetitions >= 0) do raise ArgumentError, "max_repetitions must be a non-negative integer, got: #{inspect(max_repetitions)}" end end defp validate_community!(community) do if !is_binary(community) do raise ArgumentError, "Community must be a binary string, got: #{inspect(community)}" end end defp validate_bulk_version!(pdu, version) do if Map.get(pdu, :type) == :get_bulk_request and version == :v1 do raise ArgumentError, "GETBULK requests require SNMPv2c or higher, cannot use v1" end end defp validate_varbinds_format(varbinds) do valid = Enum.all?(varbinds, fn {oid, _type, _value} when is_list(oid) -> Enum.all?(oid, &is_integer/1) _ -> false end) if valid, do: :ok, else: {:error, :invalid_varbind_format} end # Helper function to validate PDU type defp validate_pdu_type_only(type) do case type do :get_request -> :ok :get_next_request -> :ok :get_response -> :ok :set_request -> :ok :get_bulk_request -> :ok :inform_request -> :ok :snmpv2_trap -> :ok :report -> :ok _ -> :error end end end