Skip to main content

Crate mib_rs

Crate mib_rs 

Source
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:

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:

ModuleSMI versionDefines
SNMPv2-SMISMIv2Core types (Integer32, Counter32, etc.), OID roots (internet, enterprises, mib-2), macros (MODULE-IDENTITY, OBJECT-TYPE, NOTIFICATION-TYPE, OBJECT-IDENTITY)
SNMPv2-TCSMIv2TEXTUAL-CONVENTION macro, standard TCs (DisplayString, TruthValue, RowStatus, etc.)
SNMPv2-CONFSMIv2Conformance macros (MODULE-COMPLIANCE, OBJECT-GROUP, NOTIFICATION-GROUP, AGENT-CAPABILITIES)
RFC1155-SMISMIv1SMIv1 base types and OID roots
RFC1065-SMISMIv1Earlier SMIv1 base (predecessor to RFC1155-SMI)
RFC-1212SMIv1SMIv1 OBJECT-TYPE macro definition
RFC-1215SMIv1SMIv1 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. Use Module::is_base to 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. The source_path for base modules is empty.
  • Included in iteration: Mib::modules, Mib::objects, Mib::types, and Mib::nodes all include base module content. Filter with Module::is_base when 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-UPDATED timestamps 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 Notification and Notification::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 Group and Group::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:

FormExampleDescription
Plain namesysDescrLooks up by object/node name across all modules
Qualified nameSNMPv2-MIB::sysDescrScoped to a specific module
Instance OIDifDescr.7Name with numeric suffix appended
Numeric OID1.3.6.1.2.1.1.1Dotted 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:

ConstructorDescription
source::file()Single file on disk, module name auto-detected
source::files()Multiple files on disk, module names auto-detected
source::dirRecursively indexes a directory tree on disk
source::dirsChains multiple directory trees
source::memorySingle in-memory module (for tests or embedding)
source::memory_modulesMultiple in-memory modules
source::chainCombines 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 by DisplayString)
  • "1x:" - hex bytes separated by colons (used by MacAddress)
  • "2d-1d-1d,1d:1d:1d.1d" - date-time components (used by DateAndTime)

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.

MethodDescription
Type::baseDirectly assigned base type (may be Unknown for derived types)
Type::effective_baseResolved base type - use this one
Type::parentImmediate parent type (if derived)
Type::display_hintThis type’s own DISPLAY-HINT (often empty)
Type::effective_display_hintFirst non-empty hint in the chain - use this one
Type::enumsThis type’s own enum values
Type::effective_enumsFirst non-empty enums in the chain - use this one
Type::sizes / Type::rangesThis type’s own constraints
Type::effective_sizes / Type::effective_rangesInherited constraints - use these
Type::is_textual_conventionWhether 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.

LevelBehaviorCorrectness riskWhen to use
StrictMinimal 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.
PermissiveAll 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-TC defines a textual convention AcmeStatus, and ACME-MIB imports 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
    END

    A third module might then mistakenly import AcmeStatus from ACME-MIB instead of from ACME-TC:

    ACME-EXTENSION-MIB DEFINITIONS ::= BEGIN
      IMPORTS AcmeStatus FROM ACME-MIB;  -- wrong: ACME-MIB doesn't define it
    END

    The resolver handles this by checking ACME-MIB’s own IMPORTS, finding that it declares AcmeStatus 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, and BITS always resolve from SNMPv2-SMI even without an explicit import.

  • Well-known OID roots: iso, ccitt, and joint-iso-ccitt always 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-v1 to SNMPv2-SMI, RFC-1213 to RFC1213-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, and DisplayString feel 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
        ...
    END

    When 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:

PresetWhat’s reportedload() fails atWhen to use
DiagnosticConfig::verbose()Everything (style, info, warnings)SevereDebugging MIB issues, understanding what the resolver did.
DiagnosticConfig::default()Minor and aboveSevereGeneral use.
DiagnosticConfig::quiet()Errors and above onlySevereProduction code that just wants to know about real problems.
DiagnosticConfig::silent()NothingFatal onlyWhen 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 causes load() to return an error. For example, set to Severity::Minor to 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

FeatureDefaultDescription
serdeyesSerde support and JSON export via export
cliyesCLI 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::Module into an ir::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.