defmodule SnmpKit.SnmpMgr.MIB do @moduledoc """ MIB compilation and symbolic name resolution. This module provides MIB compilation using Erlang's :snmpc when available, and includes a built-in registry of standard MIB objects for basic operations. """ use GenServer alias SnmpKit.SnmpLib.MIB alias SnmpKit.SnmpLib.MIB.Parser alias SnmpKit.SnmpLib.OID require Logger @compile {:no_warn_undefined, [:snmpc, :snmp_misc]} @standard_mibs %{ # System group (1.3.6.1.2.1.1) "sysDescr" => [1, 3, 6, 1, 2, 1, 1, 1], "sysObjectID" => [1, 3, 6, 1, 2, 1, 1, 2], "sysUpTime" => [1, 3, 6, 1, 2, 1, 1, 3], "sysContact" => [1, 3, 6, 1, 2, 1, 1, 4], "sysName" => [1, 3, 6, 1, 2, 1, 1, 5], "sysLocation" => [1, 3, 6, 1, 2, 1, 1, 6], "sysServices" => [1, 3, 6, 1, 2, 1, 1, 7], # Interface group (1.3.6.1.2.1.2) "ifNumber" => [1, 3, 6, 1, 2, 1, 2, 1], "ifTable" => [1, 3, 6, 1, 2, 1, 2, 2], "ifEntry" => [1, 3, 6, 1, 2, 1, 2, 2, 1], "ifIndex" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 1], "ifDescr" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 2], "ifType" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 3], "ifMtu" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 4], "ifSpeed" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 5], "ifPhysAddress" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 6], "ifAdminStatus" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 7], "ifOperStatus" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 8], "ifLastChange" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 9], "ifInOctets" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 10], "ifInUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 11], "ifInNUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 12], "ifInDiscards" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 13], "ifInErrors" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 14], "ifInUnknownProtos" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 15], "ifOutOctets" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 16], "ifOutUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 17], "ifOutNUcastPkts" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 18], "ifOutDiscards" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 19], "ifOutErrors" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 20], "ifOutQLen" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 21], "ifSpecific" => [1, 3, 6, 1, 2, 1, 2, 2, 1, 22], # Interface Extensions (ifX) group (1.3.6.1.2.1.31) "ifXTable" => [1, 3, 6, 1, 2, 1, 31, 1], "ifXEntry" => [1, 3, 6, 1, 2, 1, 31, 1, 1], "ifName" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 1], "ifInMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 2], "ifInBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 3], "ifOutMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 4], "ifOutBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 5], "ifHCInOctets" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 6], "ifHCInUcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 7], "ifHCInMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 8], "ifHCInBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 9], "ifHCOutOctets" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 10], "ifHCOutUcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 11], "ifHCOutMulticastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 12], "ifHCOutBroadcastPkts" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 13], "ifLinkUpDownTrapEnable" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 14], "ifHighSpeed" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 15], "ifPromiscuousMode" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 16], "ifConnectorPresent" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 17], "ifAlias" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 18], "ifCounterDiscontinuityTime" => [1, 3, 6, 1, 2, 1, 31, 1, 1, 19], # IP group (1.3.6.1.2.1.4) "ipForwarding" => [1, 3, 6, 1, 2, 1, 4, 1], "ipDefaultTTL" => [1, 3, 6, 1, 2, 1, 4, 2], "ipInReceives" => [1, 3, 6, 1, 2, 1, 4, 3], "ipInHdrErrors" => [1, 3, 6, 1, 2, 1, 4, 4], "ipInAddrErrors" => [1, 3, 6, 1, 2, 1, 4, 5], # SNMP group (1.3.6.1.2.1.11) "snmpInPkts" => [1, 3, 6, 1, 2, 1, 11, 1], "snmpOutPkts" => [1, 3, 6, 1, 2, 1, 11, 2], "snmpInBadVersions" => [1, 3, 6, 1, 2, 1, 11, 3], "snmpInBadCommunityNames" => [1, 3, 6, 1, 2, 1, 11, 4], "snmpInBadCommunityUses" => [1, 3, 6, 1, 2, 1, 11, 5], "snmpInASNParseErrs" => [1, 3, 6, 1, 2, 1, 11, 6], "snmpInTooBigs" => [1, 3, 6, 1, 2, 1, 11, 8], "snmpInNoSuchNames" => [1, 3, 6, 1, 2, 1, 11, 9], "snmpInBadValues" => [1, 3, 6, 1, 2, 1, 11, 10], "snmpInReadOnlys" => [1, 3, 6, 1, 2, 1, 11, 11], "snmpInGenErrs" => [1, 3, 6, 1, 2, 1, 11, 12], "snmpInTotalReqVars" => [1, 3, 6, 1, 2, 1, 11, 13], "snmpInTotalSetVars" => [1, 3, 6, 1, 2, 1, 11, 14], "snmpInGetRequests" => [1, 3, 6, 1, 2, 1, 11, 15], "snmpInGetNexts" => [1, 3, 6, 1, 2, 1, 11, 16], "snmpInSetRequests" => [1, 3, 6, 1, 2, 1, 11, 17], "snmpInGetResponses" => [1, 3, 6, 1, 2, 1, 11, 18], "snmpInTraps" => [1, 3, 6, 1, 2, 1, 11, 19], "snmpOutTooBigs" => [1, 3, 6, 1, 2, 1, 11, 20], "snmpOutNoSuchNames" => [1, 3, 6, 1, 2, 1, 11, 21], "snmpOutBadValues" => [1, 3, 6, 1, 2, 1, 11, 22], "snmpOutGenErrs" => [1, 3, 6, 1, 2, 1, 11, 24], "snmpOutGetRequests" => [1, 3, 6, 1, 2, 1, 11, 25], "snmpOutGetNexts" => [1, 3, 6, 1, 2, 1, 11, 26], "snmpOutSetRequests" => [1, 3, 6, 1, 2, 1, 11, 27], "snmpOutGetResponses" => [1, 3, 6, 1, 2, 1, 11, 28], "snmpOutTraps" => [1, 3, 6, 1, 2, 1, 11, 29], "snmpEnableAuthenTraps" => [1, 3, 6, 1, 2, 1, 11, 30], # Common group prefixes for bulk walking "system" => [1, 3, 6, 1, 2, 1, 1], "interfaces" => [1, 3, 6, 1, 2, 1, 2], "if" => [1, 3, 6, 1, 2, 1, 2], "ifX" => [1, 3, 6, 1, 2, 1, 31], "ip" => [1, 3, 6, 1, 2, 1, 4], "icmp" => [1, 3, 6, 1, 2, 1, 5], "tcp" => [1, 3, 6, 1, 2, 1, 6], "udp" => [1, 3, 6, 1, 2, 1, 7], "snmp" => [1, 3, 6, 1, 2, 1, 11], "mib-2" => [1, 3, 6, 1, 2, 1], "mgmt" => [1, 3, 6, 1, 2], "internet" => [1, 3, 6, 1], # Common enterprise OIDs "enterprises" => [1, 3, 6, 1, 4, 1], "cisco" => [1, 3, 6, 1, 4, 1, 9], "hp" => [1, 3, 6, 1, 4, 1, 11], "3com" => [1, 3, 6, 1, 4, 1, 43], "sun" => [1, 3, 6, 1, 4, 1, 42], "dec" => [1, 3, 6, 1, 4, 1, 36], "ibm" => [1, 3, 6, 1, 4, 1, 2], "microsoft" => [1, 3, 6, 1, 4, 1, 311], "netapp" => [1, 3, 6, 1, 4, 1, 789], "juniper" => [1, 3, 6, 1, 4, 1, 2636], "fortinet" => [1, 3, 6, 1, 4, 1, 12_356], "paloalto" => [1, 3, 6, 1, 4, 1, 25_461], "mikrotik" => [1, 3, 6, 1, 4, 1, 14_988], # Cable/DOCSIS industry OIDs "cablelabs" => [1, 3, 6, 1, 4, 1, 4491], "docsis" => [1, 3, 6, 1, 2, 1, 127], "cableDataPrivateMib" => [1, 3, 6, 1, 4, 1, 4491, 2, 1], "arris" => [1, 3, 6, 1, 4, 1, 4115], "motorola" => [1, 3, 6, 1, 4, 1, 1166], "scientificatlanta" => [1, 3, 6, 1, 4, 1, 1429], "broadcom" => [1, 3, 6, 1, 4, 1, 4413] } # Curated minimal metadata for high-value IF-MIB objects (stopgap until full compiler integration) @curated_syntax %{ # SNMPv2-MIB system group "sysDescr" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "sysObjectID" => %{base: :object_identifier, textual_convention: nil, display_hint: nil}, "sysUpTime" => %{base: :timeticks, textual_convention: nil, display_hint: nil}, "sysContact" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "sysName" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "sysLocation" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "sysServices" => %{base: :integer, textual_convention: nil, display_hint: nil}, # IF-MIB ifTable (1.3.6.1.2.1.2.2.1) "ifIndex" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ifDescr" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "ifType" => %{base: :integer, textual_convention: "IANAifType", display_hint: nil}, "ifMtu" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ifSpeed" => %{base: :gauge32, textual_convention: nil, display_hint: nil}, "ifPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil}, "ifAdminStatus" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ifOperStatus" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ifLastChange" => %{base: :timeticks, textual_convention: nil, display_hint: nil}, "ifInOctets" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInNUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInDiscards" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInErrors" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInUnknownProtos" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutOctets" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutNUcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutDiscards" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutErrors" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutQLen" => %{base: :gauge32, textual_convention: nil, display_hint: nil}, "ifSpecific" => %{base: :object_identifier, textual_convention: nil, display_hint: nil}, # IF-MIB ifXTable (1.3.6.1.2.1.31.1.1.1) "ifName" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "ifInMulticastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifInBroadcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutMulticastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifOutBroadcastPkts" => %{base: :counter32, textual_convention: nil, display_hint: nil}, "ifHCInOctets" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCInUcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCInMulticastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCInBroadcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCOutOctets" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCOutUcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCOutMulticastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifHCOutBroadcastPkts" => %{base: :counter64, textual_convention: nil, display_hint: nil}, "ifLinkUpDownTrapEnable" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ifHighSpeed" => %{base: :gauge32, textual_convention: nil, display_hint: nil}, "ifPromiscuousMode" => %{base: :boolean, textual_convention: "TruthValue", display_hint: nil}, "ifConnectorPresent" => %{base: :boolean, textual_convention: "TruthValue", display_hint: nil}, "ifAlias" => %{base: :octet_string, textual_convention: "DisplayString", display_hint: "255a"}, "ifCounterDiscontinuityTime" => %{base: :timeticks, textual_convention: "TimeStamp", display_hint: nil}, # IP-MIB (ARP table: ipNetToMediaTable 1.3.6.1.2.1.4.22) "ipNetToMediaIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ipNetToMediaPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil}, "ipNetToMediaNetAddress" => %{base: :ip_address, textual_convention: "IpAddress", display_hint: nil}, "ipNetToMediaType" => %{base: :integer, textual_convention: nil, display_hint: nil}, # BRIDGE-MIB (dot1dTpFdbTable and base) "dot1dBaseBridgeAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil}, "dot1dBaseNumPorts" => %{base: :integer, textual_convention: nil, display_hint: nil}, "dot1dBasePortIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil}, "dot1dTpFdbAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil}, "dot1dTpFdbPort" => %{base: :integer, textual_convention: nil, display_hint: nil}, "dot1dTpFdbStatus" => %{base: :integer, textual_convention: nil, display_hint: nil}, # IP-MIB (RFC 4293) modern ARP replacement: ipNetToPhysicalTable # Prefer these over ipNetToMedia* "ipNetToPhysicalIfIndex" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ipNetToPhysicalPhysAddress" => %{base: :octet_string, textual_convention: "PhysAddress", display_hint: nil}, "ipNetToPhysicalNetAddress" => %{base: :octet_string, textual_convention: "InetAddress", display_hint: nil}, "ipNetToPhysicalType" => %{base: :integer, textual_convention: nil, display_hint: nil}, "ipNetToPhysicalLastUpdated" => %{base: :timeticks, textual_convention: "TimeStamp", display_hint: nil}, # Q-BRIDGE-MIB (VLAN-aware FDB) "dot1qTpFdbAddress" => %{base: :octet_string, textual_convention: "MacAddress", display_hint: nil}, "dot1qTpFdbPort" => %{base: :integer, textual_convention: nil, display_hint: nil}, "dot1qTpFdbStatus" => %{base: :integer, textual_convention: nil, display_hint: nil} } ## Public API @doc """ Starts the MIB registry GenServer. """ def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Compiles a MIB file using SnmpKit.SnmpLib.MIB pure Elixir implementation. Enhanced to use SnmpKit.SnmpLib.MIB for improved compilation with better error handling. ## Examples iex> SnmpKit.SnmpMgr.MIB.compile("SNMPv2-MIB.mib") {:ok, "SNMPv2-MIB.bin"} iex> SnmpKit.SnmpMgr.MIB.compile("nonexistent.mib") {:error, :file_not_found} """ def compile(mib_file, opts \\ []) do # Try SnmpKit.SnmpLib.MIB first for enhanced compilation case compile_with_snmp_lib(mib_file, opts) do {:ok, result} -> {:ok, result} {:error, :snmp_lib_not_available} -> {:error, :snmp_lib_not_available} {:error, reason} -> {:error, reason} end end @doc """ Compiles all MIB files in a directory using enhanced SnmpKit.SnmpLib.MIB capabilities. """ def compile_dir(directory, opts \\ []) do # Try SnmpKit.SnmpLib.MIB.compile_all first for enhanced batch compilation if File.exists?(directory) do case compile_all_with_snmp_lib(directory, opts) do {:ok, results} -> {:ok, results} {:error, :snmp_lib_not_available} -> # Fallback to individual file compilation compile_dir_fallback(directory, opts) {:error, reason} -> {:error, reason} end else {:error, {:directory_error, :enoent}} end end @doc """ Returns enriched MIB metadata for an object by name or OID. Input may be a dotted OID string (with or without instance), an OID list, or a base name (optionally with an instance suffix like "ifDescr.6"). Returns a map with at least: name (base symbol), base oid, and optional instance fields when input includes an instance. Includes curated syntax metadata for a subset of high-value IF-MIB objects as a stopgap until full compiler metadata is wired in. """ @spec object_info(String.t() | [integer]) :: {:ok, map()} | {:error, term()} def object_info(name_or_oid) do with {:ok, input_oid} <- normalize_to_oid_list(name_or_oid), {:ok, base_name, _maybe_index} <- base_name_and_index(input_oid), {:ok, base_oid} <- name_to_oid(base_name) do # Prefer compiled/parsed metadata when available compiled_meta = GenServer.call(__MODULE__, {:get_metadata, base_name}) syntax = case compiled_meta do %{syntax_base: base} = m -> %{base: base, textual_convention: Map.get(m, :textual_convention), display_hint: Map.get(m, :display_hint)} _ -> syntax_for(base_name) end base_map = %{ name: base_name, module: module_for(base_name), oid: base_oid, syntax: syntax } enriched = maybe_put_instance(base_map, input_oid, base_oid) # Optionally add access/status/description if we have compiled metadata enriched = case compiled_meta do nil -> enriched m -> enriched |> maybe_put(:access, Map.get(m, :access)) |> maybe_put(:status, Map.get(m, :status)) |> maybe_put(:description, Map.get(m, :description)) end {:ok, enriched} end end @doc """ Alias for object_info/1 to match proposal naming. """ @spec reverse_lookup_enriched(String.t() | [integer]) :: {:ok, map()} | {:error, term()} def reverse_lookup_enriched(name_or_oid), do: object_info(name_or_oid) @doc """ Batch variant of object_info/1. Returns {:ok, list_of_maps} or {:error, reason} if any lookup fails. """ @spec object_info_many([String.t() | [integer]]) :: {:ok, [map()]} | {:error, term()} def object_info_many(list) when is_list(list) do results = Enum.map(list, &object_info/1) case Enum.find(results, fn {:error, _} -> true _ -> false end) do {:error, reason} -> {:error, reason} _ -> {:ok, Enum.map(results, fn {:ok, m} -> m end)} end end @doc """ Parses a MIB file to extract object definitions using SnmpKit.SnmpLib.MIB.Parser. This provides enhanced MIB analysis without requiring compilation. ## Examples iex> SnmpKit.SnmpMgr.MIB.parse_mib_file("SNMPv2-MIB.mib") {:ok, %{objects: [...], imports: [...], exports: [...]}} """ def parse_mib_file(mib_file, opts \\ []) do case File.read(mib_file) do {:ok, content} -> parse_mib_content(content, opts) {:error, reason} -> {:error, {:file_read_error, reason}} end end @doc """ Parses MIB content string using SnmpKit.SnmpLib.MIB.Parser. ## Examples iex> content = "sysDescr OBJECT-TYPE SYNTAX DisplayString ACCESS read-only STATUS mandatory" iex> SnmpKit.SnmpMgr.MIB.parse_mib_content(content) {:ok, %{tokens: [...], parsed_objects: [...]}} """ def parse_mib_content(content, opts \\ []) when is_binary(content) do # Use SnmpKit.SnmpLib.MIB.Parser for enhanced parsing case Parser.tokenize(content) do {:ok, tokens} -> {:ok, objects} = parse_tokens_to_objects(tokens, opts) {:ok, %{ tokens: tokens, parsed_objects: objects, parser: :snmp_lib_enhanced }} {:error, reason} -> {:error, {:tokenization_failed, reason}} end end @doc """ Loads a compiled MIB file using SnmpKit.SnmpLib.MIB.load_compiled with fallback. """ def load(compiled_mib_path) do # Try SnmpKit.SnmpLib.MIB.load_compiled first for enhanced loading case load_with_snmp_lib(compiled_mib_path) do {:ok, result} -> GenServer.call(__MODULE__, {:register_loaded_mib, result}) {:error, :snmp_lib_not_available} -> GenServer.call(__MODULE__, {:load_mib, compiled_mib_path}) {:error, reason} -> {:error, reason} end end @doc """ Enhanced MIB object resolution with parsed MIB data integration. Returns enriched object information including OID, syntax, module, and more. Leverages both standard MIBs and any loaded/parsed MIB files for comprehensive resolution. ## Examples iex> SnmpKit.SnmpMgr.MIB.resolve_enhanced("sysDescr") {:ok, %{name: "sysDescr", oid: [1, 3, 6, 1, 2, 1, 1, 1], module: "SNMPv2-MIB", syntax: %{...}}} iex> SnmpKit.SnmpMgr.MIB.resolve_enhanced("sysDescr.0") {:ok, %{name: "sysDescr", oid: [1, 3, 6, 1, 2, 1, 1, 1], instance_oid: [1, 3, 6, 1, 2, 1, 1, 1, 0], ...}} """ def resolve_enhanced(name, _opts \\ []) do # Use object_info for enriched resolution object_info(name) end @doc """ Loads and parses a MIB file, integrating it into the name resolution system. This combines compilation/loading with parsing for comprehensive MIB support. """ def load_and_integrate_mib(mib_file, opts \\ []) do with {:ok, compiled} <- compile(mib_file, opts), :ok <- GenServer.call(__MODULE__, {:integrate_mib_data, mib_file, compiled}) do {:ok, compiled} end end @doc """ Loads standard MIBs that are built into the library. """ def load_standard_mibs do GenServer.call(__MODULE__, :load_standard_mibs) end @doc """ Resolves a symbolic name to an OID. ## Examples iex> SnmpKit.SnmpMgr.MIB.resolve("sysDescr.0") {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} iex> SnmpKit.SnmpMgr.MIB.resolve("sysDescr") {:ok, [1, 3, 6, 1, 2, 1, 1, 1]} iex> SnmpKit.SnmpMgr.MIB.resolve("unknownName") {:error, :not_found} """ def resolve(name) do GenServer.call(__MODULE__, {:resolve, name}) end @doc """ Performs reverse lookup from OID to symbolic name. ## Examples iex> SnmpKit.SnmpMgr.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1, 0]) {:ok, "sysDescr.0"} iex> SnmpKit.SnmpMgr.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1]) {:ok, "sysDescr"} """ def reverse_lookup(oid) when is_list(oid) do GenServer.call(__MODULE__, {:reverse_lookup, oid}) end def reverse_lookup(oid_string) when is_binary(oid_string) do case OID.string_to_list(oid_string) do {:ok, oid_list} -> reverse_lookup(oid_list) error -> error end end @doc """ Gets the children of an OID node. """ def children(oid) do GenServer.call(__MODULE__, {:children, oid}) end @doc """ Gets the parent of an OID node. """ def parent([_ | _] = oid) when is_list(oid) do {:ok, Enum.drop(oid, -1)} end def parent([]), do: {:error, :no_parent} def parent(oid_string) when is_binary(oid_string) do case OID.string_to_list(oid_string) do {:ok, oid_list} -> parent(oid_list) error -> error end end @doc """ Walks the MIB tree starting from a root OID. """ def walk_tree(root_oid, opts \\ []) do GenServer.call(__MODULE__, {:walk_tree, root_oid, opts}) end ## GenServer Implementation @impl true def init(_opts) do # Initialize with standard MIBs reverse_map = build_reverse_map(@standard_mibs) state = %{ name_to_oid: @standard_mibs, oid_to_name: reverse_map, name_to_meta: %{}, loaded_mibs: [:standard] } {:ok, state} end @impl true def handle_call({:resolve, name}, _from, state) do result = resolve_name(name, state.name_to_oid) {:reply, result, state} end @impl true def handle_call({:reverse_lookup, oid}, _from, state) do result = reverse_lookup_oid(oid, state.oid_to_name) {:reply, result, state} end @impl true def handle_call({:children, oid}, _from, state) do result = find_children(oid, state.name_to_oid) {:reply, result, state} end @impl true def handle_call({:walk_tree, root_oid, _opts}, _from, state) do result = walk_tree_from_root(root_oid, state.name_to_oid) {:reply, result, state} end @impl true def handle_call(:load_standard_mibs, _from, state) do # Standard MIBs are already loaded in init {:reply, :ok, state} end @impl true def handle_call({:register_loaded_mib, mib_data}, _from, state) do # Register MIB data loaded via SnmpKit.SnmpLib.MIB.load_compiled new_state = merge_snmp_lib_mib_data(state, mib_data) {:reply, :ok, new_state} end @impl true def handle_call({:get_metadata, base_name}, _from, state) do meta = Map.get(state.name_to_meta, base_name) {:reply, meta, state} end @impl true def handle_call({:resolve_enhanced, name, _opts}, _from, state) do # Enhanced resolution using loaded MIB data result = resolve_with_loaded_mibs(name, state) {:reply, result, state} end @impl true def handle_call({:integrate_mib_data, mib_file, parsed_data}, _from, state) do # Integrate both compiled and parsed MIB data new_state = integrate_parsed_mib_data(state, mib_file, parsed_data) {:reply, :ok, new_state} end def handle_call({:bulk_register, mappings}, _from, state) do # Bulk register pre-compiled MIB mappings (from JSON files) merged_name_to_oid = Map.merge(state.name_to_oid, mappings) merged_oid_to_name = build_reverse_map(merged_name_to_oid) new_state = state |> Map.put(:name_to_oid, merged_name_to_oid) |> Map.put(:oid_to_name, merged_oid_to_name) {:reply, :ok, new_state} end ## Private Functions defp compile_with_snmp_lib(mib_file, opts) do case MIB.compile(mib_file, opts) do {:ok, result} -> {:ok, result} {:error, reason} -> {:error, {:snmp_lib_compilation_failed, reason}} end rescue UndefinedFunctionError -> {:error, :snmp_lib_not_available} end defp compile_all_with_snmp_lib(directory, opts) do case File.ls(directory) do {:ok, files} -> mib_files = files |> Enum.filter(&String.ends_with?(&1, ".mib")) |> Enum.map(&Path.join(directory, &1)) case MIB.compile_all(mib_files, opts) do {:ok, results} -> {:ok, results} {:error, reason} -> {:error, {:snmp_lib_batch_compilation_failed, reason}} end {:error, reason} -> {:error, {:directory_error, reason}} end rescue UndefinedFunctionError -> {:error, :snmp_lib_not_available} end defp extract_name_to_oid_from_symbols(symbols) when is_map(symbols) do Enum.reduce(symbols, %{}, fn {name, defn}, acc -> extract_single_name_to_oid(name, defn, acc) end) end defp extract_single_name_to_oid(name, %{} = defn, acc) do case Map.get(defn, :oid) do nil -> acc oid_any -> maybe_add_normalized_oid(name, oid_any, acc) end end defp extract_single_name_to_oid(_name, _defn, acc), do: acc defp maybe_add_normalized_oid(name, oid_any, acc) do case normalize_parsed_oid(oid_any) do {:ok, oid_list} -> Map.put(acc, name, oid_list) _ -> acc end end defp extract_meta_from_symbols(symbols) when is_map(symbols) do Enum.reduce(symbols, %{}, fn {name, defn}, acc -> extract_single_meta(name, defn, acc) end) end defp extract_single_meta(name, %{__type__: :object_type} = defn, acc) do meta = build_meta_from_object_type(defn) Map.put(acc, name, meta) end defp extract_single_meta(_name, _defn, acc), do: acc defp build_meta_from_object_type(defn) do syntax_any = Map.get(defn, :syntax) %{ syntax_base: syntax_base_from(syntax_any), textual_convention: textual_convention_from(syntax_any), display_hint: nil, access: Map.get(defn, :max_access), status: Map.get(defn, :status), description: Map.get(defn, :description) } end defp compile_dir_fallback(directory, opts) do case File.ls(directory) do {:ok, files} -> mib_files = Enum.filter(files, &String.ends_with?(&1, ".mib")) results = Enum.map(mib_files, fn file -> file_path = Path.join(directory, file) {file, compile(file_path, opts)} end) {:ok, results} {:error, reason} -> {:error, {:directory_error, reason}} end end defp parse_tokens_to_objects(tokens, _opts) do # Extract OBJECT-TYPE definitions from tokens objects = extract_object_definitions(tokens) {:ok, objects} end defp extract_object_definitions(tokens) do # Simple object extraction - can be enhanced further tokens |> Enum.chunk_every(3, 1, :discard) |> Enum.filter(fn [{:atom, _, name}, {:"OBJECT-TYPE", _}, _] -> %{name: name, type: :object} _ -> false end) |> Enum.map(fn [{:atom, _, name}, {:"OBJECT-TYPE", _}, _] -> %{name: name, type: :object_type} end) end defp load_with_snmp_lib(compiled_mib_path) do case MIB.load_compiled(compiled_mib_path) do {:ok, result} -> {:ok, result} {:error, reason} -> {:error, {:snmp_lib_load_failed, reason}} end rescue UndefinedFunctionError -> {:error, :snmp_lib_not_available} end # Derive base syntax from parsed syntax term defp syntax_base_from(syntax) do case syntax do atom when is_atom(atom) -> syntax_atom_to_base(atom) {type, _} when type in [:integer, :octet_string, :object_identifier] -> type {:type, t} when is_atom(t) -> syntax_type_to_base(t) _ -> nil end end # Map simple syntax atoms to base types defp syntax_atom_to_base(atom) do base_types = [ :integer, :octet_string, :object_identifier, :timeticks, :counter32, :counter64, :gauge32, :ip_address ] if atom in base_types, do: atom end # Handle typed syntax conversions defp syntax_type_to_base(type) do case type do :"octet string" -> :octet_string :"object identifier" -> :object_identifier other -> other end end # Best-effort textual convention detection from syntax term defp textual_convention_from(syntax) do case syntax do {:type, t} when is_atom(t) -> Atom.to_string(t) _ -> nil end end defp merge_snmp_lib_mib_data(state, mib_data) do # Accept either compiled format with :symbols or a parsed map with name_to_oid/name_to_meta {add_map, add_meta} = cond do is_map(mib_data) and Map.has_key?(mib_data, :symbols) -> symbols = Map.get(mib_data, :symbols, %{}) {extract_name_to_oid_from_symbols(symbols), extract_meta_from_symbols(symbols)} is_map(mib_data) and Map.has_key?(mib_data, :name_to_oid) -> raw = Map.get(mib_data, :name_to_oid, %{}) meta = Map.get(mib_data, :name_to_meta, %{}) {normalize_name_to_oid(raw), meta} true -> {%{}, %{}} end merged_name_to_oid = Map.merge(state.name_to_oid, add_map) merged_oid_to_name = build_reverse_map(merged_name_to_oid) merged_name_to_meta = Map.merge(state.name_to_meta, add_meta) state |> Map.put(:name_to_oid, merged_name_to_oid) |> Map.put(:oid_to_name, merged_oid_to_name) |> Map.put(:name_to_meta, merged_name_to_meta) |> Map.update(:snmp_lib_mibs, [mib_data], fn list -> [mib_data | list] end) end defp resolve_with_loaded_mibs(name, state) do case Map.get(state, :name_to_oid) do %{} = m when is_binary(name) -> case Map.get(m, name) do nil -> {:error, :not_found} oid -> {:ok, oid} end _ -> {:error, :not_found} end end defp integrate_parsed_mib_data(state, mib_file, compiled_data) do # Read the MIB file content for workaround mib_content = case File.read(mib_file) do {:ok, content} -> content _ -> "" end # Extract OID mappings from compiled MIB symbols mib_mappings = extract_mib_mappings_from_compiled(compiled_data, state, mib_content) # Merge extracted OIDs into state merged_name_to_oid = Map.merge(state.name_to_oid, mib_mappings.name_to_oid) merged_oid_to_name = build_reverse_map(merged_name_to_oid) merged_name_to_meta = Map.merge(state.name_to_meta, mib_mappings.name_to_meta) # Also store raw compiled data integrated_mibs = Map.get(state, :integrated_mibs, %{}) new_integrated = Map.put(integrated_mibs, mib_file, compiled_data) state |> Map.put(:name_to_oid, merged_name_to_oid) |> Map.put(:oid_to_name, merged_oid_to_name) |> Map.put(:name_to_meta, merged_name_to_meta) |> Map.put(:integrated_mibs, new_integrated) end # Extract OID mappings from compiled MIB data (symbols format) defp extract_mib_mappings_from_compiled(compiled_data, state, mib_content) do symbols = Map.get(compiled_data, :symbols, %{}) # Convert symbols to definitions format definitions = Enum.map(symbols, fn {name, symbol_data} -> Map.put(symbol_data, :name, name) end) # Reuse existing extraction logic extract_mib_mappings(%{definitions: definitions}, state, mib_content) end defp resolve_name(name, _name_to_oid_map) when is_nil(name) or not is_binary(name) do {:error, :invalid_name} end defp resolve_name(name, name_to_oid_map) do case Map.get(name_to_oid_map, name) do nil -> resolve_name_with_instance(name, name_to_oid_map) oid -> {:ok, oid} end end defp resolve_name_with_instance(name, name_to_oid_map) do if String.contains?(name, ".") do parse_name_with_instance(name, name_to_oid_map) else {:error, :not_found} end end defp parse_name_with_instance(name, name_to_oid_map) do [base_name | instance_parts] = String.split(name, ".") with {:ok, base_oid} <- get_base_oid(base_name, name_to_oid_map), {:ok, instance_oids} <- parse_instance_parts(instance_parts) do {:ok, base_oid ++ instance_oids} end end defp get_base_oid(base_name, name_to_oid_map) do case Map.get(name_to_oid_map, base_name) do nil -> {:error, :not_found} oid -> {:ok, oid} end end defp parse_instance_parts(parts) do parts |> Enum.reduce_while([], fn part, acc -> case Integer.parse(part) do {int, ""} -> {:cont, [int | acc]} _ -> {:halt, :error} end end) |> case do :error -> {:error, :invalid_instance} oids -> {:ok, Enum.reverse(oids)} end end defp reverse_lookup_oid(oid, oid_to_name_map) do case Map.get(oid_to_name_map, oid) do nil -> # Try to find a partial match find_partial_reverse_match(oid, oid_to_name_map) name -> # Exact match - return as-is (already includes any suffix in the map) {:ok, name} end end defp find_partial_reverse_match(oid, oid_to_name_map) do # Handle case where oid might be a string instead of list if is_binary(oid) do {:error, :invalid_oid_format} else # Handle empty list case if Enum.empty?(oid) do {:error, :empty_oid} else # Try progressively shorter OIDs to find a base match find_partial_match(oid, oid_to_name_map, length(oid) - 1) end end end defp find_partial_match(_oid, _map, length) when length <= 0, do: {:error, :not_found} defp find_partial_match(oid, oid_to_name_map, length) do partial_oid = Enum.take(oid, length) case Map.get(oid_to_name_map, partial_oid) do nil -> find_partial_match(oid, oid_to_name_map, length - 1) base_name -> # Found a base match - append the remaining OID elements as the index suffix base = strip_instance_suffix(base_name) suffix = Enum.drop(oid, length) case suffix do [] -> {:ok, base} _ -> {:ok, base <> "." <> Enum.join(suffix, ".")} end end end defp find_children(parent_oid, name_to_oid_map) do case normalize_parent_oid_for_children(parent_oid) do {:ok, normalized_oid} -> children = filter_and_collect_children(normalized_oid, name_to_oid_map) {:ok, children} {:error, reason} -> {:error, reason} end end # Normalize parent OID to list format defp normalize_parent_oid_for_children(nil), do: {:ok, []} defp normalize_parent_oid_for_children(parent_oid) when is_list(parent_oid), do: {:ok, parent_oid} defp normalize_parent_oid_for_children(parent_oid) when is_binary(parent_oid) do case OID.string_to_list(parent_oid) do {:ok, oid_list} -> {:ok, oid_list} {:error, _} -> {:error, :invalid_parent_oid} end end defp normalize_parent_oid_for_children(_), do: {:error, :invalid_parent_oid} # Filter and collect direct children of parent OID defp filter_and_collect_children(normalized_oid, name_to_oid_map) do name_to_oid_map |> Enum.filter(fn {_name, oid} -> direct_child_of?(oid, normalized_oid) end) |> Enum.map(fn {name, _oid} -> name end) |> Enum.sort() end # Check if oid is a direct child of parent_oid defp direct_child_of?(oid, parent_oid) when is_list(oid) and is_list(parent_oid) do length(oid) == length(parent_oid) + 1 and List.starts_with?(oid, parent_oid) end defp direct_child_of?(_oid, _parent_oid), do: false defp walk_tree_from_root(root_oid, name_to_oid_map) do root_oid = cond do is_binary(root_oid) -> case OID.string_to_list(root_oid) do {:ok, oid_list} -> oid_list {:error, _} -> [] end is_list(root_oid) -> root_oid is_nil(root_oid) -> [] true -> [] end descendants = name_to_oid_map |> Enum.filter(fn {_name, oid} -> is_list(oid) and List.starts_with?(oid, root_oid) end) |> Enum.map(fn {name, oid} -> {name, oid} end) |> Enum.sort_by(fn {_name, oid} -> oid end) {:ok, descendants} end defp build_reverse_map(name_to_oid_map) do Map.new(name_to_oid_map, fn {name, oid} -> {oid, name} end) end # Normalize any dotted instance suffix from a name like "ifDescr.1" -> "ifDescr" defp strip_instance_suffix(name) when is_binary(name) do case String.split(name, ".", parts: 2) do [base] -> base [base, _rest] -> base end end defp strip_instance_suffix(other), do: other defp normalize_to_oid_list(oid_any) when is_list(oid_any) do case OID.valid_oid?(oid_any) do :ok -> {:ok, oid_any} error -> error end end defp normalize_to_oid_list(oid_any) when is_binary(oid_any) do if numeric_oid_string?(oid_any) do OID.string_to_list(oid_any) else resolve_named_oid(oid_any) end end defp normalize_to_oid_list(_), do: {:error, :invalid_input} defp numeric_oid_string?(str) do String.contains?(str, ".") and String.match?(str, ~r/^\.?\d+(?:\.\d+)*$/) end defp resolve_named_oid(oid_str) do case String.split(oid_str, ".", parts: 2) do [base] -> resolve(base) [base, instance_str] -> resolve_with_instance(base, instance_str) end end defp resolve_with_instance(base, instance_str) do with {:ok, base_oid} <- resolve(base), {:ok, instance_index} <- parse_instance(instance_str) do {:ok, base_oid ++ instance_index} end end defp parse_instance(instance_str) do parts = String.split(instance_str, ".") try do ints = Enum.map(parts, fn p -> case Integer.parse(p) do {i, ""} -> i _ -> throw(:bad) end end) {:ok, ints} catch :bad -> {:error, :invalid_instance} end end defp base_name_and_index(oid_list) do with {:ok, name_with_index} <- reverse_lookup(oid_list), base_name = strip_instance_suffix(name_with_index), {:ok, base_oid} <- name_to_oid(base_name) do extract_index_from_oid(oid_list, base_name, base_oid) end end defp extract_index_from_oid(oid_list, base_name, base_oid) do base_len = length(base_oid) if length(oid_list) > base_len do {:ok, base_name, Enum.drop(oid_list, base_len)} else {:ok, base_name, nil} end end defp name_to_oid(name) when is_binary(name) do case resolve(name) do {:ok, oid} -> {:ok, oid} error -> error end end defp maybe_put_instance(map, input_oid, base_oid) do base_len = length(base_oid) if length(input_oid) > base_len do instance = Enum.drop(input_oid, base_len) instance_index = case instance do [i] -> i list -> list end map |> Map.put(:instance_index, instance_index) |> Map.put(:instance_oid, input_oid) else map end end defp syntax_for(base_name) do Map.get(@curated_syntax, base_name, %{base: nil, textual_convention: nil, display_hint: nil}) end defp maybe_put(map, _key, nil), do: map defp maybe_put(map, key, val), do: Map.put(map, key, val) defp module_for(base_name) do # Map of prefix patterns to MIB modules # Order matters: more specific prefixes before general ones prefix_to_module = [ {"ifHC", "IF-MIB"}, {"ifIn", "IF-MIB"}, {"ifOut", "IF-MIB"}, {"if", "IF-MIB"}, {"ipNetToPhysical", "IP-MIB"}, {"ipNetToMedia", "IP-MIB"}, {"sys", "SNMPv2-MIB"}, {"dot1d", "BRIDGE-MIB"}, {"dot1q", "Q-BRIDGE-MIB"} ] Enum.find_value(prefix_to_module, fn {prefix, module} -> if String.starts_with?(base_name, prefix), do: module end) end # Normalize name->oid map from arbitrary representations defp normalize_name_to_oid(raw) when is_map(raw) do Enum.reduce(raw, %{}, fn {name, oid_any}, acc -> case normalize_parsed_oid(oid_any) do {:ok, oid_list} -> Map.put(acc, name, oid_list) _ -> acc end end) end # Convert parsed OID representation to a flat integer list when possible defp normalize_parsed_oid(oid) when is_list(oid) do if Enum.all?(oid, &is_integer/1) do {:ok, oid} else parse_complex_oid_elements(oid) end end # Handle tuple format: {parent_name, index_binary} # This format is used by the MIB parser for OID references defp normalize_parsed_oid({parent_name, index_binary}) when is_atom(parent_name) and is_binary(index_binary) do # Try to resolve the parent OID from current state parent_str = Atom.to_string(parent_name) case resolve(parent_str) do {:ok, parent_oid} when is_list(parent_oid) -> # Parse the index as an integer case :binary.decode_unsigned(index_binary) do index when is_integer(index) -> {:ok, parent_oid ++ [index]} _ -> {:error, :invalid_index} end _ -> # Parent not resolved yet - will be handled in second pass {:error, :unresolved_parent} end end defp normalize_parsed_oid(_), do: {:error, :invalid_oid} # Handle lists like [%{value: 1}, %{value: 3}, ...] possibly with names defp parse_complex_oid_elements(oid) do vals = Enum.map(oid, &normalize_oid_element/1) if Enum.any?(vals, &(&1 == :error)) do {:error, :unresolved_oid} else {:ok, Enum.map(vals, fn {:ok, i} -> i end)} end end # Normalize a single OID element to an integer defp normalize_oid_element(%{value: v}) when is_integer(v), do: {:ok, v} defp normalize_oid_element(%{value: v}) when is_binary(v) do case Integer.parse(v) do {i, ""} -> {:ok, i} _ -> :error end end defp normalize_oid_element(v) when is_integer(v), do: {:ok, v} defp normalize_oid_element(_), do: :error defp extract_mib_mappings(mib_data, state, mib_content) do # Extract name-to-OID mappings and basic metadata from parsed MIB data definitions = Map.get(mib_data, :definitions, []) tc_map = build_tc_map(definitions) # Build a map of name to OID definition for resolution oid_defs = build_oid_definition_map(definitions) # Workaround for parser bug: manually extract definitions with missing sub_index oid_defs = fix_missing_sub_indices(oid_defs, mib_content) # Resolve OIDs recursively, using existing state for parent resolution name_to_oid_map = resolve_all_oids(oid_defs, state.name_to_oid) # Build metadata for object types name_to_meta = build_metadata_map(definitions, tc_map) %{name_to_oid: name_to_oid_map, name_to_meta: name_to_meta} end # Workaround for parser bug: extract OBJECT-IDENTIFIER definitions with missing sub_index # The parser fails to extract sub_index when the value is 10 (newline character) defp fix_missing_sub_indices(oid_defs, mib_content) do # Find entries with nil sub_index but parent reference missing_indices = oid_defs |> Enum.filter(fn {_name, oid_def} -> case oid_def do {_parent, nil} -> true _ -> false end end) |> Enum.map(fn {name, _} -> name end) # For each missing entry, try to extract from raw MIB content Enum.reduce(missing_indices, oid_defs, fn name, acc -> # Pattern: name OBJECT IDENTIFIER ::= { parent index } regex = ~r/#{name}\s+OBJECT\s+IDENTIFIER\s+::=\s+\{\s+(\w+)\s+(\d+)\s+\}/ case Regex.run(regex, mib_content) do [_, parent, index_str] -> index = String.to_integer(index_str) # Convert to the same format as other indices (binary with the integer as byte) parent_atom = String.to_atom(parent) Map.put(acc, name, {parent_atom, <>}) _ -> acc end end) end # Build textual convention map from definitions defp build_tc_map(definitions) do definitions |> Enum.filter(&(Map.get(&1, :__type__) == :textual_convention)) |> Enum.reduce(%{}, fn tc, acc -> tc_name = Map.get(tc, :name) tc_syntax = Map.get(tc, :syntax) display_hint = Map.get(tc, :display_hint) Map.put(acc, tc_name, %{syntax_base: syntax_base_from(tc_syntax), display_hint: display_hint}) end) end # Build a map of name to OID definition for resolution defp build_oid_definition_map(definitions) do definitions |> Enum.filter(fn def -> type = Map.get(def, :__type__) type in [:object_type, :object_identifier, :module_identity] end) |> Enum.reduce(%{}, fn def, acc -> name = Map.get(def, :name) oid = Map.get(def, :oid) parent = Map.get(def, :parent) sub_index = Map.get(def, :sub_index) cond do # Object has explicit OID (e.g., {:parentName, <>}) name && oid -> Map.put(acc, name, oid) # OBJECT-IDENTIFIER with parent and sub_index name && parent && sub_index -> # Convert parent/sub_index to the same format as OID tuples parent_atom = String.to_atom(parent) Map.put(acc, name, {parent_atom, sub_index}) # OBJECT-IDENTIFIER with parent but missing sub_index (parser bug) # Mark with nil so fix_missing_sub_indices can handle it name && parent -> parent_atom = String.to_atom(parent) Map.put(acc, name, {parent_atom, nil}) # No usable data true -> acc end end) end # Resolve all OIDs recursively defp resolve_all_oids(oid_defs, existing_oids) do # Start with existing OIDs from state and iterate until all resolvable OIDs are resolved resolve_oids_iteratively(oid_defs, existing_oids, 10) end defp resolve_oids_iteratively(_oid_defs, resolved, 0) do # Max iterations reached, return what we have resolved end defp resolve_oids_iteratively(oid_defs, resolved, iterations_left) do {new_resolved, remaining} = Enum.reduce(oid_defs, {resolved, %{}}, fn {name, oid_def}, {res_acc, rem_acc} -> case resolve_single_oid(oid_def, res_acc) do {:ok, oid_list} -> {Map.put(res_acc, name, oid_list), rem_acc} {:error, _reason} -> {res_acc, Map.put(rem_acc, name, oid_def)} end end) if map_size(remaining) == map_size(oid_defs) do # No progress made, stop iterating new_resolved else # Continue with remaining unresolved OIDs resolve_oids_iteratively(remaining, new_resolved, iterations_left - 1) end end defp resolve_single_oid(oid_def, _resolved) when is_list(oid_def) do # Already a list of integers case oid_def do [] -> {:error, :empty_oid} _ -> if Enum.all?(oid_def, &is_integer/1) do {:ok, oid_def} else {:error, :not_resolved} end end end defp resolve_single_oid({parent_name, index_binary}, resolved) when is_atom(parent_name) and is_binary(index_binary) do parent_str = Atom.to_string(parent_name) case Map.get(resolved, parent_str) do parent_oid when is_list(parent_oid) -> # Decode the index - MIB parser uses UTF-8 character for numeric values index = decode_mib_index(index_binary) {:ok, parent_oid ++ [index]} _ -> {:error, :parent_not_resolved} end end defp resolve_single_oid(_oid_def, _resolved), do: {:error, :invalid_format} # Decode MIB index from binary # The parser represents integers as UTF-8 characters where the code point is the value defp decode_mib_index(binary) when is_binary(binary) do case String.to_charlist(binary) do [code_point | _] -> code_point [] -> :binary.decode_unsigned(binary) end end # Build metadata map for object types defp build_metadata_map(definitions, tc_map) do definitions |> Enum.filter(&(Map.get(&1, :__type__) == :object_type)) |> Enum.reduce(%{}, fn def, acc -> name = Map.get(def, :name) if name do meta = build_object_type_meta(def, tc_map) Map.put(acc, name, meta) else acc end end) end # Build metadata for an object type definition defp build_object_type_meta(defn, tc_map) do syntax_any = Map.get(defn, :syntax) {syntax_base, textual_convention, display_hint} = resolve_syntax_metadata(syntax_any, tc_map) %{ syntax_base: syntax_base, textual_convention: textual_convention, display_hint: display_hint, access: Map.get(defn, :max_access), status: Map.get(defn, :status), description: Map.get(defn, :description) } end # Resolve syntax metadata, looking up textual conventions defp resolve_syntax_metadata(syntax_any, tc_map) when is_atom(syntax_any) do primitives = MapSet.new([ :integer, :octet_string, :object_identifier, :timeticks, :counter32, :counter64, :gauge32, :ip_address ]) if MapSet.member?(primitives, syntax_any) do {syntax_base_from(syntax_any), textual_convention_from(syntax_any), nil} else lookup_tc_metadata(syntax_any, tc_map) end end defp resolve_syntax_metadata(syntax_any, _tc_map) do {syntax_base_from(syntax_any), textual_convention_from(syntax_any), nil} end # Look up textual convention metadata defp lookup_tc_metadata(syntax_atom, tc_map) do tc_key = Atom.to_string(syntax_atom) case Map.get(tc_map, tc_key) do %{syntax_base: base, display_hint: hint} -> {base, tc_key, hint} _ -> {syntax_base_from(syntax_atom), textual_convention_from(syntax_atom), nil} end end end