Current section
Files
Jump to
Current section
Files
lib/snmpkit/snmp_sim/oid_tree.ex
defmodule SnmpKit.SnmpSim.OIDTree do
@moduledoc """
Optimized OID tree for fast lookups and lexicographic traversal.
Supports GETNEXT operations and GETBULK bulk retrieval.
Uses a radix tree structure optimized for OID operations with:
- Fast O(log n) lookups and insertions
- Efficient lexicographic traversal for GETNEXT
- Memory-efficient storage for 10K+ OIDs
- Optimized bulk operations
"""
defstruct [
# Root node of the OID tree
:root,
# Number of OIDs in the tree
:size,
# Cached sorted OID list for traversal
:sorted_oids
]
# Tree node structure
defmodule Node do
@moduledoc false
defstruct [
# This part of the OID (integer)
:oid_part,
# Value for this OID (if it's a leaf)
:value,
# Behavior configuration for this OID
:behavior,
# Map of child nodes
:children,
# Whether this node represents a complete OID
:is_leaf
]
end
@doc """
Create a new empty OID tree.
## Examples
tree = SnmpKit.SnmpSim.OIDTree.new()
"""
def new() do
%__MODULE__{
root: %Node{oid_part: nil, children: %{}, is_leaf: false},
size: 0,
sorted_oids: []
}
end
@doc """
Insert an OID with its value and behavior information into the tree.
Maintains lexicographic ordering for efficient GETNEXT operations.
## Examples
tree = SnmpKit.SnmpSim.OIDTree.new()
tree = SnmpKit.SnmpSim.OIDTree.insert(tree, "1.3.6.1.2.1.1.1.0", "System Description", nil)
"""
def insert(%__MODULE__{} = tree, oid_string, value, behavior_info \\ nil) do
oid_parts = parse_oid(oid_string)
{new_root, inserted} = insert_node(tree.root, oid_parts, value, behavior_info)
new_size = if inserted, do: tree.size + 1, else: tree.size
# Mark sorted OIDs as stale (will be rebuilt on next traversal)
%{tree | root: new_root, size: new_size, sorted_oids: nil}
end
@doc """
Get the value for an exact OID match.
## Examples
{:ok, value, behavior} = SnmpKit.SnmpSim.OIDTree.get(tree, "1.3.6.1.2.1.1.1.0")
:not_found = SnmpKit.SnmpSim.OIDTree.get(tree, "1.3.6.1.2.1.1.1.999")
"""
def get(%__MODULE__{} = tree, oid_string) do
oid_parts = parse_oid(oid_string)
get_node(tree.root, oid_parts)
end
@doc """
Get the next OID in lexicographic order (GETNEXT operation).
Returns the next OID after the given OID, or :end_of_mib if no more OIDs exist.
## Examples
{:ok, next_oid, value, behavior} = SnmpKit.SnmpSim.OIDTree.get_next(tree, "1.3.6.1.2.1.1.1.0")
:end_of_mib = SnmpKit.SnmpSim.OIDTree.get_next(tree, "1.3.6.1.9.9.9.9.9")
"""
def get_next(%__MODULE__{} = tree, oid_string) do
# Ensure we have a sorted OID list for traversal
tree = ensure_sorted_oids(tree)
target_oid_parts = parse_oid(oid_string)
case find_next_oid(tree.sorted_oids, target_oid_parts) do
nil ->
:end_of_mib
next_oid_string ->
case get(tree, next_oid_string) do
{:ok, value, behavior} -> {:ok, next_oid_string, value, behavior}
:not_found -> :end_of_mib
end
end
end
@doc """
Perform a bulk walk operation starting from the given OID.
Used for GETBULK operations with proper handling of non-repeaters and max-repetitions.
## Examples
results = SnmpKit.SnmpSim.OIDTree.bulk_walk(tree, "1.3.6.1.2.1.2.2.1", 10, 0)
"""
def bulk_walk(%__MODULE__{} = tree, start_oid, max_repetitions, non_repeaters \\ 0) do
# Ensure we have a sorted OID list for traversal
tree = ensure_sorted_oids(tree)
start_oid_parts = parse_oid(start_oid)
# Find starting position in sorted list
start_index = find_start_index(tree.sorted_oids, start_oid_parts)
# Collect results up to max_repetitions
collect_bulk_results(tree, start_index, max_repetitions, non_repeaters)
end
@doc """
Get all OIDs in the tree in lexicographic order.
Useful for debugging and full tree traversal.
## Examples
oids = SnmpKit.SnmpSim.OIDTree.list_oids(tree)
"""
def list_oids(%__MODULE__{} = tree) do
tree = ensure_sorted_oids(tree)
tree.sorted_oids
end
@doc """
Get the size of the tree (number of OIDs).
## Examples
size = SnmpKit.SnmpSim.OIDTree.size(tree)
"""
def size(%__MODULE__{} = tree), do: tree.size
@doc """
Check if the tree is empty.
## Examples
empty? = SnmpKit.SnmpSim.OIDTree.empty?(tree)
"""
def empty?(%__MODULE__{} = tree), do: tree.size == 0
# Private helper functions
defp parse_oid(oid_string) when is_binary(oid_string) do
oid_string
|> String.trim_leading(".")
|> String.split(".")
|> Enum.map(&String.to_integer/1)
end
defp insert_node(%Node{} = node, [], value, behavior) do
# Reached the end of OID parts - this node becomes a leaf
inserted = not node.is_leaf
updated_node = %{node | value: value, behavior: behavior, is_leaf: true}
{updated_node, inserted}
end
defp insert_node(%Node{} = node, [part | rest], value, behavior) do
child = Map.get(node.children, part, %Node{oid_part: part, children: %{}, is_leaf: false})
{updated_child, inserted} = insert_node(child, rest, value, behavior)
updated_children = Map.put(node.children, part, updated_child)
updated_node = %{node | children: updated_children}
{updated_node, inserted}
end
defp get_node(%Node{} = node, []) do
if node.is_leaf do
{:ok, node.value, node.behavior}
else
:not_found
end
end
defp get_node(%Node{} = node, [part | rest]) do
case Map.get(node.children, part) do
nil -> :not_found
child_node -> get_node(child_node, rest)
end
end
defp ensure_sorted_oids(%__MODULE__{sorted_oids: nil} = tree) do
sorted_oids = build_sorted_oids(tree.root, [])
%{tree | sorted_oids: sorted_oids}
end
defp ensure_sorted_oids(%__MODULE__{} = tree), do: tree
defp build_sorted_oids(%Node{} = node, current_path) do
# Collect this node if it's a leaf
this_oids =
if node.is_leaf do
[Enum.join(current_path, ".")]
else
[]
end
# Collect all child OIDs in sorted order
child_oids =
node.children
# Sort by OID part numerically
|> Enum.sort_by(fn {part, _} -> part end)
|> Enum.flat_map(fn {part, child_node} ->
build_sorted_oids(child_node, current_path ++ [part])
end)
this_oids ++ child_oids
end
defp find_next_oid(sorted_oids, target_oid_parts) do
_target_oid_string = Enum.join(target_oid_parts, ".")
sorted_oids
|> Enum.find(fn oid_string ->
compare_oids(parse_oid(oid_string), target_oid_parts) == :gt
end)
end
defp find_start_index(sorted_oids, start_oid_parts) do
sorted_oids
|> Enum.find_index(fn oid_string ->
compare_oids(parse_oid(oid_string), start_oid_parts) == :gt
end) || length(sorted_oids)
end
defp collect_bulk_results(%__MODULE__{} = tree, start_index, max_repetitions, _non_repeaters) do
available_oids = Enum.drop(tree.sorted_oids, start_index)
# Take up to max_repetitions OIDs (considering non_repeaters)
# For now, simplified to just take max_repetitions
oids_to_fetch = Enum.take(available_oids, max_repetitions)
# Fetch values for each OID
Enum.map(oids_to_fetch, fn oid_string ->
case get(tree, oid_string) do
{:ok, value, behavior} -> {oid_string, value, behavior}
:not_found -> {oid_string, nil, nil}
end
end)
end
# Compare two OID part lists lexicographically
defp compare_oids([], []), do: :eq
defp compare_oids([], _), do: :lt
defp compare_oids(_, []), do: :gt
defp compare_oids([a | rest_a], [b | rest_b]) when a == b do
compare_oids(rest_a, rest_b)
end
defp compare_oids([a | _], [b | _]) when a < b, do: :lt
defp compare_oids([a | _], [b | _]) when a > b, do: :gt
end