Expand description
SNMP MIB parsing, resolution, query, and tooling APIs.
§What is a MIB?
A MIB (Management Information Base) is a text file that describes the
structure of data available from an SNMP-managed device. Each piece of
data (a counter, a name, a status flag, a table row) is identified by an
OID (Object Identifier), a dotted-decimal path like 1.3.6.1.2.1.1.1.
MIB files give those numeric OIDs human-readable names, types, and
descriptions, so instead of 1.3.6.1.2.1.1.1 you can say sysDescr.
MIBs are written in a language called SMI (Structure of Management Information), which has two versions: SMIv1 (RFC 1155/1212) and SMIv2 (RFC 2578/2579/2580). This crate handles both transparently.
§API Layers
Most callers should use the handle API: start with Loader,
get a Mib, and navigate the resolved model through borrowed
handle types (Node, Object, Type, Module,
Notification, Group, Compliance, Capability).
Handles wrap an arena ID and a &Mib reference. Methods on
handles return further handles, so typical usage looks like
object.ty()?.effective_base() without touching IDs directly.
The handle API covers OID resolution, type chain introspection,
table/index navigation, module iteration, diagnostics, and
everything else documented in the sections below.
Every handle exposes its arena ID via .id(). IDs are
Copy + Eq + Hash + Ord, so you can store them in collections
for deduplication or cross-referencing, then convert back to
handles with mib.*_by_id() when you need to query again.
Mib also has query methods that work with IDs directly:
Mib::modules_defining and Mib::modules_importing find
modules by symbol name, Mib::objects_by_base_type and
Mib::objects_by_type_name filter objects by type, and
Mib::available_symbols returns everything visible in a
module’s scope (own definitions plus resolved imports).
§Raw data access
Mib::raw() returns a RawMib view that
exposes the arena-backed data records directly. This is useful
when you need things the handle API doesn’t surface:
- Per-clause source spans on
ObjectDataandTypeData(e.g.syntax_span,access_span) for pointing diagnostics at specific clauses. - Import metadata (
ModuleData::is_import_used,ModuleData::import_source). - Symbolic OID references via
oid_refs()on entity records. - Bulk arena slices (
raw.*_slice()) for batch analysis.
See the raw module and the raw example.
§Compiler pipeline
The ast, parser, lower, ir, and token
modules expose pre-resolution stages for callers that need
syntax-aware analysis before full resolution. The parser
produces partial ASTs from broken input, which matters for
editor integration where the user is mid-edit. Token types
carry classification predicates for syntax highlighting.
See the compile module and the tokens example.
§Loading MIBs
Use Loader to configure sources, select modules, and run the pipeline:
use mib_rs::{BaseType, Loader};
fn example_mib() -> mib_rs::Mib {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
r#"DOC-EXAMPLE-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
FROM SNMPv2-SMI
TEXTUAL-CONVENTION, DisplayString
FROM SNMPv2-TC;
docExampleMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "Example module used in crate docs."
::= { enterprises 99999 }
DocName ::= TEXTUAL-CONVENTION
DISPLAY-HINT "255a"
STATUS current
DESCRIPTION "Example display string type."
SYNTAX DisplayString (SIZE (0..255))
docScalars OBJECT IDENTIFIER ::= { docExampleMib 1 }
docTables OBJECT IDENTIFIER ::= { docExampleMib 2 }
docDeviceName OBJECT-TYPE
SYNTAX DocName
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A scalar object."
::= { docScalars 1 }
docTable OBJECT-TYPE
SYNTAX SEQUENCE OF DocEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Example table."
::= { docTables 1 }
docEntry OBJECT-TYPE
SYNTAX DocEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Example row."
INDEX { docIndex }
::= { docTable 1 }
DocEntry ::= SEQUENCE {
docIndex Integer32,
docDescr DisplayString
}
docIndex OBJECT-TYPE
SYNTAX Integer32 (1..2147483647)
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Example index."
::= { docEntry 1 }
docDescr OBJECT-TYPE
SYNTAX DisplayString
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Example column."
::= { docEntry 2 }
END
"#,
);
Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("example MIB should load")
}
let mib = example_mib();
let object = mib.object("docDeviceName").expect("object should exist");
let ty = object.ty().expect("object should have a type");
assert_eq!(object.name(), "docDeviceName");
assert_eq!(ty.name(), "DocName");
assert_eq!(ty.effective_base(), BaseType::OctetString);
assert_eq!(ty.effective_display_hint(), "255a");§OIDs and Resolution
Every named element in a MIB has an OID, a path through a global tree
shared by all SNMP devices. OIDs are written as dotted decimal
(1.3.6.1.2.1.1.1) or symbolically (sysDescr). The tree is
hierarchical: enterprises is 1.3.6.1.4.1, and a vendor’s subtree
hangs beneath that.
Instance OIDs extend a base OID with a suffix that identifies a
specific value. For a scalar like sysDescr, the instance is always
sysDescr.0. For table columns, the suffix encodes the row’s index
values, e.g. ifDescr.7 for interface 7.
This crate resolves both directions: name to numeric OID, and numeric OID back to its closest named node.
fn example_mib() -> mib_rs::Mib {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
);
mib_rs::Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("example MIB should load")
}
let mib = example_mib();
let column_oid = mib.resolve_oid("docDescr").expect("OID should resolve");
assert_eq!(column_oid.to_string(), "1.3.6.1.4.1.99999.2.1.1.2");
let node = mib
.exact_node_by_oid(&column_oid)
.expect("exact node should exist");
assert_eq!(node.name(), "docDescr");
let instance_node = mib
.resolve_node("docDescr.7")
.expect("instance OID should resolve to its base node");
assert_eq!(instance_node.name(), "docDescr");
let instance_oid = mib.resolve_oid("docDescr.7").expect("instance OID should resolve");
assert_eq!(instance_oid.to_string(), "1.3.6.1.4.1.99999.2.1.1.2.7");
assert_eq!(mib.lookup_oid(&instance_oid).name(), "docDescr");
assert_eq!(mib.lookup_oid(&"1.3.6.1.4.1.99999.2.1.1.2.99".parse().unwrap()).name(), "docDescr");§Tables and Indexes
SNMP models tabular data as three nested objects:
- A table (
SEQUENCE OF) is a container, not directly readable. - A row (entry) represents one row, also not directly readable. It declares which columns are index columns, whose values together form the instance suffix that identifies each row.
- Columns are the actual readable/writable values. Each column’s full OID is the column OID plus the index suffix.
For example, ifTable contains ifEntry rows indexed by ifIndex.
The column ifDescr for interface 7 has OID ifDescr.7 (i.e.
the column’s base OID with the index value 7 appended).
§AUGMENTS
Some rows use AUGMENTS instead of INDEX. An augmenting row
extends another table’s rows with additional columns, sharing the
same index structure. For example, ifXEntry AUGMENTS ifEntry
adds columns like ifHighSpeed to each ifEntry row, using the
same ifIndex to identify rows. Use Object::augments to find
the target row and Object::augmented_by to find extending rows.
Object::effective_indexes follows the augment chain
automatically, returning the inherited index list.
§Index encoding
Each index component has an IndexEncoding that describes how
its value maps to OID sub-identifiers in the instance suffix.
Integer indexes use a single sub-identifier. Fixed-length strings
(with a single-value SIZE constraint) use one sub-identifier per
octet. Variable-length strings are length-prefixed. The IMPLIED
keyword omits the length prefix, relying on the index being the
last component. Index::encoding returns the derived encoding.
Use mib::index::decode_suffix to decode raw OID suffix arcs
into typed IndexValues, or call OidLookup::decode_indexes
for the common case of processing a varbind OID.
Use Object::is_table, Object::is_row, Object::is_column,
and Object::is_scalar to distinguish these, or use the filtered
iterators like Mib::tables and Mib::scalars.
fn example_mib() -> mib_rs::Mib {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
);
mib_rs::Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("example MIB should load")
}
let mib = example_mib();
let table = mib.object("docTable").expect("table should exist");
let row = table.row().expect("table should have a row");
let column_names: Vec<_> = table.columns().map(|col| col.name()).collect();
assert_eq!(column_names, vec!["docIndex", "docDescr"]);
let indexes: Vec<_> = row.effective_indexes().collect();
assert_eq!(indexes.len(), 1);
assert_eq!(indexes[0].row().name(), "docEntry");
let index_object = indexes[0].object().expect("index object");
let index_type = indexes[0].ty().expect("index type");
assert_eq!(index_object.name(), "docIndex");
assert_eq!(indexes[0].name(), "docIndex");
assert_eq!(index_type.name(), "Integer32");§Modules
A MIB file contains one module (e.g. IF-MIB, SNMPv2-MIB). Modules
import symbols from other modules, so loading one module typically
pulls in its dependencies automatically.
§Base modules
Seven base modules are built into the library and always available:
| Module | SMI version | Defines |
|---|---|---|
SNMPv2-SMI | SMIv2 | Core types (Integer32, Counter32, etc.), OID roots (internet, enterprises, mib-2), macros (MODULE-IDENTITY, OBJECT-TYPE, NOTIFICATION-TYPE, OBJECT-IDENTITY) |
SNMPv2-TC | SMIv2 | TEXTUAL-CONVENTION macro, standard TCs (DisplayString, TruthValue, RowStatus, etc.) |
SNMPv2-CONF | SMIv2 | Conformance macros (MODULE-COMPLIANCE, OBJECT-GROUP, NOTIFICATION-GROUP, AGENT-CAPABILITIES) |
RFC1155-SMI | SMIv1 | SMIv1 base types and OID roots |
RFC1065-SMI | SMIv1 | Earlier SMIv1 base (predecessor to RFC1155-SMI) |
RFC-1212 | SMIv1 | SMIv1 OBJECT-TYPE macro definition |
RFC-1215 | SMIv1 | SMIv1 TRAP-TYPE macro definition |
These modules define the SMI language itself, specifically the ASN.1
macros (OBJECT-TYPE, MODULE-IDENTITY, TEXTUAL-CONVENTION, etc.)
that all other MIB modules use. The library constructs them
programmatically rather than parsing them from files, because they
contain ASN.1 MACRO definitions that require a general ASN.1 macro
parser to process. Since RFC 2578 Section 3 explicitly prohibits
user-defined macros in MIB modules (“Additional ASN.1 macros must not
be defined in SMIv2 information modules”), the library only needs to
handle the fixed set of macros defined by the SMI RFCs.
Implications for users:
- No files needed: You do not need to supply these modules as source files. If they exist on disk in a source directory, the synthetic versions take priority and the files are not parsed.
- Always present: Base modules are included in every loaded
Mib, even if nothing imports them. UseModule::is_baseto distinguish them from user-supplied modules (e.g. when iterating modules). - No source spans: Definitions from base modules carry synthetic
span values (
Span::SYNTHETIC) rather than real byte offsets, since there is no parsed source text. Thesource_pathfor base modules is empty. - Included in iteration:
Mib::modules,Mib::objects,Mib::types, andMib::nodesall include base module content. Filter withModule::is_basewhen you only want user-supplied definitions. Module-scoped iterators (e.g.module.objects()) are naturally limited to a single module.
§OID ownership
Several base modules define overlapping OID trees. For example, both
RFC1155-SMI (SMIv1) and SNMPv2-SMI (SMIv2) define internet,
enterprises, and other well-known roots. When multiple modules
register the same OID, the resolver determines which module “owns”
the node using these tiebreakers, in order:
- Base modules take priority over user modules.
- SMIv2 modules are preferred over SMIv1.
- Among modules with the same SMI version, newer
LAST-UPDATEDtimestamps win. - Lexicographic module name as a final deterministic fallback.
In practice this means SNMPv2-SMI owns nodes like enterprises
even though RFC1155-SMI also defines them. Node::module returns
the winning module. Both modules still function normally for imports,
so SMIv1 MIBs that IMPORTS ... FROM RFC1155-SMI continue to work.
Use Module handles to scope lookups and iteration to a single
module:
fn example_mib() -> mib_rs::Mib {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
);
mib_rs::Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("example MIB should load")
}
let mib = example_mib();
let module = mib.module("DOC-EXAMPLE-MIB").expect("module should exist");
assert_eq!(module.object("docDeviceName").unwrap().module(), Some(module));
let object_names: Vec<_> = module.objects().map(|obj| obj.name()).collect();
assert!(object_names.contains(&"docTable"));
assert!(object_names.contains(&"docDescr"));
let type_names: Vec<_> = module.types().map(|ty| ty.name()).collect();
assert!(type_names.contains(&"DocName"));§Notifications and Conformance
Beyond objects and types, SMI defines several constructs for event reporting and conformance testing:
-
NOTIFICATION-TYPE (SMIv2) / TRAP-TYPE (SMIv1) - defines an asynchronous event an agent can send. Each notification lists the objects it carries as payload via its OBJECTS clause. SMIv1 traps additionally carry an enterprise OID and trap number. See
NotificationandNotification::objects. -
OBJECT-GROUP / NOTIFICATION-GROUP - bundles related objects or notifications into a named set. Groups are the unit of conformance: a compliance statement says “you must implement these groups”. See
GroupandGroup::members. -
MODULE-COMPLIANCE - declares which groups a compliant implementation must support, with optional per-object refinements that can narrow syntax or access requirements. See
Compliance. -
AGENT-CAPABILITIES - declares what an actual agent implementation supports, including which groups it includes and any per-object variations (restricted syntax, different defaults). See
Capability.
These are less commonly needed than objects and types, but matter
for MIB validation tooling, compliance checking, and understanding
which objects are required vs optional. The notifications example
demonstrates querying all four.
§Query Formats
Once a MIB is loaded, you can look up nodes and OIDs using several
formats. Qualified names (MODULE::name) are useful when multiple
modules define the same name. Mib::resolve_oid,
Mib::resolve_node, and RawMib::resolve all accept these forms:
| Form | Example | Description |
|---|---|---|
| Plain name | sysDescr | Looks up by object/node name across all modules |
| Qualified name | SNMPv2-MIB::sysDescr | Scoped to a specific module |
| Instance OID | ifDescr.7 | Name with numeric suffix appended |
| Numeric OID | 1.3.6.1.2.1.1.1 | Dotted decimal, leading dot optional |
For instance OIDs (both symbolic and numeric), Mib::resolve_node returns
the deepest matching tree node, while Mib::resolve_oid returns the full
numeric OID with the suffix included.
Mib::format_oid converts a numeric Oid back to MODULE::name.suffix
form using longest-prefix matching.
§Sources
Sources tell the loader where to find MIB files. For testing and
embedding, use in-memory sources. For production use, point at
directories on disk or use system path auto-discovery to find MIBs
installed by net-snmp or libsmi. The source module has several
constructors:
| Constructor | Description |
|---|---|
source::file() | Single file on disk, module name auto-detected |
source::files() | Multiple files on disk, module names auto-detected |
source::dir | Recursively indexes a directory tree on disk |
source::dirs | Chains multiple directory trees |
source::memory | Single in-memory module (for tests or embedding) |
source::memory_modules | Multiple in-memory modules |
source::chain | Combines multiple sources; first match wins |
Loader::system_paths auto-discovers
net-snmp and libsmi MIB directories from config files and environment
variables (see searchpath).
Module names are derived from file content (scanning for DEFINITIONS
headers), not from filenames. Files are matched by extension using
source::DEFAULT_EXTENSIONS (.mib, .smi, .txt, .my, or no
extension).
§Type Introspection
SMI types form parent chains. A MIB might define HostName as a
refinement of DisplayString, which is itself a textual convention
over OCTET STRING. Each link in the chain can add constraints
(size limits, value ranges), a display hint (how to render the value
as text), or enumeration labels.
A textual convention (TC) is the standard way to define reusable
types in SMIv2 (RFC 2579). A TC wraps a base type with a name,
description, and optional DISPLAY-HINT and constraints. For example,
DisplayString is a TC over OCTET STRING (SIZE (0..255)) with
display hint "255a". Use Type::is_textual_convention to check
whether a type was defined as a TC.
§Constraints: SIZE vs range
Both Type::sizes and Type::ranges return &[Range], but
they constrain different things:
- SIZE constrains the length (in octets) of string-like types
(
OCTET STRING,Opaque). Example:SIZE (0..255)means at most 255 bytes. - Range constrains the numeric value of integer-like types.
Example:
(1..2147483647)means the value must be at least 1.
The effective_* variants walk the parent chain to find inherited
constraints.
§Display hints
A DISPLAY-HINT string (RFC 2579, Section 3) tells a MIB browser or SNMP tool how to render a raw value as human-readable text. Common examples:
"255a"- up to 255 ASCII characters (used byDisplayString)"1x:"- hex bytes separated by colons (used byMacAddress)"2d-1d-1d,1d:1d:1d.1d"- date-time components (used byDateAndTime)
Type::effective_display_hint and
Object::effective_display_hint return the hint string.
Object::format_integer, Object::format_octets, and
Object::scale_integer apply the hint directly to raw values.
The display_hint module exposes the same
formatting functions for use without an Object handle.
§Direct vs effective accessors
Each Type handle exposes two families of accessors:
- Direct (
base,display_hint,enums,sizes,ranges) - return only what this specific type declares. These are empty/None if the type inherits everything from its parent. - Effective (
effective_base,effective_display_hint,effective_enums,effective_sizes,effective_ranges) - walk up the parent chain and return the first non-empty value. These give you the “resolved” answer.
In most cases, use the effective_* methods. They give you the
answer you actually want: “what base type does this ultimately
represent?”, “how should I format this value?”, “what are the valid
enum labels?”. The direct methods are mainly useful when you need to
know exactly where in the chain a property was introduced, for
instance when building a MIB browser that shows the full type
derivation.
| Method | Description |
|---|---|
Type::base | Directly assigned base type (may be Unknown for derived types) |
Type::effective_base | Resolved base type - use this one |
Type::parent | Immediate parent type (if derived) |
Type::display_hint | This type’s own DISPLAY-HINT (often empty) |
Type::effective_display_hint | First non-empty hint in the chain - use this one |
Type::enums | This type’s own enum values |
Type::effective_enums | First non-empty enums in the chain - use this one |
Type::sizes / Type::ranges | This type’s own constraints |
Type::effective_sizes / Type::effective_ranges | Inherited constraints - use these |
Type::is_textual_convention | Whether defined as a TEXTUAL-CONVENTION |
Convenience predicates: Type::is_counter, Type::is_gauge,
Type::is_string, Type::is_enumeration, Type::is_bits.
These all use the effective base type internally.
Objects expose the same effective accessors directly (e.g.
Object::effective_display_hint, Object::effective_enums) so
you don’t need to go through the type handle for common lookups.
§Diagnostics and Configuration
Real-world MIB files frequently contain errors, vendor-specific
extensions, or references to modules you don’t have. Rather than
failing on the first problem, this library collects diagnostics and
continues, producing as much useful output as possible. After
loading, check Mib::has_errors and inspect Mib::diagnostics
for details.
There are two independent knobs that control loading behavior. They can seem redundant at first (“don’t both of them control how strict loading is?”), but they operate at different levels.
Think of it like a Rust analogy: ResolverStrictness is like
controlling how use imports are resolved. In Rust, use foo::Bar
must name the exact path. In MIBs, imports work similarly - a module
declares IMPORTS DisplayString FROM SNMPv2-TC. But many real-world
MIBs get this wrong: they import from the wrong module, forget to
import well-known names they assume are built-in, or don’t declare
imports at all. ResolverStrictness controls how hard the resolver
tries to recover from these mistakes, from following only explicit
import chains (Strict) to searching all loaded modules by name
(Permissive).
DiagnosticConfig, on the other hand, is like compiler warnings
and -Werror. It controls what gets reported across the entire
pipeline (lexing, parsing, and resolution), and whether problems
cause load() to fail. It doesn’t change what gets resolved.
The key tradeoff with ResolverStrictness is correctness vs
completeness. The more permissive you go, the more things get
resolved, but the higher the risk of incorrect results. At
Permissive, the resolver falls back to searching all loaded
modules for a matching symbol name. If multiple modules define a
symbol with the same name, you’re essentially guessing which one
was intended. At Strict, the resolver only follows deterministic
strategies where the source is unambiguous (explicit imports,
import forwarding chains, ASN.1 primitives), so resolved symbols
are traceable back to their origin.
§ResolverStrictness - what the resolver attempts
ResolverStrictness controls how aggressively the resolver tries
to recover when it can’t find a symbol through explicit imports. Set
via Loader::resolver_strictness.
| Level | Behavior | Correctness risk | When to use |
|---|---|---|---|
Strict | Minimal fallbacks. Only deterministic strategies that don’t guess the source module. | Lowest - if it resolves, the import chain is traceable. | Validating MIBs for correctness, linting, CI checks. |
Normal (default) | Constrained fallbacks: searches well-known base modules for unimported types and OID roots, and resolves module name aliases. | Low - fallbacks are limited to safe, unambiguous cases. | General use. Handles sloppy imports that are obviously resolvable. |
Permissive | All fallbacks, including searching every loaded module for a symbol by name. | Higher - if two modules define FooStatus, the resolver picks one. | Loading badly-written vendor MIBs that you can’t fix. |
§Specific behaviors by level
All levels (including Strict):
-
Direct import resolution (symbol found in the named source module).
-
Import forwarding: MIB authors often import a symbol from a module that uses it, not realizing that module doesn’t define the symbol - it imports it from somewhere else. SMI imports are not transitive (importing from a module only gives you what that module defines, not what it imports), but many MIB authors treat them as if they were, similar to how programmers sometimes confuse which scope a variable is visible in.
For example, suppose
ACME-TCdefines a textual conventionAcmeStatus, andACME-MIBimports and uses it:ACME-TC DEFINITIONS ::= BEGIN AcmeStatus ::= TEXTUAL-CONVENTION ... END ACME-MIB DEFINITIONS ::= BEGIN IMPORTS AcmeStatus FROM ACME-TC; -- uses AcmeStatus in OBJECT-TYPE definitions ENDA third module might then mistakenly import
AcmeStatusfromACME-MIBinstead of fromACME-TC:ACME-EXTENSION-MIB DEFINITIONS ::= BEGIN IMPORTS AcmeStatus FROM ACME-MIB; -- wrong: ACME-MIB doesn't define it ENDThe resolver handles this by checking
ACME-MIB’s own IMPORTS, finding that it declaresAcmeStatus FROM ACME-TC, and following that chain. This is deterministic - the intermediate module explicitly names its source - so it is enabled at all strictness levels. -
Partial import resolution: when a source module has some but not all of the requested symbols, the ones that exist are resolved individually and the rest are reported as unresolved.
-
ASN.1 primitive type fallback:
INTEGER,OCTET STRING,OBJECT IDENTIFIER, andBITSalways resolve from SNMPv2-SMI even without an explicit import. -
Well-known OID roots:
iso,ccitt, andjoint-iso-ccittalways resolve to their fixed arc values.
Normal and Permissive (constrained fallbacks):
-
Module name aliases: maps alternate module names to their canonical form (e.g.
SNMPv2-SMI-v1toSNMPv2-SMI,RFC-1213toRFC1213-MIB). These aliases exist because modules have been renamed over time as RFCs were revised, and some vendors use non-standard names in their IMPORTS. -
Unimported well-known symbol fallback: names like
enterprises,Counter64, andDisplayStringfeel like built-in language keywords, but they’re actually defined in specific base modules (SNMPv2-SMI,SNMPv2-TC, etc.) and formally need to be imported. Many MIB authors skip the import, treating these names as globally available:ACME-MIB DEFINITIONS ::= BEGIN IMPORTS MODULE-IDENTITY, OBJECT-TYPE FROM SNMPv2-SMI; -- no import for enterprises or Counter64 acmeMib MODULE-IDENTITY ... ::= { enterprises 12345 } acmeCounter OBJECT-TYPE SYNTAX Counter64 -- not imported ... ENDWhen a type or OID parent is not found via imports, the resolver searches the well-known base modules (SNMPv2-SMI, RFC1155-SMI, SNMPv2-TC). This is limited to those specific modules, so there is no ambiguity about which definition is meant.
-
TRAP-TYPE enterprise global lookup: the ENTERPRISE reference in TRAP-TYPE definitions is searched across all modules when not found via imports.
Permissive only (global fallbacks):
- Global object lookup: INDEX objects, AUGMENTS targets, NOTIFICATION-TYPE OBJECTS members, and DEFVAL object references are searched across all loaded modules when not found via imports.
- Global group/compliance member lookup: OBJECT-GROUP members, MODULE-COMPLIANCE mandatory groups, and AGENT-CAPABILITIES variation targets are searched globally.
Which should I use? Start with Normal (the default). If you
get unresolved-reference diagnostics, it’s usually better to fix
the MIB file directly (correcting the IMPORTS statement to name
the right source module) rather than reaching for Permissive.
MIB files are plain text, and import fixes are usually obvious from
the diagnostic message. Reserve Permissive for cases where you
can’t modify the MIB files, such as vendor-supplied MIBs loaded
from a read-only path. Strict is useful for validation tooling
or CI, where you want broken imports to surface as unresolved
references rather than being silently fixed up.
§DiagnosticConfig - what gets reported
DiagnosticConfig controls which diagnostics are collected and
which severity level causes load() to fail. This is purely about
reporting - it does not change what the resolver does. Set via
Loader::diagnostic_config.
It has four preset constructors:
| Preset | What’s reported | load() fails at | When to use |
|---|---|---|---|
DiagnosticConfig::verbose() | Everything (style, info, warnings) | Severe | Debugging MIB issues, understanding what the resolver did. |
DiagnosticConfig::default() | Minor and above | Severe | General use. |
DiagnosticConfig::quiet() | Errors and above only | Severe | Production code that just wants to know about real problems. |
DiagnosticConfig::silent() | Nothing | Fatal only | When you don’t care about diagnostics at all and want load() to succeed unless something is truly broken. |
Which should I use? The default is fine for most cases. Use
quiet() in production if you don’t want to surface minor issues
to users. Use silent() when loading untrusted or messy vendor
MIBs where you just want whatever data you can get. Use verbose()
when diagnosing why something isn’t resolving correctly.
§Combining the two
Since strictness controls resolution behavior and diagnostics controls reporting across the whole pipeline, they can be mixed freely:
Normal+default()- good general-purpose defaults.Permissive+silent()- maximum tolerance. Tries every fallback, suppresses all diagnostics, only fails on fatal errors. Good for loading a pile of vendor MIBs where you want whatever data you can get. Be aware that some resolved symbols may be incorrect due to ambiguous fallback matches.Strict+verbose()- maximum strictness. Minimal fallbacks, all diagnostics reported (including parse warnings and style issues). Good for validating MIBs you author.Normal+quiet()- reasonable for a production SNMP tool that loads user-provided MIBs. Safe fallbacks, but only real errors are surfaced.
§Fine-tuning
For more control, DiagnosticConfig also supports:
fail_at- change which severity causesload()to return an error. For example, set toSeverity::Minorto fail on any minor issue.overrides- promote or demote specific diagnostic codes (e.g. turn a warning into an error).ignore- glob patterns to suppress specific diagnostic codes entirely (e.g."import-*"to ignore all import-related diagnostics).
§Feature Flags
| Feature | Default | Description |
|---|---|---|
serde | yes | Serde support and JSON export via export |
cli | yes | CLI binary (mib-rs) |
§Examples
Runnable examples live in the examples/ directory. Each one can be run
with cargo run --example <name>.
§Basic usage
Load a MIB from memory, query objects, and display module metadata.
//! Load a MIB from memory, query objects, and display module metadata.
use mib_rs::{BaseType, Loader};
fn main() {
// Load from an in-memory MIB source.
let source = mib_rs::source::memory(
"MY-MIB",
r#"MY-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
FROM SNMPv2-SMI
DisplayString
FROM SNMPv2-TC;
myMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example Corp"
CONTACT-INFO "support@example.com"
DESCRIPTION "A basic example module."
REVISION "202603120000Z"
DESCRIPTION "Initial version."
::= { enterprises 99999 }
myScalars OBJECT IDENTIFIER ::= { myMib 1 }
myName OBJECT-TYPE
SYNTAX DisplayString (SIZE (0..255))
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A name."
::= { myScalars 1 }
myCount OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A counter."
::= { myScalars 2 }
END
"#,
);
let mib = Loader::new()
.source(source)
.modules(["MY-MIB"])
.load()
.expect("should load");
// -- Module metadata --
let module = mib.module("MY-MIB").expect("module exists");
println!("Module: {}", module.name());
println!("Language: {:?}", module.language());
println!("Is base: {}", module.is_base());
if let Some(oid) = module.oid() {
println!("OID: {oid}");
}
// Module-level metadata from MODULE-IDENTITY.
println!("Organization: {}", module.organization());
println!("Description: {}", module.description());
println!("Last updated: {}", module.last_updated());
for rev in module.revisions() {
println!(" Revision: {} - {}", rev.date, rev.description);
}
// -- Object lookup by name --
let obj = mib.object("myName").expect("object exists");
println!("\nObject: {}", obj.name());
println!(" Status: {:?}", obj.status());
println!(" Access: {:?}", obj.access());
println!(" Description: {}", obj.description());
println!(" Kind: {:?}", obj.kind());
// -- Type information --
let ty = obj.ty().expect("has type");
println!(" Type name: {}", ty.name());
println!(" Base type: {:?}", ty.effective_base());
assert_eq!(ty.effective_base(), BaseType::OctetString);
// -- Iterate all objects in the module --
println!("\nAll objects in {}:", module.name());
for obj in module.objects() {
let oid = obj.node().oid();
let type_name = obj
.ty()
.map(|t| t.name().to_string())
.unwrap_or_else(|| "-".into());
println!(" {:<20} {:<24} {}", obj.name(), oid, type_name);
}
// -- Iterate all types in the module --
println!("\nAll types in {}:", module.name());
for ty in module.types() {
println!(
" {:<20} base={:?} tc={}",
ty.name(),
ty.effective_base(),
ty.is_textual_convention()
);
}
// -- Iterate all nodes in the module --
println!("\nAll nodes in {}:", module.name());
for node in module.nodes() {
println!(" {:<20} {}", node.name(), node.oid());
}
// -- Count summary --
println!("\nSummary:");
println!(" Modules: {}", mib.modules().count());
println!(" Objects: {}", mib.objects().count());
println!(" Types: {}", mib.types().count());
println!(" Scalars: {}", mib.scalars().count());
println!(" Tables: {}", mib.tables().count());
// -- Diagnostics --
println!(" Errors: {}", mib.has_errors());
if !mib.diagnostics().is_empty() {
println!(" Diagnostics:");
for d in mib.diagnostics() {
println!(" {d}");
}
}
}§OID tree walking
Root traversal, subtree iteration, depth-first walk, and node navigation.
//! OID tree walking: root traversal, subtree iteration, depth-first walk,
//! and node navigation.
use mib_rs::Loader;
fn main() {
let source = mib_rs::source::memory(
"EXAMPLE-FULL-MIB",
include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["EXAMPLE-FULL-MIB"])
.load()
.expect("should load");
// -- Root node --
let root = mib.root_node();
println!("Root: name={:?}, oid={}", root.name(), root.oid());
// -- Top-level children of the OID tree --
println!("\n=== Top-level arcs ===");
for child in root.children() {
println!(" {} (arc={})", child.name(), child.arc());
}
// -- Walking from a specific subtree --
let start = mib.resolve_node("exObjects").expect("should resolve");
println!("\n=== Subtree of {} ({}) ===", start.name(), start.oid());
for node in start.subtree() {
// Compute depth relative to start for indentation.
let depth = node.oid().len() - start.oid().len();
let indent = " ".repeat(depth);
let kind = node.kind();
println!(" {indent}{} ({}) [{kind:?}]", node.name(), node.oid());
}
// -- Node parent navigation --
let leaf = mib.resolve_node("exIfOutOctets").unwrap();
println!("\n=== Parent chain from {} ===", leaf.name());
let mut current = Some(leaf);
while let Some(node) = current {
println!(" {} ({})", node.name(), node.oid());
current = node.parent();
}
// -- Children of a specific node --
let entry = mib.resolve_node("exIfEntry").unwrap();
println!("\n=== Children of {} ===", entry.name());
for child in entry.children() {
let obj_info = child
.object()
.map(|o| format!("{:?} {:?}", o.access(), o.kind()))
.unwrap_or_else(|| "no object".into());
println!(" arc={}: {} - {}", child.arc(), child.name(), obj_info);
}
// -- Node properties --
let node = mib.resolve_node("exDeviceName").unwrap();
println!("\n=== Node properties: {} ===", node.name());
println!(" OID: {}", node.oid());
println!(" Arc: {}", node.arc());
println!(" Kind: {:?}", node.kind());
println!(" Status: {:?}", node.status());
println!(" Description: {}", node.description());
println!(
" Module: {:?}",
node.module().map(|m| m.name().to_string())
);
// Object attached to this node
if let Some(obj) = node.object() {
println!(" Object: {} ({:?})", obj.name(), obj.access());
}
// -- Notification attached to a node --
let node = mib.resolve_node("exStatusChange").unwrap();
if let Some(notif) = node.notification() {
println!("\n=== Notification on node {} ===", node.name());
println!(" Name: {}", notif.name());
}
// -- Total node count --
println!("\nTotal OID tree nodes: {}", mib.node_count());
// -- Iterate all nodes via Mib::nodes() --
println!("\n=== All named nodes ===");
let mut count = 0;
for node in mib.nodes() {
count += 1;
if count <= 10 {
println!(" {:<30} {}", node.name(), node.oid());
}
}
if count > 10 {
println!(" ... and {} more", count - 10);
}
}§Type introspection
Type chains, effective values, constraints, enums, display hints, and classification predicates.
//! Type chains, effective values, constraints, enums, display hints,
//! and classification predicates.
use mib_rs::{BaseType, Loader};
fn main() {
let source = mib_rs::source::memory(
"EXAMPLE-FULL-MIB",
include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["EXAMPLE-FULL-MIB"])
.load()
.expect("should load");
// -- Textual convention with display hint --
let ty = mib.r#type("ExPercentage").unwrap();
println!("=== ExPercentage ===");
println!(" Name: {}", ty.name());
println!(" Is TC: {}", ty.is_textual_convention());
println!(" Base: {:?}", ty.base());
println!(" Eff. base: {:?}", ty.effective_base());
println!(" Display hint: {:?}", ty.display_hint());
println!(" Eff. hint: {:?}", ty.effective_display_hint());
println!(" Status: {:?}", ty.status());
println!(" Description: {}", ty.description());
assert_eq!(ty.effective_base(), BaseType::Integer32);
assert_eq!(ty.effective_display_hint(), "d-%");
// Range constraints
let ranges = ty.effective_ranges();
println!(" Ranges:");
for r in ranges {
println!(" {}..{}", r.min, r.max);
}
// -- Textual convention with size constraint --
let ty = mib.r#type("ExName").unwrap();
println!("\n=== ExName ===");
println!(" Eff. base: {:?}", ty.effective_base());
println!(" Display hint: {:?}", ty.effective_display_hint());
let sizes = ty.effective_sizes();
println!(" Sizes:");
for s in sizes {
println!(" {}..{}", s.min, s.max);
}
assert!(ty.is_string());
// -- Enumeration type --
let ty = mib.r#type("ExDeviceStatus").unwrap();
println!("\n=== ExDeviceStatus ===");
println!(" Is TC: {}", ty.is_textual_convention());
println!(" Is enum: {}", ty.is_enumeration());
println!(" Eff. base: {:?}", ty.effective_base());
let enums = ty.effective_enums();
println!(" Enum values:");
for e in enums {
println!(" {}({})", e.label, e.value);
}
// -- Type parent chain --
// ExName -> DisplayString -> (base: OctetString)
let ty = mib.r#type("ExName").unwrap();
println!("\n=== Type chain: ExName ===");
print_type_chain(&ty);
// ExPercentage -> (base: Integer32)
let ty = mib.r#type("ExPercentage").unwrap();
println!("\n=== Type chain: ExPercentage ===");
print_type_chain(&ty);
// -- Type accessed through an object --
let obj = mib.object("exIfSpeed").unwrap();
let ty = obj.ty().unwrap();
println!("\n=== exIfSpeed type ===");
println!(" Type name: {}", ty.name());
println!(" Is gauge: {}", ty.is_gauge());
println!(" Eff. base: {:?}", ty.effective_base());
assert!(ty.is_gauge());
// -- Counter type --
let obj = mib.object("exUptime").unwrap();
let ty = obj.ty().unwrap();
println!("\n=== exUptime type ===");
println!(" Type name: {}", ty.name());
println!(" Is counter: {}", ty.is_counter());
assert!(ty.is_counter());
// -- Effective accessors on Object (shortcut through type chain) --
let obj = mib.object("exDeviceName").unwrap();
println!("\n=== Object effective accessors ===");
println!(
" exDeviceName.effective_display_hint: {:?}",
obj.effective_display_hint()
);
println!(
" exDeviceName.effective_sizes: {:?}",
obj.effective_sizes()
);
let obj = mib.object("exDeviceStatus").unwrap();
println!(" exDeviceStatus.effective_enums:");
for e in obj.effective_enums() {
println!(" {}({})", e.label, e.value);
}
// -- Display-hint formatting --
//
// Objects with a DISPLAY-HINT can format raw SNMP values directly.
// Integer hints (d, d-N, x, o, b) format integer values.
// Octet-string hints format byte slices.
// ExHundredths has DISPLAY-HINT "d-2": 1234 -> "12.34"
use mib_rs::mib::display_hint::{self, HexCase};
let obj = mib.object("exTemperature").unwrap();
println!("\n=== Display-hint formatting ===");
println!(" hint: {:?}", obj.effective_display_hint());
println!(
" format(2345): {:?}",
obj.format_integer(2345, HexCase::Upper)
);
println!(" scale(2345): {:?}", obj.scale_integer(2345));
println!(" units: {:?}", obj.units());
assert_eq!(
obj.format_integer(2345, HexCase::Upper),
Some("23.45".into())
);
assert_eq!(obj.scale_integer(2345), Some(23.45));
// ExMacAddress has DISPLAY-HINT "1x:": formats as colon-separated hex.
let obj = mib.object("exDeviceMac").unwrap();
let mac = [0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e];
println!(
" mac format: {:?}",
obj.format_octets(&mac, HexCase::Upper)
);
assert_eq!(
obj.format_octets(&mac, HexCase::Upper),
Some("00:1A:2B:3C:4D:5E".into())
);
// ExName has DISPLAY-HINT "255a": ASCII display.
let obj = mib.object("exDeviceName").unwrap();
println!(
" name format: {:?}",
obj.format_octets(b"switch-01", HexCase::Upper)
);
assert_eq!(
obj.format_octets(b"switch-01", HexCase::Upper),
Some("switch-01".into()),
);
// Objects without a display hint return None.
let obj = mib.object("exUptime").unwrap();
assert_eq!(obj.format_integer(12345, HexCase::Upper), None);
// The display_hint module is also available for direct use without
// an Object handle, e.g. when you have a hint string from elsewhere.
assert_eq!(
display_hint::format_integer("d-2", 1234, HexCase::Upper),
Some("12.34".into())
);
assert_eq!(
display_hint::format_octets("1d.1d.1d.1d", &[10, 0, 0, 1], HexCase::Upper),
Some("10.0.0.1".into())
);
// -- Units clause --
let obj = mib.object("exUptime").unwrap();
println!("\n exUptime.units: {:?}", obj.units());
let obj = mib.object("exIfSpeed").unwrap();
println!(" exIfSpeed.units: {:?}", obj.units());
// -- DEFVAL --
let obj = mib.object("exRebootEnabled").unwrap();
if let Some(defval) = obj.default_value() {
println!(
"\n exRebootEnabled.defval: {} (kind={:?})",
defval,
defval.kind()
);
}
// -- Classification predicates --
println!("\n=== Type classification ===");
for name in ["ExName", "ExPercentage", "ExDeviceStatus"] {
let ty = mib.r#type(name).unwrap();
println!(
" {:<20} string={:<5} counter={:<5} gauge={:<5} enum={:<5} bits={:<5}",
ty.name(),
ty.is_string(),
ty.is_counter(),
ty.is_gauge(),
ty.is_enumeration(),
ty.is_bits(),
);
}
// -- Iterate all types in the loaded MIB --
println!("\n=== All user-defined types ===");
let module = mib.module("EXAMPLE-FULL-MIB").unwrap();
for ty in module.types() {
let parent_name = ty
.parent()
.map(|p| p.name().to_string())
.unwrap_or_default();
println!(
" {:<20} base={:<16?} parent={:<20} tc={}",
ty.name(),
ty.effective_base(),
parent_name,
ty.is_textual_convention(),
);
}
}
fn print_type_chain(ty: &mib_rs::Type<'_>) {
let mut current = Some(*ty);
let mut depth = 0;
while let Some(t) = current {
let indent = " ".repeat(depth + 1);
let hint = t.display_hint();
let hint_str = if hint.is_empty() {
String::new()
} else {
format!(" hint={hint:?}")
};
println!(
"{}{} (base={:?}, tc={}{})",
indent,
t.name(),
t.base(),
t.is_textual_convention(),
hint_str,
);
current = t.parent();
depth += 1;
}
}§Table navigation
Tables, rows, columns, indexes, and object kind predicates.
//! Table navigation: rows, columns, indexes, and object kind predicates.
use mib_rs::Loader;
fn main() {
let source = mib_rs::source::memory(
"EXAMPLE-FULL-MIB",
include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["EXAMPLE-FULL-MIB"])
.load()
.expect("should load");
// -- Find all tables --
println!("=== Tables ===");
for table in mib.tables() {
println!(" {} ({})", table.name(), table.node().oid());
}
// -- Table structure --
let table = mib.object("exIfTable").unwrap();
println!("\n=== exIfTable structure ===");
println!(" Is table: {}", table.is_table());
println!(" Kind: {:?}", table.kind());
// Get the row entry
let row = table.row().expect("table should have a row");
println!("\n Row: {}", row.name());
println!(" Is row: {}", row.is_row());
// Navigate back to the table from the row
let back = row.table().expect("row should reference table");
assert_eq!(back.name(), table.name());
// -- Columns --
println!("\n Columns:");
for col in table.columns() {
let ty = col.ty().map(|t| t.name().to_string()).unwrap_or_default();
println!(
" {:<20} {:?} type={:<20} index={}",
col.name(),
col.access(),
ty,
col.is_index(),
);
assert!(col.is_column());
}
// Navigate from a column back to its row and table.
let col = mib.object("exIfName").unwrap();
let col_row = col.row().expect("column should have a row");
let col_table = col.table().expect("column should have a table");
println!(
"\n exIfName -> row={}, table={}",
col_row.name(),
col_table.name()
);
// -- Indexes --
println!("\n=== Indexes ===");
let indexes: Vec<_> = row.effective_indexes().collect();
for idx in &indexes {
let obj = idx.object().expect("index object");
let ty = idx.ty().expect("index type");
println!(" {}", idx.name());
println!(" Object: {}", obj.name());
println!(
" Type: {} (base={:?})",
ty.name(),
ty.effective_base()
);
println!(" Implied: {}", idx.implied());
println!(" Encoding: {:?}", idx.encoding());
println!(" Row: {}", idx.row().name());
}
// -- All scalars --
println!("\n=== Scalars ===");
for scalar in mib.scalars() {
let ty_name = scalar
.ty()
.map(|t| t.name().to_string())
.unwrap_or_default();
println!(
" {:<20} {:<20} {:?}",
scalar.name(),
ty_name,
scalar.access(),
);
assert!(scalar.is_scalar());
}
// -- All rows --
println!("\n=== Rows ===");
for row in mib.rows() {
let idx_names: Vec<_> = row
.effective_indexes()
.map(|i| i.name().to_string())
.collect();
println!(" {:<20} indexes=[{}]", row.name(), idx_names.join(", "));
}
// -- All columns --
println!("\n=== All columns ===");
for col in mib.columns() {
println!(
" {:<20} table={}",
col.name(),
col.table()
.map(|t| t.name().to_string())
.unwrap_or_default(),
);
}
// -- Object kind filtering --
println!("\n=== Object counts by kind ===");
println!(" Tables: {}", mib.tables().count());
println!(" Rows: {}", mib.rows().count());
println!(" Columns: {}", mib.columns().count());
println!(" Scalars: {}", mib.scalars().count());
}§Module metadata
Module metadata, imports, revisions, base modules, and module-scoped iteration.
//! Module metadata, imports, revisions, base modules, and module-scoped
//! iteration.
use mib_rs::Loader;
fn main() {
let source = mib_rs::source::memory(
"EXAMPLE-FULL-MIB",
include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["EXAMPLE-FULL-MIB"])
.load()
.expect("should load");
// -- List all loaded modules --
println!("=== All loaded modules ===");
for module in mib.modules() {
let tag = if module.is_base() { " (base)" } else { "" };
println!(" {:<30} {:?}{}", module.name(), module.language(), tag,);
}
// -- Module handle basics --
let module = mib.module("EXAMPLE-FULL-MIB").unwrap();
println!("\n=== {} (handle) ===", module.name());
println!(" Language: {:?}", module.language());
println!(" Is base: {}", module.is_base());
println!(" Source path: {}", module.source_path());
if let Some(oid) = module.oid() {
println!(" OID: {oid}");
}
// -- Module metadata from MODULE-IDENTITY --
println!("\n=== {} (metadata) ===", module.name());
println!(" Organization: {}", module.organization());
println!(" Contact: {}", module.contact_info());
println!(" Description: {}", module.description());
println!(" Last updated: {}", module.last_updated());
// -- Revisions --
println!("\n Revisions:");
for rev in module.revisions() {
println!(" {} - {}", rev.date, rev.description);
}
// -- Imports --
println!("\n Imports:");
for imp in module.imports() {
let symbols: Vec<_> = imp.symbols.iter().map(|s| s.name.as_str()).collect();
println!(" FROM {}: {}", imp.module, symbols.join(", "));
}
// -- Module-scoped lookups (via handle) --
println!("\n Module-scoped object lookup:");
let obj = module.object("exDeviceName").expect("object in module");
println!(" {} ({:?})", obj.name(), obj.access());
println!("\n Module-scoped type lookup:");
let ty = module.r#type("ExPercentage").expect("type in module");
println!(" {} (base={:?})", ty.name(), ty.effective_base());
// Cross-check: object's module matches
assert_eq!(obj.module().unwrap().name(), module.name());
// -- Module-scoped iteration --
println!("\n Objects in module:");
for obj in module.objects() {
println!(" {:<24} {:?}", obj.name(), obj.kind());
}
println!("\n Types in module:");
for ty in module.types() {
println!(" {:<24} {:?}", ty.name(), ty.effective_base());
}
println!("\n Nodes in module:");
let mut count = 0;
for node in module.nodes() {
count += 1;
if count <= 5 {
println!(" {:<24} {}", node.name(), node.oid());
}
}
if count > 5 {
println!(" ... and {} more", count - 5);
}
// -- Base module inspection --
// Seven base modules are always present in every loaded Mib. They define
// the SMI language itself (ASN.1 macros like OBJECT-TYPE, MODULE-IDENTITY,
// TEXTUAL-CONVENTION) plus the core types and OID tree roots.
//
// These are constructed programmatically, not parsed from files:
// - You don't need to supply them as source files
// - If they exist on disk, the synthetic versions take priority
// - Spans are synthetic (no real source text to point to)
// - source_path() returns an empty string
//
// Use is_base() to distinguish them from user-supplied modules.
let base = mib.module("SNMPv2-SMI").unwrap();
println!("\n=== Base module: {} ===", base.name());
println!(" Is base: {}", base.is_base());
println!(" Language: {:?}", base.language());
println!(
" Source path: {:?} (empty for base modules)",
base.source_path()
);
// Base modules provide the well-known OID tree roots.
println!(" Some nodes:");
for name in ["iso", "internet", "mgmt", "mib-2", "enterprises"] {
if let Some(node) = base.node(name) {
println!(" {:<16} {}", node.name(), node.oid());
}
}
// -- Which modules define/import a symbol --
let definers = mib.modules_defining("exDeviceName");
println!("\nModules defining 'exDeviceName': {}", definers.len());
let importers = mib.modules_importing("DisplayString");
println!("Modules importing 'DisplayString': {}", importers.len());
// -- All symbols across the MIB --
// Symbol is a raw-level enum (stores arena IDs), so module() returns
// ModuleId. Use module_by_id() to get back to a handle for the name.
let symbols = mib.all_symbols();
let module_symbols: Vec<_> = symbols
.iter()
.filter(|s| {
s.module(&mib)
.map(|mid| mib.module_by_id(mid).name() == "EXAMPLE-FULL-MIB")
.unwrap_or(false)
})
.collect();
println!(
"\nTotal symbols in EXAMPLE-FULL-MIB: {}",
module_symbols.len()
);
}§JSON export
JSON export of a resolved MIB using the serde-based export API.
//! JSON export of a resolved MIB using the serde-based export API.
use mib_rs::{Loader, ResolverStrictness};
fn main() {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("should load");
// -- Export to JSON --
let payload = mib_rs::export::export_payload(&mib, ResolverStrictness::Normal);
// The payload is a fully serializable structure.
let json = serde_json::to_string_pretty(&payload).expect("should serialize");
println!("=== JSON export (truncated) ===");
// Print just the first 80 lines to keep output manageable.
for (i, line) in json.lines().enumerate() {
if i >= 80 {
println!("... ({} more lines)", json.lines().count() - 80);
break;
}
println!("{line}");
}
// -- Inspect export payload fields --
println!("\n=== Export payload structure ===");
println!(" Schema version: {}", payload.schema_version);
println!(" Export kind: {}", payload.export_kind);
println!(" Strictness: {}", payload.strictness);
println!(
" Exporter: {} v{}",
payload.exporter.implementation, payload.exporter.version
);
println!(" Modules: {}", payload.modules.len());
println!(" Types: {}", payload.types.len());
println!(" Nodes: {}", payload.nodes.len());
println!(" Objects: {}", payload.objects.len());
println!(" Notifications: {}", payload.notifications.len());
println!(" Groups: {}", payload.groups.len());
println!(" Compliances: {}", payload.compliances.len());
println!(" Diagnostics: {}", payload.diagnostics.len());
// -- Inspect exported objects --
println!("\n=== Exported objects ===");
for obj in &payload.objects {
println!(
" {:<24} oid={:<30} kind={} access={}",
obj.name, obj.oid, obj.kind, obj.access,
);
}
// -- Inspect exported types --
println!("\n=== Exported types ===");
for ty in &payload.types {
println!(
" {:<24} module={:<20} base={}",
ty.name, ty.module, ty.base,
);
}
// -- Inspect exported nodes (first few) --
println!("\n=== Exported nodes (first 10) ===");
for node in payload.nodes.iter().take(10) {
println!(" {:<30} {}", node.name, node.oid);
}
if payload.nodes.len() > 10 {
println!(" ... and {} more", payload.nodes.len() - 10);
}
}§Notifications, groups, and compliance
Notifications, object groups, notification groups, and compliance statements.
//! Notifications, groups, and compliance statements.
use mib_rs::Loader;
fn main() {
let source = mib_rs::source::memory(
"EXAMPLE-FULL-MIB",
include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["EXAMPLE-FULL-MIB"])
.load()
.expect("should load");
// -- Notifications --
println!("=== Notifications ===");
// Look up a notification by name.
let notif = mib
.notification("exStatusChange")
.expect("notification exists");
println!(" {}", notif.name());
println!(" OID: {}", notif.node().unwrap().oid());
println!(" Status: {:?}", notif.status());
println!(" Description: {}", notif.description());
println!(" Objects:");
for obj in notif.objects() {
println!(" {}", obj.name());
}
let notif2 = mib
.notification("exIfStatusChange")
.expect("notification exists");
println!("\n {}", notif2.name());
println!(" OID: {}", notif2.node().unwrap().oid());
println!(
" Objects: {:?}",
notif2.objects().map(|o| o.name()).collect::<Vec<_>>()
);
// -- Enumerate all notifications in the user module --
println!("\n All notifications:");
for notif in mib.notifications() {
if notif.module().map(|m| m.name()) == Some("EXAMPLE-FULL-MIB") {
println!(" {}", notif.name());
}
}
// -- Object Groups --
println!("\n=== Object Groups ===");
let group = mib.group("exScalarGroup").expect("group exists");
println!(" {}", group.name());
println!(" OID: {}", group.node().unwrap().oid());
println!(" Status: {:?}", group.status());
println!(" Description: {}", group.description());
println!(
" Is notification group: {}",
group.is_notification_group()
);
println!(" Members:");
for member in group.members() {
println!(" {}", member.name());
}
// -- Notification Group --
let ngroup = mib.group("exNotifGroup").expect("group exists");
println!("\n {}", ngroup.name());
println!(
" Is notification group: {}",
ngroup.is_notification_group()
);
println!(" Members:");
for member in ngroup.members() {
println!(" {}", member.name());
}
// -- Compliance --
let compliance = mib
.compliance("exBasicCompliance")
.expect("compliance exists");
println!("\n=== Compliance ===");
println!(" {}", compliance.name());
println!(" OID: {}", compliance.node().unwrap().oid());
println!(" Status: {:?}", compliance.status());
println!(" Description: {}", compliance.description());
println!(" MODULE clauses: {}", compliance.modules().len());
// -- Node-level cross references --
// Nodes can tell you what's attached to them.
let node = mib.resolve_node("exStatusChange").unwrap();
println!("\n=== Node cross-references ===");
println!(" Node: {}", node.name());
println!(" Has notification: {}", node.notification().is_some());
println!(" Has object: {}", node.object().is_some());
println!(" Has group: {}", node.group().is_some());
let node = mib.resolve_node("exScalarGroup").unwrap();
println!(" Node: {}", node.name());
println!(" Has group: {}", node.group().is_some());
let node = mib.resolve_node("exBasicCompliance").unwrap();
println!(" Node: {}", node.name());
println!(" Has compliance: {}", node.compliance().is_some());
}§Query formats
Plain names, qualified names, numeric OIDs, instance OIDs, and OID formatting.
//! Demonstrate all query formats: plain names, qualified names, numeric OIDs,
//! instance OIDs, and OID formatting.
use mib_rs::Loader;
fn main() {
let source = mib_rs::source::memory(
"DOC-EXAMPLE-MIB",
include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["DOC-EXAMPLE-MIB"])
.load()
.expect("should load");
// -- Plain name lookup --
// resolve_node returns the Node handle for a name.
let node = mib.resolve_node("docDeviceName").expect("should resolve");
println!("Plain name lookup:");
println!(" Name: {}", node.name());
println!(" OID: {}", node.oid());
println!(" Kind: {:?}", node.kind());
// -- Qualified name (MODULE::name) --
// Scopes the lookup to a specific module.
let node = mib
.resolve_node("DOC-EXAMPLE-MIB::docDeviceName")
.expect("should resolve");
println!("\nQualified name lookup:");
println!(" Name: {}", node.name());
println!(" OID: {}", node.oid());
// -- Numeric OID --
let oid_str = "1.3.6.1.4.1.99999.1.1";
let node = mib.resolve_node(oid_str).expect("should resolve");
println!("\nNumeric OID lookup ({oid_str}):");
println!(" Name: {}", node.name());
println!(" OID: {}", node.oid());
// -- Leading-dot numeric OID --
let node = mib
.resolve_node(".1.3.6.1.4.1.99999.1.1")
.expect("should resolve");
println!("\nLeading-dot OID: {}", node.name());
// -- resolve_oid: symbolic name to numeric OID --
let oid = mib.resolve_oid("docDescr").expect("should resolve");
println!("\nresolve_oid(\"docDescr\"): {oid}");
// -- Instance OIDs (name.suffix) --
// resolve_node returns the deepest matching tree node.
let node = mib
.resolve_node("docDescr.7")
.expect("should resolve instance");
println!("\nInstance OID - resolve_node(\"docDescr.7\"):");
println!(
" Node name: {} (the base node, not the instance)",
node.name()
);
// resolve_oid returns the full numeric OID with suffix included.
let instance_oid = mib
.resolve_oid("docDescr.7")
.expect("should resolve instance");
println!(" Full OID: {instance_oid}");
// Multi-component instance suffix
let deep = mib.resolve_oid("docDescr.1.2.3").expect("should resolve");
println!(" Multi-suffix: {deep}");
// -- Numeric instance OID --
let parsed: mib_rs::Oid = "1.3.6.1.4.1.99999.2.1.1.2.42".parse().unwrap();
let node = mib.lookup_oid(&parsed);
println!(
"\nlookup_oid(\"...2.42\"): {} (longest prefix match)",
node.name()
);
// exact_node_by_oid only matches if the OID is an exact tree node.
let exact = mib.exact_node_by_oid(&parsed);
println!(
"exact_node_by_oid: {:?} (None because .42 is an instance)",
exact.map(|n| n.name())
);
let base_oid: mib_rs::Oid = "1.3.6.1.4.1.99999.2.1.1.2".parse().unwrap();
let exact = mib.exact_node_by_oid(&base_oid);
println!(
"exact_node_by_oid(base): {:?} (exact match)",
exact.map(|n| n.name())
);
// -- format_oid: numeric OID back to MODULE::name.suffix --
let oid = mib.resolve_oid("docDescr.7").unwrap();
let formatted = mib.format_oid(&oid);
println!("\nformat_oid: {formatted}");
// Round-trip: formatted string back to OID.
let round_trip = mib.resolve_oid(&formatted).unwrap();
println!("Round-trip: {round_trip}");
assert_eq!(oid, round_trip);
// -- lookup_instance: node + suffix + index decoding --
// Given a full instance OID (column.index), split it into the base
// node and instance suffix, then decode the suffix into typed index
// values using the row's INDEX clause.
let oid = mib.resolve_oid("docDescr.7").unwrap();
let lookup = mib.lookup_instance(&oid);
println!("\nlookup_instance(\"docDescr.7\"):");
println!(" Node: {}", lookup.node().name());
println!(" Suffix: {:?}", lookup.suffix());
for idx in lookup.decode_indexes() {
println!(" Index: {}={}", idx.name(), idx.value());
}
// -- resolve: returns NodeId (lower-level) --
let node_id = mib.raw().resolve("docTable");
println!("\nresolve(\"docTable\"): {:?}", node_id);
// -- symbol_by_name: untyped symbol lookup --
let sym = mib.symbol_by_name("DocName").expect("type should exist");
println!("symbol_by_name(\"DocName\"): {:?}", sym);
// -- Module-scoped lookups --
let module = mib.module("DOC-EXAMPLE-MIB").unwrap();
let obj = module.object("docDeviceName").expect("in module");
println!("\nModule-scoped: {}.{}", module.name(), obj.name());
let ty = module.r#type("DocName").expect("type in module");
println!(
"Module-scoped type: {} (tc={})",
ty.name(),
ty.is_textual_convention()
);
// -- OID tree: children and subtree --
let table_node = mib.resolve_node("docTable").unwrap();
println!("\nChildren of {}:", table_node.name());
for child in table_node.children() {
println!(" {} ({})", child.name(), child.oid());
}
println!("\nSubtree of {}:", table_node.name());
for node in table_node.subtree() {
println!(" {} ({})", node.name(), node.oid());
}
}§Diagnostics
Diagnostic collection, strictness levels, reporting configuration, filtering, and severity overrides.
//! Diagnostic collection, strictness levels, reporting configuration,
//! filtering, and severity overrides.
use mib_rs::{DiagnosticConfig, Loader, ReportingLevel, ResolverStrictness};
/// Create a fresh source for each loader (Source is not Clone).
fn make_source() -> Box<dyn mib_rs::Source> {
mib_rs::source::memory(
"DIAG-EXAMPLE-MIB",
br#"DIAG-EXAMPLE-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
FROM SNMPv2-SMI
DisplayString, NoSuchThing
FROM SNMPv2-TC;
diagMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "MIB for diagnostics demo."
::= { enterprises 99997 }
diagValue OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A value."
::= { diagMib 1 }
END
"#,
)
}
fn main() {
// -- Default settings --
println!("=== Default strictness (Normal) ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.load()
.expect("should load");
println!(" Has errors: {}", mib.has_errors());
println!(" Diagnostics: {}", mib.diagnostics().len());
for d in mib.diagnostics() {
println!(" {d}");
}
// Unresolved references
let unresolved = mib.unresolved();
if !unresolved.is_empty() {
println!(" Unresolved references: {}", unresolved.len());
for u in unresolved {
println!(" {u:?}");
}
}
// -- Strict mode --
println!("\n=== Strict mode ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.resolver_strictness(ResolverStrictness::Strict)
.load()
.expect("should load");
println!(" Has errors: {}", mib.has_errors());
println!(" Diagnostics: {}", mib.diagnostics().len());
for d in mib.diagnostics() {
println!(" {d}");
}
// -- Permissive mode --
println!("\n=== Permissive mode ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.resolver_strictness(ResolverStrictness::Permissive)
.load()
.expect("should load");
println!(" Has errors: {}", mib.has_errors());
println!(" Diagnostics: {}", mib.diagnostics().len());
// -- Reporting levels --
println!("\n=== Verbose reporting ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(DiagnosticConfig::verbose())
.load()
.expect("should load");
println!(" Diagnostics (verbose): {}", mib.diagnostics().len());
for d in mib.diagnostics() {
println!(" [{:?}] {}", d.severity, d.message);
}
println!("\n=== Quiet reporting ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(DiagnosticConfig::quiet())
.load()
.expect("should load");
println!(" Diagnostics (quiet): {}", mib.diagnostics().len());
for d in mib.diagnostics() {
println!(" [{:?}] {}", d.severity, d.message);
}
println!("\n=== Silent reporting ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(DiagnosticConfig::silent())
.load()
.expect("should load");
println!(" Diagnostics (silent): {}", mib.diagnostics().len());
// -- Custom diagnostic config --
println!("\n=== Custom config with overrides ===");
let mut config = DiagnosticConfig::for_reporting(ReportingLevel::Verbose);
// Ignore specific diagnostic patterns.
config.ignore.push("import-*".to_string());
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(config)
.load()
.expect("should load");
println!(
" Diagnostics (import-* ignored): {}",
mib.diagnostics().len()
);
for d in mib.diagnostics() {
println!(" [{}] {}", d.code, d.message);
}
// -- Diagnostic severity threshold --
// DiagnosticConfig.fail_at controls what severity causes LoadError::DiagnosticThreshold.
println!("\n=== Fail-at threshold ===");
let mut config = DiagnosticConfig::for_reporting(ReportingLevel::Verbose);
config.fail_at = mib_rs::Severity::Minor;
let result = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(config)
.load();
match result {
Ok(mib) => println!(" Loaded OK, errors={}", mib.has_errors()),
Err(e) => println!(" Load failed: {e}"),
}
// -- Inspecting diagnostic fields --
println!("\n=== Diagnostic details ===");
let mib = Loader::new()
.source(make_source())
.modules(["DIAG-EXAMPLE-MIB"])
.diagnostic_config(DiagnosticConfig::verbose())
.load()
.expect("should load");
for d in mib.diagnostics() {
println!(" Severity: {:?}", d.severity);
println!(" Code: {}", d.code);
println!(" Message: {}", d.message);
if let Some(ref module) = d.module {
println!(" Module: {module}");
}
if let Some(line) = d.line {
print!(" Location: line {line}");
if let Some(col) = d.column {
print!(", col {col}");
}
println!();
}
println!();
}
}§Raw data access
Low-level raw data access: sub-clause spans, import metadata, OID references, symbol tables, and bulk arena access.
//! Low-level raw data access for tooling: arena IDs, sub-clause spans,
//! import metadata, OID references, symbol tables, and OID tree traversal.
//!
//! The raw API (`mib.raw()`) is designed for tools that need capabilities
//! beyond the handle API: linters, language servers, exporters, and editor
//! integrations. This example demonstrates what it offers that the handle
//! API does not.
use mib_rs::Loader;
use mib_rs::types::Span;
fn main() {
// Use a MIB with a deliberately unused import (NoSuchThing) and
// a used-but-from-wrong-module pattern to show import analysis.
let source = mib_rs::source::memory(
"RAW-EXAMPLE-MIB",
br#"RAW-EXAMPLE-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
FROM SNMPv2-SMI
TEXTUAL-CONVENTION, DisplayString, TruthValue
FROM SNMPv2-TC;
rawMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example Corp"
CONTACT-INFO "support@example.com"
DESCRIPTION "Example module for raw API demo."
::= { enterprises 99990 }
RawName ::= TEXTUAL-CONVENTION
DISPLAY-HINT "255a"
STATUS current
DESCRIPTION "A name string."
SYNTAX DisplayString (SIZE (1..64))
rawScalars OBJECT IDENTIFIER ::= { rawMib 1 }
rawDeviceName OBJECT-TYPE
SYNTAX RawName
MAX-ACCESS read-only
STATUS current
DESCRIPTION "The device name."
::= { rawScalars 1 }
rawEnabled OBJECT-TYPE
SYNTAX TruthValue
MAX-ACCESS read-write
STATUS current
DESCRIPTION "Whether the device is enabled."
DEFVAL { true }
::= { rawScalars 2 }
rawCount OBJECT-TYPE
SYNTAX Integer32 (0..1000)
UNITS "items"
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Item count."
::= { rawScalars 3 }
END
"#
.as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["RAW-EXAMPLE-MIB"])
.load()
.expect("should load");
let raw = mib.raw();
// ---------------------------------------------------------------
// 1. Sub-clause spans
//
// ObjectData exposes per-clause source locations: syntax_span(),
// access_span(), units_span(), augments_span(), default_value_span().
// These let a linter or language server point diagnostics at the
// specific clause that's wrong, not the whole definition.
// ---------------------------------------------------------------
println!("=== Sub-clause spans ===");
let mod_data = raw.module(mib.module_by_name("RAW-EXAMPLE-MIB").unwrap());
// Helper: absent clauses produce Span::ZERO or Span::SYNTHETIC.
let has_span = |s: Span| s != Span::ZERO && !s.is_synthetic();
for &obj_id in mod_data.objects() {
let obj = raw.object(obj_id);
println!(" {}:", obj.name());
// Definition span covers the whole OBJECT-TYPE.
let def = obj.span();
let (def_line, _) = mod_data.line_col(def.start);
println!(" definition: line {def_line}");
// SYNTAX clause span.
let syn = obj.syntax_span();
if has_span(syn) {
let (line, col) = mod_data.line_col(syn.start);
println!(" SYNTAX: line {line}, col {col}");
}
// MAX-ACCESS clause span.
let acc = obj.access_span();
if has_span(acc) {
let (line, col) = mod_data.line_col(acc.start);
println!(" MAX-ACCESS: line {line}, col {col}");
}
// UNITS clause span (only present on some objects).
let units = obj.units_span();
if has_span(units) {
let (line, col) = mod_data.line_col(units.start);
println!(" UNITS: line {line}, col {col}");
}
// DEFVAL clause span (only present on some objects).
let defval = obj.default_value_span();
if has_span(defval) {
let (line, col) = mod_data.line_col(defval.start);
println!(" DEFVAL: line {line}, col {col}");
}
}
// TypeData also has syntax_span() for the SYNTAX clause in TCs.
for &type_id in mod_data.types() {
let ty = raw.type_(type_id);
let syn = ty.syntax_span();
if has_span(syn) {
let (line, col) = mod_data.line_col(syn.start);
println!(" {} (type):", ty.name());
println!(" SYNTAX: line {line}, col {col}");
}
}
// ---------------------------------------------------------------
// 2. Import resolution metadata
//
// ModuleData tracks which imports were actually used during
// resolution, and where each imported symbol was resolved from.
// This is the data a linter needs for "unused import" warnings.
// ---------------------------------------------------------------
println!("\n=== Import analysis ===");
for imp in mod_data.imports() {
println!(" FROM {}:", imp.module);
for sym in &imp.symbols {
let used = mod_data.is_import_used(&sym.name);
let resolved_from = mod_data.import_source(&sym.name);
let status = if !used {
"UNUSED".to_string()
} else if let Some(source_id) = resolved_from {
let source_mod = raw.module(source_id);
if source_mod.name() != imp.module {
// Resolved from a different module than declared.
format!("resolved from {}", source_mod.name())
} else {
"ok".to_string()
}
} else {
"unresolved".to_string()
};
// ImportSymbol carries a span for "go to definition" on imports.
let (line, col) = mod_data.line_col(sym.span.start);
println!(" {:<24} line {}:{:<4} {}", sym.name, line, col, status);
}
}
// ---------------------------------------------------------------
// 3. OID references (oid_refs)
//
// Entity definitions record the symbolic names referenced in their
// OID value assignments. For example, { enterprises 99990 } produces
// an OidRef for "enterprises" with its span. A language server uses
// these for "go to definition" on OID components and for reference
// highlighting.
// ---------------------------------------------------------------
println!("\n=== OID references ===");
for &obj_id in mod_data.objects() {
let obj = raw.object(obj_id);
let refs = obj.oid_refs();
if !refs.is_empty() {
println!(" {}:", obj.name());
for r in refs {
let (line, col) = mod_data.line_col(r.span.start);
println!(" ref {:?} at line {}:{}", r.name, line, col);
}
}
}
// ---------------------------------------------------------------
// 4. Symbol tables and available_symbols
//
// Mib::available_symbols(mod_id) returns everything visible in a
// module's scope: own definitions first, then resolved imports.
// This is what a completion engine would use to suggest names.
// ---------------------------------------------------------------
println!("\n=== Available symbols in RAW-EXAMPLE-MIB ===");
let mod_id = mib.module_by_name("RAW-EXAMPLE-MIB").unwrap();
let symbols = mib.available_symbols(mod_id);
// Show own definitions vs imported symbols.
let own_count = mod_data.definitions().count();
println!(
" {} own definitions, {} total (including imports)",
own_count,
symbols.len()
);
println!("\n Own definitions:");
for sym in symbols.iter().take(own_count) {
let kind = match sym {
mib_rs::raw::Symbol::Object(_) => "object",
mib_rs::raw::Symbol::Type(_) => "type",
mib_rs::raw::Symbol::Node(_) => "node",
mib_rs::raw::Symbol::Notification(_) => "notification",
mib_rs::raw::Symbol::Group(_) => "group",
mib_rs::raw::Symbol::Compliance(_) => "compliance",
mib_rs::raw::Symbol::Capability(_) => "capability",
};
println!(" {:<24} {}", sym.name(&mib), kind);
}
println!("\n Imported symbols (first 10):");
for sym in symbols.iter().skip(own_count).take(10) {
let source_mod = sym
.module(&mib)
.map(|id| raw.module(id).name().to_string())
.unwrap_or_default();
println!(" {:<24} from {}", sym.name(&mib), source_mod);
}
// ---------------------------------------------------------------
// 5. ID-only workflows
//
// Handles and raw access share the same arena IDs (ObjectId,
// NodeId, etc.). Handles expose theirs via .id(), so you can
// always get an ID. The raw layer lets you work entirely in IDs:
// follow cross-refs like obj_data.type_id(), look up data with
// raw.object(id), and iterate arenas without constructing
// handles. IDs are Copy + Eq + Hash + Ord, so they work as map
// keys or can be sent across channels.
// ---------------------------------------------------------------
println!("\n=== ID-only workflows ===");
// Get an ID from a handle, or directly from Mib.
let handle = mib.object("rawDeviceName").unwrap();
let obj_id = handle.id(); // same as mib.object_by_name("rawDeviceName")
println!(" ObjectId index: {}", obj_id.index());
// Follow cross-references without handles.
let obj_data = raw.object(obj_id);
if let Some(type_id) = obj_data.type_id() {
let type_data = raw.type_(type_id);
println!(
" {} -> type {} (no handles needed)",
obj_data.name(),
type_data.name()
);
}
// IDs can be collected into sets for deduplication.
use std::collections::HashSet;
let mut seen = HashSet::new();
seen.insert(obj_id);
println!(" IDs are hashable: {}", seen.contains(&obj_id));
// ---------------------------------------------------------------
// 6. Bulk arena access
//
// raw.*_slice() gives direct &[Data] access to the arena backing
// stores. No iterator adapters, no handle wrapping. Useful for
// exporters, batch analysis, or building secondary indices.
// ---------------------------------------------------------------
println!("\n=== Arena slices ===");
println!(" Modules: {}", raw.modules_slice().len());
println!(" Objects: {}", raw.objects_slice().len());
println!(" Types: {}", raw.types_slice().len());
println!(" Notifications: {}", raw.notifications_slice().len());
// Build a quick index: type name -> list of objects using it.
use std::collections::HashMap;
let mut type_usage: HashMap<&str, Vec<&str>> = HashMap::new();
for obj_data in raw.objects_slice() {
if let Some(type_id) = obj_data.type_id() {
let type_name = raw.type_(type_id).name();
type_usage
.entry(type_name)
.or_default()
.push(obj_data.name());
}
}
println!("\n Type usage index:");
let mut entries: Vec<_> = type_usage.iter().collect();
entries.sort_by_key(|(name, _)| *name);
for (type_name, objects) in &entries {
if objects.iter().any(|o| o.starts_with("raw")) {
println!(" {:<20} used by {}", type_name, objects.join(", "));
}
}
// ---------------------------------------------------------------
// 7. OID tree direct access
//
// raw.tree() gives access to the OidTree with walk_oid(),
// subtree(), all_nodes(), and longest_prefix_from(). The node
// BTreeMap<u32, NodeId> children are in arc order, which matters
// for ordered tree walks in an OID browser.
// ---------------------------------------------------------------
println!("\n=== OID tree ===");
// Walk to a subtree and enumerate children with their arcs.
let scalars_id = raw.resolve("rawScalars").unwrap();
let scalars = raw.node(scalars_id);
println!(" Children of {} (arc order):", scalars.name());
for (arc, &child_id) in scalars.children() {
let child = raw.node(child_id);
let kind = child.kind();
println!(" arc {arc}: {:<20} [{kind:?}]", child.name());
}
// Longest prefix match (for instance OID resolution).
let instance_oid: mib_rs::Oid = "1.3.6.1.4.1.99990.1.1.42".parse().unwrap();
let prefix_id = raw.longest_prefix_by_oid(&instance_oid);
let prefix = raw.node(prefix_id);
println!("\n Longest prefix for {}: {}", instance_oid, prefix.name());
// Effective module ownership for a node.
if let Some(mod_id) = raw.effective_module(prefix_id) {
println!(" Effective owner: {}", raw.module(mod_id).name());
}
// Depth-first subtree iteration via the tree.
let tree = raw.tree();
let subtree_count = tree.subtree(scalars_id).count();
println!(
"\n Subtree size of {}: {} nodes",
scalars.name(),
subtree_count
);
// ---------------------------------------------------------------
// 8. Cross-reference queries
//
// Mib-level queries that return IDs rather than handles, useful
// for building reference indices.
// ---------------------------------------------------------------
println!("\n=== Cross-references ===");
// Which modules define a given symbol?
let definers = mib.modules_defining("rawDeviceName");
println!(" 'rawDeviceName' defined in:");
for mod_id in &definers {
println!(" {}", raw.module(*mod_id).name());
}
// Which modules import DisplayString?
let importers = mib.modules_importing("DisplayString");
println!(" 'DisplayString' imported by:");
for mod_id in &importers {
println!(" {}", raw.module(*mod_id).name());
}
// Find all objects of a given base type.
let counters = mib.objects_by_base_type(mib_rs::BaseType::Integer32);
println!(
"\n Objects with effective base Integer32: {}",
counters.len()
);
for id in counters.iter().take(5) {
let obj = raw.object(*id);
let mod_name = obj
.module()
.map(|mid| raw.module(mid).name())
.unwrap_or("?");
println!(" {}::{}", mod_name, obj.name());
}
// Find all objects using a specific named type.
let by_type = mib.objects_by_type_name("RawName");
println!("\n Objects with type 'RawName':");
for id in &by_type {
println!(" {}", raw.object(*id).name());
}
// ---------------------------------------------------------------
// 9. Combining handle and raw access
//
// The raw and handle APIs are views over the same data. You can
// freely cross between them: handle.id() drops to raw, and
// mib.*_by_id(id) lifts back to a handle. Use handles for
// navigation, raw for bulk work and span access.
// ---------------------------------------------------------------
println!("\n=== Crossing between handle and raw ===");
// Start with a handle, drop to raw for span info.
let handle = mib.object("rawCount").unwrap();
let id = handle.id(); // -> ObjectId
let data = raw.object(id); // -> &ObjectData
let (syn_line, _) = mod_data.line_col(data.syntax_span().start);
let (acc_line, _) = mod_data.line_col(data.access_span().start);
println!(
" {}: SYNTAX at line {}, MAX-ACCESS at line {}",
handle.name(),
syn_line,
acc_line
);
// Start with raw, lift to handle for navigation.
if let Some(type_id) = data.type_id() {
let type_handle = mib.type_by_id(type_id);
println!(
" Type chain: {} -> effective base {:?}",
type_handle.name(),
type_handle.effective_base()
);
}
}§Tokenization
Lexical tokenization of MIB source text for syntax highlighting, linting, or custom tooling.
//! Lexical tokenization of MIB source text.
//!
//! The token API lets you work with raw MIB syntax without parsing,
//! useful for syntax highlighting, linting, or custom tooling.
use mib_rs::token::{self, TokenKind};
fn main() {
let source = br#"EXAMPLE-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
FROM SNMPv2-SMI;
exampleMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example Corp"
CONTACT-INFO "support@example.com"
DESCRIPTION "An example."
::= { enterprises 99999 }
exValue OBJECT-TYPE
SYNTAX Integer32 (0..100)
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A value."
::= { exampleMib 1 }
END
"#;
// -- Tokenize --
let (tokens, diagnostics) = token::tokenize(source);
println!("=== Tokens ({} total) ===", tokens.len());
for tok in &tokens {
// Extract the token text from the source bytes.
let start = tok.span.start.0 as usize;
let end = tok.span.end.0 as usize;
let text = std::str::from_utf8(&source[start..end]).unwrap_or("<binary>");
// Skip comments for brevity.
if tok.kind == TokenKind::Comment {
continue;
}
let category = classify(tok.kind);
println!(
" {:<24} {:<14} {:?}",
text,
tok.kind.libsmi_name(),
category,
);
// Stop at EOF.
if tok.kind == TokenKind::Eof {
break;
}
}
// -- Diagnostics from lexing --
if !diagnostics.is_empty() {
println!("\nLexer diagnostics:");
for d in &diagnostics {
println!(" {:?}", d);
}
} else {
println!("\nNo lexer diagnostics.");
}
// -- Token classification predicates --
println!("\n=== Token classification ===");
let interesting = [
TokenKind::KwDefinitions,
TokenKind::KwObjectType,
TokenKind::KwModuleIdentity,
TokenKind::KwSyntax,
TokenKind::KwInteger,
TokenKind::KwReadOnly,
TokenKind::KwCurrent,
TokenKind::UppercaseIdent,
TokenKind::LowercaseIdent,
TokenKind::Number,
TokenKind::QuotedString,
TokenKind::ColonColonEqual,
TokenKind::LBrace,
];
for kind in interesting {
println!(
" {:<24} keyword={:<5} macro={:<5} clause={:<5} type={:<5} ident={:<5} status/access={}",
kind.libsmi_name(),
kind.is_keyword(),
kind.is_macro_keyword(),
kind.is_clause_keyword(),
kind.is_type_keyword(),
kind.is_identifier(),
kind.is_status_access_keyword(),
);
}
// -- Display names (human-readable for error messages) --
println!("\n=== Display names ===");
for kind in [
TokenKind::LBrace,
TokenKind::ColonColonEqual,
TokenKind::KwObjectType,
TokenKind::UppercaseIdent,
TokenKind::Number,
TokenKind::Eof,
] {
println!(
" {:<24} display={:?} libsmi={:?}",
format!("{kind:?}"),
kind.display_name(),
kind.libsmi_name(),
);
}
// -- Count tokens by category --
println!("\n=== Token statistics ===");
let mut keywords = 0;
let mut identifiers = 0;
let mut literals = 0;
let mut punctuation = 0;
let mut other = 0;
for tok in &tokens {
if tok.kind == TokenKind::Eof || tok.kind == TokenKind::Comment {
continue;
}
match classify(tok.kind) {
"keyword" => keywords += 1,
"identifier" => identifiers += 1,
"literal" => literals += 1,
"punctuation" => punctuation += 1,
_ => other += 1,
}
}
println!(" Keywords: {keywords}");
println!(" Identifiers: {identifiers}");
println!(" Literals: {literals}");
println!(" Punctuation: {punctuation}");
println!(" Other: {other}");
}
fn classify(kind: TokenKind) -> &'static str {
if kind.is_keyword() {
"keyword"
} else if kind.is_identifier() {
"identifier"
} else if matches!(
kind,
TokenKind::Number
| TokenKind::NegativeNumber
| TokenKind::QuotedString
| TokenKind::HexString
| TokenKind::BinString
) {
"literal"
} else if matches!(
kind,
TokenKind::LBrace
| TokenKind::RBrace
| TokenKind::LParen
| TokenKind::RParen
| TokenKind::LBracket
| TokenKind::RBracket
| TokenKind::Comma
| TokenKind::Semicolon
| TokenKind::Colon
| TokenKind::Dot
| TokenKind::DotDot
| TokenKind::Pipe
| TokenKind::Minus
| TokenKind::ColonColonEqual
) {
"punctuation"
} else {
"other"
}
}§Sources
Source types: in-memory modules, directory sources, chaining, and module listing.
//! Source types: in-memory modules, directory sources, chaining,
//! and module listing.
use mib_rs::Loader;
fn main() {
// -- Single in-memory module --
println!("=== Memory source (single) ===");
let source = mib_rs::source::memory(
"MEM-MIB",
br#"MEM-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, enterprises
FROM SNMPv2-SMI;
memMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "In-memory module."
::= { enterprises 11111 }
END
"#
.as_slice(),
);
let mib = Loader::new()
.source(source)
.modules(["MEM-MIB"])
.load()
.expect("should load");
println!(" Loaded: {}", mib.module("MEM-MIB").unwrap().name());
// -- Multiple in-memory modules --
println!("\n=== Memory source (multiple) ===");
let source = mib_rs::source::memory_modules(vec![
(
"MULTI-A-MIB",
br#"MULTI-A-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, enterprises
FROM SNMPv2-SMI;
multiAMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "Module A."
::= { enterprises 22221 }
END
"#
.as_slice(),
),
(
"MULTI-B-MIB",
br#"MULTI-B-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, enterprises
FROM SNMPv2-SMI;
multiBMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "Module B."
::= { enterprises 22222 }
END
"#
.as_slice(),
),
]);
let mib = Loader::new()
.source(source)
.modules(["MULTI-A-MIB", "MULTI-B-MIB"])
.load()
.expect("should load");
for module in mib.modules() {
if !module.is_base() {
println!(" Loaded: {}", module.name());
}
}
// -- Source chaining --
// chain() combines multiple sources; first match wins.
println!("\n=== Chained sources ===");
let primary = mib_rs::source::memory(
"PRIMARY-MIB",
br#"PRIMARY-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, enterprises
FROM SNMPv2-SMI;
primaryMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "Primary source."
::= { enterprises 33331 }
END
"#
.as_slice(),
);
let fallback = mib_rs::source::memory(
"FALLBACK-MIB",
br#"FALLBACK-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, enterprises
FROM SNMPv2-SMI;
fallbackMib MODULE-IDENTITY
LAST-UPDATED "202603120000Z"
ORGANIZATION "Example"
CONTACT-INFO "Example"
DESCRIPTION "Fallback source."
::= { enterprises 33332 }
END
"#
.as_slice(),
);
let chained = mib_rs::source::chain(vec![primary, fallback]);
let mib = Loader::new()
.source(chained)
.modules(["PRIMARY-MIB", "FALLBACK-MIB"])
.load()
.expect("should load");
for module in mib.modules() {
if !module.is_base() {
println!(" Loaded: {}", module.name());
}
}
// -- Module listing from a source --
// Sources can list what modules they contain.
println!("\n=== Listing modules from a source ===");
let source = mib_rs::source::memory_modules(vec![
("LIST-A", b"LIST-A DEFINITIONS ::= BEGIN END".as_slice()),
("LIST-B", b"LIST-B DEFINITIONS ::= BEGIN END".as_slice()),
("LIST-C", b"LIST-C DEFINITIONS ::= BEGIN END".as_slice()),
]);
let modules = source.list_modules().expect("should list");
println!(" Available modules: {}", modules.join(", "));
// -- FindResult gives you the raw content and path --
let result = source.find("LIST-A").expect("should not error");
if let Some(found) = result {
println!(
" Found LIST-A: path={:?}, size={} bytes",
found.path,
found.content.len()
);
}
// -- Loading without specifying modules loads all available --
println!("\n=== Load all available modules ===");
let source = mib_rs::source::memory_modules(vec![
(
"ALL-A-MIB",
br#"ALL-A-MIB DEFINITIONS ::= BEGIN
IMPORTS MODULE-IDENTITY, enterprises FROM SNMPv2-SMI;
allAMib MODULE-IDENTITY LAST-UPDATED "202603120000Z"
ORGANIZATION "Example" CONTACT-INFO "Example"
DESCRIPTION "A." ::= { enterprises 44441 }
END
"#
.as_slice(),
),
(
"ALL-B-MIB",
br#"ALL-B-MIB DEFINITIONS ::= BEGIN
IMPORTS MODULE-IDENTITY, enterprises FROM SNMPv2-SMI;
allBMib MODULE-IDENTITY LAST-UPDATED "202603120000Z"
ORGANIZATION "Example" CONTACT-INFO "Example"
DESCRIPTION "B." ::= { enterprises 44442 }
END
"#
.as_slice(),
),
]);
let mib = Loader::new()
.source(source)
.load()
.expect("should load all");
let user_modules: Vec<_> = mib.modules().filter(|m| !m.is_base()).collect();
println!(" Loaded {} user modules", user_modules.len());
for m in &user_modules {
println!(" {}", m.name());
}
// -- Directory source (commented out - requires actual MIB files on disk) --
// let source = mib_rs::source::dir("/usr/share/snmp/mibs").expect("dir exists");
// let mib = Loader::new()
// .source(source)
// .modules(["IF-MIB"])
// .load()
// .expect("should load");
// -- System path auto-discovery (commented out - requires net-snmp/libsmi installed) --
// let mib = Loader::new()
// .system_paths()
// .modules(["IF-MIB", "SNMPv2-MIB"])
// .load()
// .expect("should load");
}Re-exports§
pub use error::LoadError;pub use load::Loader;pub use load::load;pub use mib::Capability;pub use mib::Compliance;pub use mib::Group;pub use mib::Index;pub use mib::Mib;pub use mib::Module;pub use mib::Node;pub use mib::Notification;pub use mib::Object;pub use mib::Oid;pub use mib::OidLookup;pub use mib::ParseOidError;pub use mib::ResolveOidError;pub use mib::Type;pub use mib::index::DecodedIndex;pub use mib::index::IndexValue;pub use source::FindResult;pub use source::Source;pub use token::Token;pub use token::TokenKind;pub use types::Access;pub use types::AccessKeyword;pub use types::BaseType;pub use types::DiagCode;pub use types::Diagnostic;pub use types::DiagnosticConfig;pub use types::IndexEncoding;pub use types::Kind;pub use types::Language;pub use types::ReportingLevel;pub use types::ResolverStrictness;pub use types::Severity;pub use types::Status;
Modules§
- ast
- Abstract syntax tree types produced by the parser.
- compile
- Compiler pipeline APIs exposed before final resolution.
- error
- Error types for the MIB loading pipeline.
- export
- JSON export of a resolved
Mib. - ir
- Intermediate representation produced by lowering the AST.
- load
- MIB loading pipeline: source discovery, parallel parsing, and resolution.
- lower
- Lowering pass: transforms an
ast::Moduleinto anir::Module. - mib
- Resolved MIB model and query API.
- parser
- Recursive descent parser for SMI MIB modules.
- raw
- Low-level resolved data access.
- searchpath
- System MIB path discovery for net-snmp and libsmi.
- source
- MIB source implementations for the loading pipeline.
- token
- Public token types and tokenization entry point.
- types
- Shared types used across the MIB parsing pipeline.