mib_rs/lib.rs
1//! SNMP MIB parsing, resolution, query, and tooling APIs.
2//!
3//! # What is a MIB?
4//!
5//! A MIB (Management Information Base) is a text file that describes the
6//! structure of data available from an SNMP-managed device. Each piece of
7//! data (a counter, a name, a status flag, a table row) is identified by an
8//! OID (Object Identifier), a dotted-decimal path like `1.3.6.1.2.1.1.1`.
9//! MIB files give those numeric OIDs human-readable names, types, and
10//! descriptions, so instead of `1.3.6.1.2.1.1.1` you can say `sysDescr`.
11//!
12//! MIBs are written in a language called SMI (Structure of Management
13//! Information), which has two versions: SMIv1 (RFC 1155/1212) and SMIv2
14//! (RFC 2578/2579/2580). This crate handles both transparently.
15//!
16//! # API Layers
17//!
18//! Most callers should use the **handle API**: start with [`Loader`],
19//! get a [`Mib`], and navigate the resolved model through borrowed
20//! handle types ([`Node`], [`Object`], [`Type`], [`Module`],
21//! [`Notification`], [`Group`], [`Compliance`], [`Capability`]).
22//! Handles wrap an arena ID and a `&Mib` reference. Methods on
23//! handles return further handles, so typical usage looks like
24//! `object.ty()?.effective_base()` without touching IDs directly.
25//! The handle API covers OID resolution, type chain introspection,
26//! table/index navigation, module iteration, diagnostics, and
27//! everything else documented in the sections below.
28//!
29//! Every handle exposes its arena ID via `.id()`. IDs are
30//! `Copy + Eq + Hash + Ord`, so you can store them in collections
31//! for deduplication or cross-referencing, then convert back to
32//! handles with `mib.*_by_id()` when you need to query again.
33//!
34//! [`Mib`] also has query methods that work with IDs directly:
35//! [`Mib::modules_defining`] and [`Mib::modules_importing`] find
36//! modules by symbol name, [`Mib::objects_by_base_type`] and
37//! [`Mib::objects_by_type_name`] filter objects by type, and
38//! [`Mib::available_symbols`] returns everything visible in a
39//! module's scope (own definitions plus resolved imports).
40//!
41//! ## Raw data access
42//!
43//! [`Mib::raw()`] returns a [`RawMib`](raw::RawMib) view that
44//! exposes the arena-backed data records directly. This is useful
45//! when you need things the handle API doesn't surface:
46//!
47//! - Per-clause source spans on [`ObjectData`](raw::ObjectData)
48//! and [`TypeData`](raw::TypeData) (e.g. `syntax_span`,
49//! `access_span`) for pointing diagnostics at specific clauses.
50//! - Import metadata ([`ModuleData::is_import_used`](raw::ModuleData::is_import_used),
51//! [`ModuleData::import_source`](raw::ModuleData::import_source)).
52//! - Symbolic OID references via `oid_refs()` on entity records.
53//! - Bulk arena slices (`raw.*_slice()`) for batch analysis.
54//!
55//! See the [`raw`] module and the `raw` example.
56//!
57//! ## Compiler pipeline
58//!
59//! The [`ast`], [`parser`], [`lower`], [`ir`], and [`token`]
60//! modules expose pre-resolution stages for callers that need
61//! syntax-aware analysis before full resolution. The parser
62//! produces partial ASTs from broken input, which matters for
63//! editor integration where the user is mid-edit. Token types
64//! carry classification predicates for syntax highlighting.
65//! See the [`compile`] module and the `tokens` example.
66//!
67//! # Loading MIBs
68//!
69//! Use [`Loader`] to configure sources, select modules, and run the pipeline:
70//!
71//! ```rust
72//! use mib_rs::{BaseType, Loader};
73//!
74//! fn example_mib() -> mib_rs::Mib {
75//! let source = mib_rs::source::memory(
76//! "DOC-EXAMPLE-MIB",
77//! r#"DOC-EXAMPLE-MIB DEFINITIONS ::= BEGIN
78//! IMPORTS
79//! MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
80//! FROM SNMPv2-SMI
81//! TEXTUAL-CONVENTION, DisplayString
82//! FROM SNMPv2-TC;
83//!
84//! docExampleMib MODULE-IDENTITY
85//! LAST-UPDATED "202603120000Z"
86//! ORGANIZATION "Example"
87//! CONTACT-INFO "Example"
88//! DESCRIPTION "Example module used in crate docs."
89//! ::= { enterprises 99999 }
90//!
91//! DocName ::= TEXTUAL-CONVENTION
92//! DISPLAY-HINT "255a"
93//! STATUS current
94//! DESCRIPTION "Example display string type."
95//! SYNTAX DisplayString (SIZE (0..255))
96//!
97//! docScalars OBJECT IDENTIFIER ::= { docExampleMib 1 }
98//! docTables OBJECT IDENTIFIER ::= { docExampleMib 2 }
99//!
100//! docDeviceName OBJECT-TYPE
101//! SYNTAX DocName
102//! MAX-ACCESS read-only
103//! STATUS current
104//! DESCRIPTION "A scalar object."
105//! ::= { docScalars 1 }
106//!
107//! docTable OBJECT-TYPE
108//! SYNTAX SEQUENCE OF DocEntry
109//! MAX-ACCESS not-accessible
110//! STATUS current
111//! DESCRIPTION "Example table."
112//! ::= { docTables 1 }
113//!
114//! docEntry OBJECT-TYPE
115//! SYNTAX DocEntry
116//! MAX-ACCESS not-accessible
117//! STATUS current
118//! DESCRIPTION "Example row."
119//! INDEX { docIndex }
120//! ::= { docTable 1 }
121//!
122//! DocEntry ::= SEQUENCE {
123//! docIndex Integer32,
124//! docDescr DisplayString
125//! }
126//!
127//! docIndex OBJECT-TYPE
128//! SYNTAX Integer32 (1..2147483647)
129//! MAX-ACCESS not-accessible
130//! STATUS current
131//! DESCRIPTION "Example index."
132//! ::= { docEntry 1 }
133//!
134//! docDescr OBJECT-TYPE
135//! SYNTAX DisplayString
136//! MAX-ACCESS read-only
137//! STATUS current
138//! DESCRIPTION "Example column."
139//! ::= { docEntry 2 }
140//!
141//! END
142//! "#,
143//! );
144//!
145//! Loader::new()
146//! .source(source)
147//! .modules(["DOC-EXAMPLE-MIB"])
148//! .load()
149//! .expect("example MIB should load")
150//! }
151//!
152//! let mib = example_mib();
153//! let object = mib.object("docDeviceName").expect("object should exist");
154//! let ty = object.ty().expect("object should have a type");
155//!
156//! assert_eq!(object.name(), "docDeviceName");
157//! assert_eq!(ty.name(), "DocName");
158//! assert_eq!(ty.effective_base(), BaseType::OctetString);
159//! assert_eq!(ty.effective_display_hint(), "255a");
160//! ```
161//!
162//! # OIDs and Resolution
163//!
164//! Every named element in a MIB has an OID, a path through a global tree
165//! shared by all SNMP devices. OIDs are written as dotted decimal
166//! (`1.3.6.1.2.1.1.1`) or symbolically (`sysDescr`). The tree is
167//! hierarchical: `enterprises` is `1.3.6.1.4.1`, and a vendor's subtree
168//! hangs beneath that.
169//!
170//! **Instance OIDs** extend a base OID with a suffix that identifies a
171//! specific value. For a scalar like `sysDescr`, the instance is always
172//! `sysDescr.0`. For table columns, the suffix encodes the row's index
173//! values, e.g. `ifDescr.7` for interface 7.
174//!
175//! This crate resolves both directions: name to numeric OID, and numeric
176//! OID back to its closest named node.
177//!
178//! ```rust
179//! fn example_mib() -> mib_rs::Mib {
180//! let source = mib_rs::source::memory(
181//! "DOC-EXAMPLE-MIB",
182//! include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
183//! );
184//!
185//! mib_rs::Loader::new()
186//! .source(source)
187//! .modules(["DOC-EXAMPLE-MIB"])
188//! .load()
189//! .expect("example MIB should load")
190//! }
191//!
192//! let mib = example_mib();
193//!
194//! let column_oid = mib.resolve_oid("docDescr").expect("OID should resolve");
195//! assert_eq!(column_oid.to_string(), "1.3.6.1.4.1.99999.2.1.1.2");
196//!
197//! let node = mib
198//! .exact_node_by_oid(&column_oid)
199//! .expect("exact node should exist");
200//! assert_eq!(node.name(), "docDescr");
201//!
202//! let instance_node = mib
203//! .resolve_node("docDescr.7")
204//! .expect("instance OID should resolve to its base node");
205//! assert_eq!(instance_node.name(), "docDescr");
206//!
207//! let instance_oid = mib.resolve_oid("docDescr.7").expect("instance OID should resolve");
208//! assert_eq!(instance_oid.to_string(), "1.3.6.1.4.1.99999.2.1.1.2.7");
209//! assert_eq!(mib.lookup_oid(&instance_oid).name(), "docDescr");
210//! assert_eq!(mib.lookup_oid(&"1.3.6.1.4.1.99999.2.1.1.2.99".parse().unwrap()).name(), "docDescr");
211//! ```
212//!
213//! # Tables and Indexes
214//!
215//! SNMP models tabular data as three nested objects:
216//!
217//! - A **table** (`SEQUENCE OF`) is a container, not directly readable.
218//! - A **row** (entry) represents one row, also not directly readable.
219//! It declares which columns are **index** columns, whose values
220//! together form the instance suffix that identifies each row.
221//! - **Columns** are the actual readable/writable values. Each column's
222//! full OID is the column OID plus the index suffix.
223//!
224//! For example, `ifTable` contains `ifEntry` rows indexed by `ifIndex`.
225//! The column `ifDescr` for interface 7 has OID `ifDescr.7` (i.e.
226//! the column's base OID with the index value `7` appended).
227//!
228//! ## AUGMENTS
229//!
230//! Some rows use `AUGMENTS` instead of `INDEX`. An augmenting row
231//! extends another table's rows with additional columns, sharing the
232//! same index structure. For example, `ifXEntry AUGMENTS ifEntry`
233//! adds columns like `ifHighSpeed` to each `ifEntry` row, using the
234//! same `ifIndex` to identify rows. Use [`Object::augments`] to find
235//! the target row and [`Object::augmented_by`] to find extending rows.
236//! [`Object::effective_indexes`] follows the augment chain
237//! automatically, returning the inherited index list.
238//!
239//! ## Index encoding
240//!
241//! Each index component has an [`IndexEncoding`] that describes how
242//! its value maps to OID sub-identifiers in the instance suffix.
243//! Integer indexes use a single sub-identifier. Fixed-length strings
244//! (with a single-value SIZE constraint) use one sub-identifier per
245//! octet. Variable-length strings are length-prefixed. The `IMPLIED`
246//! keyword omits the length prefix, relying on the index being the
247//! last component. [`Index::encoding`] returns the derived encoding.
248//!
249//! Use [`mib::index::decode_suffix`] to decode raw OID suffix arcs
250//! into typed [`IndexValue`]s, or call [`OidLookup::decode_indexes`]
251//! for the common case of processing a varbind OID.
252//!
253//! Use [`Object::is_table`], [`Object::is_row`], [`Object::is_column`],
254//! and [`Object::is_scalar`] to distinguish these, or use the filtered
255//! iterators like [`Mib::tables`] and [`Mib::scalars`].
256//!
257//! ```rust
258//! fn example_mib() -> mib_rs::Mib {
259//! let source = mib_rs::source::memory(
260//! "DOC-EXAMPLE-MIB",
261//! include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
262//! );
263//!
264//! mib_rs::Loader::new()
265//! .source(source)
266//! .modules(["DOC-EXAMPLE-MIB"])
267//! .load()
268//! .expect("example MIB should load")
269//! }
270//!
271//! let mib = example_mib();
272//! let table = mib.object("docTable").expect("table should exist");
273//! let row = table.row().expect("table should have a row");
274//!
275//! let column_names: Vec<_> = table.columns().map(|col| col.name()).collect();
276//! assert_eq!(column_names, vec!["docIndex", "docDescr"]);
277//!
278//! let indexes: Vec<_> = row.effective_indexes().collect();
279//! assert_eq!(indexes.len(), 1);
280//! assert_eq!(indexes[0].row().name(), "docEntry");
281//! let index_object = indexes[0].object().expect("index object");
282//! let index_type = indexes[0].ty().expect("index type");
283//! assert_eq!(index_object.name(), "docIndex");
284//! assert_eq!(indexes[0].name(), "docIndex");
285//! assert_eq!(index_type.name(), "Integer32");
286//! ```
287//!
288//! # Modules
289//!
290//! A MIB file contains one module (e.g. `IF-MIB`, `SNMPv2-MIB`). Modules
291//! import symbols from other modules, so loading one module typically
292//! pulls in its dependencies automatically.
293//!
294//! ## Base modules
295//!
296//! Seven **base modules** are built into the library and always available:
297//!
298//! | Module | SMI version | Defines |
299//! |--------|-------------|---------|
300//! | `SNMPv2-SMI` | SMIv2 | Core types (`Integer32`, `Counter32`, etc.), OID roots (`internet`, `enterprises`, `mib-2`), macros (`MODULE-IDENTITY`, `OBJECT-TYPE`, `NOTIFICATION-TYPE`, `OBJECT-IDENTITY`) |
301//! | `SNMPv2-TC` | SMIv2 | `TEXTUAL-CONVENTION` macro, standard TCs (`DisplayString`, `TruthValue`, `RowStatus`, etc.) |
302//! | `SNMPv2-CONF` | SMIv2 | Conformance macros (`MODULE-COMPLIANCE`, `OBJECT-GROUP`, `NOTIFICATION-GROUP`, `AGENT-CAPABILITIES`) |
303//! | `RFC1155-SMI` | SMIv1 | SMIv1 base types and OID roots |
304//! | `RFC1065-SMI` | SMIv1 | Earlier SMIv1 base (predecessor to RFC1155-SMI) |
305//! | `RFC-1212` | SMIv1 | SMIv1 `OBJECT-TYPE` macro definition |
306//! | `RFC-1215` | SMIv1 | SMIv1 `TRAP-TYPE` macro definition |
307//!
308//! These modules define the SMI language itself, specifically the ASN.1
309//! macros (`OBJECT-TYPE`, `MODULE-IDENTITY`, `TEXTUAL-CONVENTION`, etc.)
310//! that all other MIB modules use. The library constructs them
311//! programmatically rather than parsing them from files, because they
312//! contain ASN.1 MACRO definitions that require a general ASN.1 macro
313//! parser to process. Since RFC 2578 Section 3 explicitly prohibits
314//! user-defined macros in MIB modules ("Additional ASN.1 macros must not
315//! be defined in SMIv2 information modules"), the library only needs to
316//! handle the fixed set of macros defined by the SMI RFCs.
317//!
318//! Implications for users:
319//!
320//! - **No files needed:** You do not need to supply these modules as source
321//! files. If they exist on disk in a source directory, the synthetic
322//! versions take priority and the files are not parsed.
323//! - **Always present:** Base modules are included in every loaded [`Mib`],
324//! even if nothing imports them. Use [`Module::is_base`] to distinguish
325//! them from user-supplied modules (e.g. when iterating modules).
326//! - **No source spans:** Definitions from base modules carry synthetic
327//! span values ([`Span::SYNTHETIC`](crate::types::Span::SYNTHETIC))
328//! rather than real byte offsets, since there is no parsed source text.
329//! The `source_path` for base modules is empty.
330//! - **Included in iteration:** [`Mib::modules`], [`Mib::objects`],
331//! [`Mib::types`], and [`Mib::nodes`] all include base module content.
332//! Filter with [`Module::is_base`] when you only want user-supplied
333//! definitions. Module-scoped iterators (e.g. `module.objects()`) are
334//! naturally limited to a single module.
335//!
336//! ## OID ownership
337//!
338//! Several base modules define overlapping OID trees. For example, both
339//! `RFC1155-SMI` (SMIv1) and `SNMPv2-SMI` (SMIv2) define `internet`,
340//! `enterprises`, and other well-known roots. When multiple modules
341//! register the same OID, the resolver determines which module "owns"
342//! the node using these tiebreakers, in order:
343//!
344//! - Base modules take priority over user modules.
345//! - SMIv2 modules are preferred over SMIv1.
346//! - Among modules with the same SMI version, newer `LAST-UPDATED`
347//! timestamps win.
348//! - Lexicographic module name as a final deterministic fallback.
349//!
350//! In practice this means `SNMPv2-SMI` owns nodes like `enterprises`
351//! even though `RFC1155-SMI` also defines them. [`Node::module`] returns
352//! the winning module. Both modules still function normally for imports,
353//! so SMIv1 MIBs that `IMPORTS ... FROM RFC1155-SMI` continue to work.
354//!
355//! Use [`Module`] handles to scope lookups and iteration to a single
356//! module:
357//!
358//! ```rust
359//! fn example_mib() -> mib_rs::Mib {
360//! let source = mib_rs::source::memory(
361//! "DOC-EXAMPLE-MIB",
362//! include_bytes!("../tests/data/doc-example-mib.txt").as_slice(),
363//! );
364//!
365//! mib_rs::Loader::new()
366//! .source(source)
367//! .modules(["DOC-EXAMPLE-MIB"])
368//! .load()
369//! .expect("example MIB should load")
370//! }
371//!
372//! let mib = example_mib();
373//! let module = mib.module("DOC-EXAMPLE-MIB").expect("module should exist");
374//!
375//! assert_eq!(module.object("docDeviceName").unwrap().module(), Some(module));
376//!
377//! let object_names: Vec<_> = module.objects().map(|obj| obj.name()).collect();
378//! assert!(object_names.contains(&"docTable"));
379//! assert!(object_names.contains(&"docDescr"));
380//!
381//! let type_names: Vec<_> = module.types().map(|ty| ty.name()).collect();
382//! assert!(type_names.contains(&"DocName"));
383//! ```
384//!
385//! # Notifications and Conformance
386//!
387//! Beyond objects and types, SMI defines several constructs for
388//! event reporting and conformance testing:
389//!
390//! - **NOTIFICATION-TYPE** (SMIv2) / **TRAP-TYPE** (SMIv1) - defines
391//! an asynchronous event an agent can send. Each notification lists
392//! the objects it carries as payload via its OBJECTS clause. SMIv1
393//! traps additionally carry an enterprise OID and trap number.
394//! See [`Notification`] and [`Notification::objects`].
395//!
396//! - **OBJECT-GROUP** / **NOTIFICATION-GROUP** - bundles related
397//! objects or notifications into a named set. Groups are the unit
398//! of conformance: a compliance statement says "you must implement
399//! these groups". See [`Group`] and [`Group::members`].
400//!
401//! - **MODULE-COMPLIANCE** - declares which groups a compliant
402//! implementation must support, with optional per-object refinements
403//! that can narrow syntax or access requirements. See [`Compliance`].
404//!
405//! - **AGENT-CAPABILITIES** - declares what an actual agent
406//! implementation supports, including which groups it includes and
407//! any per-object variations (restricted syntax, different defaults).
408//! See [`Capability`].
409//!
410//! These are less commonly needed than objects and types, but matter
411//! for MIB validation tooling, compliance checking, and understanding
412//! which objects are required vs optional. The `notifications` example
413//! demonstrates querying all four.
414//!
415//! # Query Formats
416//!
417//! Once a MIB is loaded, you can look up nodes and OIDs using several
418//! formats. Qualified names (`MODULE::name`) are useful when multiple
419//! modules define the same name. [`Mib::resolve_oid`],
420//! [`Mib::resolve_node`], and [`RawMib::resolve`](raw::RawMib::resolve) all accept these forms:
421//!
422//! | Form | Example | Description |
423//! |------|---------|-------------|
424//! | Plain name | `sysDescr` | Looks up by object/node name across all modules |
425//! | Qualified name | `SNMPv2-MIB::sysDescr` | Scoped to a specific module |
426//! | Instance OID | `ifDescr.7` | Name with numeric suffix appended |
427//! | Numeric OID | `1.3.6.1.2.1.1.1` | Dotted decimal, leading dot optional |
428//!
429//! For instance OIDs (both symbolic and numeric), [`Mib::resolve_node`] returns
430//! the deepest matching tree node, while [`Mib::resolve_oid`] returns the full
431//! numeric OID with the suffix included.
432//!
433//! [`Mib::format_oid`] converts a numeric [`Oid`] back to `MODULE::name.suffix`
434//! form using longest-prefix matching.
435//!
436//! # Sources
437//!
438//! Sources tell the loader where to find MIB files. For testing and
439//! embedding, use in-memory sources. For production use, point at
440//! directories on disk or use system path auto-discovery to find MIBs
441//! installed by net-snmp or libsmi. The [`source`] module has several
442//! constructors:
443//!
444//! | Constructor | Description |
445//! |-------------|-------------|
446//! | [`source::file()`] | Single file on disk, module name auto-detected |
447//! | [`source::files()`] | Multiple files on disk, module names auto-detected |
448//! | [`source::dir`] | Recursively indexes a directory tree on disk |
449//! | [`source::dirs`] | Chains multiple directory trees |
450//! | [`source::memory`] | Single in-memory module (for tests or embedding) |
451//! | [`source::memory_modules`] | Multiple in-memory modules |
452//! | [`source::chain`] | Combines multiple sources; first match wins |
453//!
454//! [`Loader::system_paths`](load::Loader::system_paths) auto-discovers
455//! net-snmp and libsmi MIB directories from config files and environment
456//! variables (see [`searchpath`]).
457//!
458//! Module names are derived from file content (scanning for `DEFINITIONS`
459//! headers), not from filenames. Files are matched by extension using
460//! [`source::DEFAULT_EXTENSIONS`] (`.mib`, `.smi`, `.txt`, `.my`, or no
461//! extension).
462//!
463//! # Type Introspection
464//!
465//! SMI types form parent chains. A MIB might define `HostName` as a
466//! refinement of `DisplayString`, which is itself a textual convention
467//! over `OCTET STRING`. Each link in the chain can add constraints
468//! (size limits, value ranges), a display hint (how to render the value
469//! as text), or enumeration labels.
470//!
471//! A **textual convention** (TC) is the standard way to define reusable
472//! types in SMIv2 (RFC 2579). A TC wraps a base type with a name,
473//! description, and optional DISPLAY-HINT and constraints. For example,
474//! `DisplayString` is a TC over `OCTET STRING (SIZE (0..255))` with
475//! display hint `"255a"`. Use [`Type::is_textual_convention`] to check
476//! whether a type was defined as a TC.
477//!
478//! ## Constraints: SIZE vs range
479//!
480//! Both [`Type::sizes`] and [`Type::ranges`] return `&[Range]`, but
481//! they constrain different things:
482//!
483//! - **SIZE** constrains the length (in octets) of string-like types
484//! (`OCTET STRING`, `Opaque`). Example: `SIZE (0..255)` means
485//! at most 255 bytes.
486//! - **Range** constrains the numeric value of integer-like types.
487//! Example: `(1..2147483647)` means the value must be at least 1.
488//!
489//! The `effective_*` variants walk the parent chain to find inherited
490//! constraints.
491//!
492//! ## Display hints
493//!
494//! A DISPLAY-HINT string (RFC 2579, Section 3) tells a MIB browser or
495//! SNMP tool how to render a raw value as human-readable text. Common
496//! examples:
497//!
498//! - `"255a"` - up to 255 ASCII characters (used by `DisplayString`)
499//! - `"1x:"` - hex bytes separated by colons (used by `MacAddress`)
500//! - `"2d-1d-1d,1d:1d:1d.1d"` - date-time components (used by
501//! `DateAndTime`)
502//!
503//! [`Type::effective_display_hint`] and
504//! [`Object::effective_display_hint`] return the hint string.
505//! [`Object::format_integer`], [`Object::format_octets`], and
506//! [`Object::scale_integer`] apply the hint directly to raw values.
507//! The [`display_hint`](mib::display_hint) module exposes the same
508//! formatting functions for use without an Object handle.
509//!
510//! ## Direct vs effective accessors
511//!
512//! Each [`Type`] handle exposes two families of accessors:
513//!
514//! - **Direct** (`base`, `display_hint`, `enums`, `sizes`, `ranges`) -
515//! return only what this specific type declares. These are empty/None
516//! if the type inherits everything from its parent.
517//! - **Effective** (`effective_base`, `effective_display_hint`,
518//! `effective_enums`, `effective_sizes`, `effective_ranges`) - walk up
519//! the parent chain and return the first non-empty value. These give
520//! you the "resolved" answer.
521//!
522//! **In most cases, use the `effective_*` methods.** They give you the
523//! answer you actually want: "what base type does this ultimately
524//! represent?", "how should I format this value?", "what are the valid
525//! enum labels?". The direct methods are mainly useful when you need to
526//! know exactly where in the chain a property was introduced, for
527//! instance when building a MIB browser that shows the full type
528//! derivation.
529//!
530//! | Method | Description |
531//! |--------|-------------|
532//! | [`Type::base`] | Directly assigned base type (may be `Unknown` for derived types) |
533//! | [`Type::effective_base`] | Resolved base type - use this one |
534//! | [`Type::parent`] | Immediate parent type (if derived) |
535//! | [`Type::display_hint`] | This type's own DISPLAY-HINT (often empty) |
536//! | [`Type::effective_display_hint`] | First non-empty hint in the chain - use this one |
537//! | [`Type::enums`] | This type's own enum values |
538//! | [`Type::effective_enums`] | First non-empty enums in the chain - use this one |
539//! | [`Type::sizes`] / [`Type::ranges`] | This type's own constraints |
540//! | [`Type::effective_sizes`] / [`Type::effective_ranges`] | Inherited constraints - use these |
541//! | [`Type::is_textual_convention`] | Whether defined as a TEXTUAL-CONVENTION |
542//!
543//! Convenience predicates: [`Type::is_counter`], [`Type::is_gauge`],
544//! [`Type::is_string`], [`Type::is_enumeration`], [`Type::is_bits`].
545//! These all use the effective base type internally.
546//!
547//! Objects expose the same effective accessors directly (e.g.
548//! [`Object::effective_display_hint`], [`Object::effective_enums`]) so
549//! you don't need to go through the type handle for common lookups.
550//!
551//! # Diagnostics and Configuration
552//!
553//! Real-world MIB files frequently contain errors, vendor-specific
554//! extensions, or references to modules you don't have. Rather than
555//! failing on the first problem, this library collects diagnostics and
556//! continues, producing as much useful output as possible. After
557//! loading, check [`Mib::has_errors`] and inspect [`Mib::diagnostics`]
558//! for details.
559//!
560//! There are two independent knobs that control loading behavior.
561//! They can seem redundant at first ("don't both of them control how
562//! strict loading is?"), but they operate at different levels.
563//!
564//! Think of it like a Rust analogy: [`ResolverStrictness`] is like
565//! controlling how `use` imports are resolved. In Rust, `use foo::Bar`
566//! must name the exact path. In MIBs, imports work similarly - a module
567//! declares `IMPORTS DisplayString FROM SNMPv2-TC`. But many real-world
568//! MIBs get this wrong: they import from the wrong module, forget to
569//! import well-known names they assume are built-in, or don't declare
570//! imports at all. `ResolverStrictness` controls how hard the resolver
571//! tries to recover from these mistakes, from following only explicit
572//! import chains (`Strict`) to searching all loaded modules by name
573//! (`Permissive`).
574//!
575//! [`DiagnosticConfig`], on the other hand, is like compiler warnings
576//! and `-Werror`. It controls what gets reported across the entire
577//! pipeline (lexing, parsing, and resolution), and whether problems
578//! cause `load()` to fail. It doesn't change what gets resolved.
579//!
580//! The key tradeoff with `ResolverStrictness` is **correctness vs
581//! completeness**. The more permissive you go, the more things get
582//! resolved, but the higher the risk of incorrect results. At
583//! `Permissive`, the resolver falls back to searching all loaded
584//! modules for a matching symbol name. If multiple modules define a
585//! symbol with the same name, you're essentially guessing which one
586//! was intended. At `Strict`, the resolver only follows deterministic
587//! strategies where the source is unambiguous (explicit imports,
588//! import forwarding chains, ASN.1 primitives), so resolved symbols
589//! are traceable back to their origin.
590//!
591//! ## ResolverStrictness - what the resolver attempts
592//!
593//! [`ResolverStrictness`] controls how aggressively the resolver tries
594//! to recover when it can't find a symbol through explicit imports. Set
595//! via [`Loader::resolver_strictness`](load::Loader::resolver_strictness).
596//!
597//! | Level | Behavior | Correctness risk | When to use |
598//! |-------|----------|-----------------|-------------|
599//! | `Strict` | Minimal fallbacks. Only deterministic strategies that don't guess the source module. | Lowest - if it resolves, the import chain is traceable. | Validating MIBs for correctness, linting, CI checks. |
600//! | `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. |
601//! | `Permissive` | All fallbacks, including searching every loaded module for a symbol by name. | Higher - if two modules define `FooStatus`, the resolver picks one. | Loading badly-written vendor MIBs that you can't fix. |
602//!
603//! ### Specific behaviors by level
604//!
605//! **All levels (including Strict):**
606//! - Direct import resolution (symbol found in the named source module).
607//! - Import forwarding: MIB authors often import a symbol from a
608//! module that uses it, not realizing that module doesn't define
609//! the symbol - it imports it from somewhere else. SMI imports
610//! are not transitive (importing from a module only gives you
611//! what that module defines, not what it imports), but many MIB
612//! authors treat them as if they were, similar to how programmers
613//! sometimes confuse which scope a variable is visible in.
614//!
615//! For example, suppose `ACME-TC` defines a textual convention
616//! `AcmeStatus`, and `ACME-MIB` imports and uses it:
617//!
618//! ```text
619//! ACME-TC DEFINITIONS ::= BEGIN
620//! AcmeStatus ::= TEXTUAL-CONVENTION ...
621//! END
622//!
623//! ACME-MIB DEFINITIONS ::= BEGIN
624//! IMPORTS AcmeStatus FROM ACME-TC;
625//! -- uses AcmeStatus in OBJECT-TYPE definitions
626//! END
627//! ```
628//!
629//! A third module might then mistakenly import `AcmeStatus` from
630//! `ACME-MIB` instead of from `ACME-TC`:
631//!
632//! ```text
633//! ACME-EXTENSION-MIB DEFINITIONS ::= BEGIN
634//! IMPORTS AcmeStatus FROM ACME-MIB; -- wrong: ACME-MIB doesn't define it
635//! END
636//! ```
637//!
638//! The resolver handles this by checking `ACME-MIB`'s own IMPORTS,
639//! finding that it declares `AcmeStatus FROM ACME-TC`, and
640//! following that chain. This is deterministic - the intermediate
641//! module explicitly names its source - so it is enabled at all
642//! strictness levels.
643//! - Partial import resolution: when a source module has some but not
644//! all of the requested symbols, the ones that exist are resolved
645//! individually and the rest are reported as unresolved.
646//! - ASN.1 primitive type fallback: `INTEGER`, `OCTET STRING`,
647//! `OBJECT IDENTIFIER`, and `BITS` always resolve from SNMPv2-SMI
648//! even without an explicit import.
649//! - Well-known OID roots: `iso`, `ccitt`, and `joint-iso-ccitt`
650//! always resolve to their fixed arc values.
651//!
652//! **Normal and Permissive (constrained fallbacks):**
653//! - Module name aliases: maps alternate module names to their
654//! canonical form (e.g. `SNMPv2-SMI-v1` to `SNMPv2-SMI`,
655//! `RFC-1213` to `RFC1213-MIB`). These aliases exist because
656//! modules have been renamed over time as RFCs were revised,
657//! and some vendors use non-standard names in their IMPORTS.
658//! - Unimported well-known symbol fallback: names like `enterprises`,
659//! `Counter64`, and `DisplayString` feel like built-in language
660//! keywords, but they're actually defined in specific base modules
661//! (`SNMPv2-SMI`, `SNMPv2-TC`, etc.) and formally need to be
662//! imported. Many MIB authors skip the import, treating these names
663//! as globally available:
664//!
665//! ```text
666//! ACME-MIB DEFINITIONS ::= BEGIN
667//! IMPORTS
668//! MODULE-IDENTITY, OBJECT-TYPE
669//! FROM SNMPv2-SMI;
670//! -- no import for enterprises or Counter64
671//!
672//! acmeMib MODULE-IDENTITY ... ::= { enterprises 12345 }
673//!
674//! acmeCounter OBJECT-TYPE
675//! SYNTAX Counter64 -- not imported
676//! ...
677//! END
678//! ```
679//!
680//! When a type or OID parent is not found via imports, the resolver
681//! searches the well-known base modules (SNMPv2-SMI, RFC1155-SMI,
682//! SNMPv2-TC). This is limited to those specific modules, so there
683//! is no ambiguity about which definition is meant.
684//! - TRAP-TYPE enterprise global lookup: the ENTERPRISE reference in
685//! TRAP-TYPE definitions is searched across all modules when not
686//! found via imports.
687//!
688//! **Permissive only (global fallbacks):**
689//! - Global object lookup: INDEX objects, AUGMENTS targets,
690//! NOTIFICATION-TYPE OBJECTS members, and DEFVAL object references
691//! are searched across all loaded modules when not found via imports.
692//! - Global group/compliance member lookup: OBJECT-GROUP members,
693//! MODULE-COMPLIANCE mandatory groups, and AGENT-CAPABILITIES
694//! variation targets are searched globally.
695//!
696//! **Which should I use?** Start with `Normal` (the default). If you
697//! get unresolved-reference diagnostics, it's usually better to fix
698//! the MIB file directly (correcting the `IMPORTS` statement to name
699//! the right source module) rather than reaching for `Permissive`.
700//! MIB files are plain text, and import fixes are usually obvious from
701//! the diagnostic message. Reserve `Permissive` for cases where you
702//! can't modify the MIB files, such as vendor-supplied MIBs loaded
703//! from a read-only path. `Strict` is useful for validation tooling
704//! or CI, where you want broken imports to surface as unresolved
705//! references rather than being silently fixed up.
706//!
707//! ## DiagnosticConfig - what gets reported
708//!
709//! [`DiagnosticConfig`] controls which diagnostics are collected and
710//! which severity level causes `load()` to fail. This is purely about
711//! reporting - it does not change what the resolver does. Set via
712//! [`Loader::diagnostic_config`](load::Loader::diagnostic_config).
713//!
714//! It has four preset constructors:
715//!
716//! | Preset | What's reported | `load()` fails at | When to use |
717//! |--------|-----------------|-------------------|-------------|
718//! | [`DiagnosticConfig::verbose()`] | Everything (style, info, warnings) | Severe | Debugging MIB issues, understanding what the resolver did. |
719//! | [`DiagnosticConfig::default()`] | Minor and above | Severe | General use. |
720//! | [`DiagnosticConfig::quiet()`] | Errors and above only | Severe | Production code that just wants to know about real problems. |
721//! | [`DiagnosticConfig::silent()`] | Nothing | Fatal only | When you don't care about diagnostics at all and want `load()` to succeed unless something is truly broken. |
722//!
723//! **Which should I use?** The default is fine for most cases. Use
724//! `quiet()` in production if you don't want to surface minor issues
725//! to users. Use `silent()` when loading untrusted or messy vendor
726//! MIBs where you just want whatever data you can get. Use `verbose()`
727//! when diagnosing why something isn't resolving correctly.
728//!
729//! ## Combining the two
730//!
731//! Since strictness controls resolution behavior and diagnostics
732//! controls reporting across the whole pipeline, they can be mixed
733//! freely:
734//!
735//! - `Normal` + `default()` - good general-purpose defaults.
736//! - `Permissive` + `silent()` - maximum tolerance. Tries every
737//! fallback, suppresses all diagnostics, only fails on fatal errors.
738//! Good for loading a pile of vendor MIBs where you want whatever
739//! data you can get. Be aware that some resolved symbols may be
740//! incorrect due to ambiguous fallback matches.
741//! - `Strict` + `verbose()` - maximum strictness. Minimal fallbacks,
742//! all diagnostics reported (including parse warnings and style
743//! issues). Good for validating MIBs you author.
744//! - `Normal` + `quiet()` - reasonable for a production SNMP tool that
745//! loads user-provided MIBs. Safe fallbacks, but only real errors
746//! are surfaced.
747//!
748//! ## Fine-tuning
749//!
750//! For more control, [`DiagnosticConfig`] also supports:
751//!
752//! - `fail_at` - change which severity causes `load()` to return an
753//! error. For example, set to [`Severity::Minor`] to fail on any
754//! minor issue.
755//! - `overrides` - promote or demote specific diagnostic codes (e.g.
756//! turn a warning into an error).
757//! - `ignore` - glob patterns to suppress specific diagnostic codes
758//! entirely (e.g. `"import-*"` to ignore all import-related
759//! diagnostics).
760//!
761//! # Feature Flags
762//!
763//! | Feature | Default | Description |
764//! |---------|---------|-------------|
765//! | `serde` | yes | Serde support and JSON export via [`export`] |
766//! | `cli` | yes | CLI binary (`mib-rs`) |
767//!
768//! # Examples
769//!
770//! Runnable examples live in the `examples/` directory. Each one can be run
771//! with `cargo run --example <name>`.
772//!
773//! ## Basic usage
774//!
775//! Load a MIB from memory, query objects, and display module metadata.
776//!
777//! ```rust,no_run
778#![doc = include_str!("../examples/basic.rs")]
779//! ```
780//!
781//! ## OID tree walking
782//!
783//! Root traversal, subtree iteration, depth-first walk, and node navigation.
784//!
785//! ```rust,no_run
786#![doc = include_str!("../examples/walk.rs")]
787//! ```
788//!
789//! ## Type introspection
790//!
791//! Type chains, effective values, constraints, enums, display hints,
792//! and classification predicates.
793//!
794//! ```rust,no_run
795#![doc = include_str!("../examples/types.rs")]
796//! ```
797//!
798//! ## Table navigation
799//!
800//! Tables, rows, columns, indexes, and object kind predicates.
801//!
802//! ```rust,no_run
803#![doc = include_str!("../examples/tables.rs")]
804//! ```
805//!
806//! ## Module metadata
807//!
808//! Module metadata, imports, revisions, base modules, and module-scoped
809//! iteration.
810//!
811//! ```rust,no_run
812#![doc = include_str!("../examples/modules.rs")]
813//! ```
814//!
815//! ## JSON export
816//!
817//! JSON export of a resolved MIB using the serde-based export API.
818//!
819//! ```rust,no_run
820#![doc = include_str!("../examples/export.rs")]
821//! ```
822//!
823//! ## Notifications, groups, and compliance
824//!
825//! Notifications, object groups, notification groups, and compliance statements.
826//!
827//! ```rust,no_run
828#![doc = include_str!("../examples/notifications.rs")]
829//! ```
830//!
831//! ## Query formats
832//!
833//! Plain names, qualified names, numeric OIDs, instance OIDs, and OID formatting.
834//!
835//! ```rust,no_run
836#![doc = include_str!("../examples/query.rs")]
837//! ```
838//!
839//! ## Diagnostics
840//!
841//! Diagnostic collection, strictness levels, reporting configuration,
842//! filtering, and severity overrides.
843//!
844//! ```rust,no_run
845#![doc = include_str!("../examples/diagnostics.rs")]
846//! ```
847//!
848//! ## Raw data access
849//!
850//! Low-level raw data access: sub-clause spans, import metadata,
851//! OID references, symbol tables, and bulk arena access.
852//!
853//! ```rust,no_run
854#![doc = include_str!("../examples/raw.rs")]
855//! ```
856//!
857//! ## Tokenization
858//!
859//! Lexical tokenization of MIB source text for syntax highlighting,
860//! linting, or custom tooling.
861//!
862//! ```rust,no_run
863#![doc = include_str!("../examples/tokens.rs")]
864//! ```
865//!
866//! ## Sources
867//!
868//! Source types: in-memory modules, directory sources, chaining,
869//! and module listing.
870//!
871//! ```rust,no_run
872#![doc = include_str!("../examples/sources.rs")]
873//! ```
874pub mod ast;
875pub mod error;
876#[cfg(feature = "serde")]
877pub mod export;
878pub(crate) mod graph;
879pub mod ir;
880pub(crate) mod lexer;
881pub mod load;
882pub mod lower;
883pub mod mib;
884pub mod parser;
885pub(crate) mod scan;
886pub mod searchpath;
887pub mod source;
888pub mod token;
889pub mod types;
890
891// Re-exports for convenience
892pub use error::LoadError;
893pub use load::{Loader, load};
894pub use mib::{
895 Capability, Compliance, Group, Index, Mib, Module, Node, Notification, Object, Oid, OidLookup,
896 ParseOidError, ResolveOidError, Type,
897 index::{DecodedIndex, IndexValue},
898};
899pub use source::{FindResult, Source};
900pub use token::{Token, TokenKind};
901pub use types::{
902 Access, AccessKeyword, BaseType, DiagCode, Diagnostic, DiagnosticConfig, IndexEncoding, Kind,
903 Language, ReportingLevel, ResolverStrictness, Severity, Status,
904};
905
906/// Low-level resolved data access.
907///
908/// This module exposes arena ids, backing records, and the explicit
909/// [`RawMib`](raw::RawMib) view returned by [`Mib::raw()`].
910pub mod raw {
911 pub use crate::mib::{
912 CapabilityData, CapabilityId, ComplianceData, ComplianceId, GroupData, GroupId, ModuleData,
913 ModuleId, NodeData, NodeId, NotificationData, NotificationId, ObjectData, ObjectId,
914 OidTree, RawMib, Symbol, TypeData, TypeId,
915 };
916}
917
918/// Compiler pipeline APIs exposed before final resolution.
919///
920/// These modules are useful when building syntax-aware tooling or diagnostics
921/// that need direct access to tokens, parsed AST, lowered IR, or the parser
922/// entry points themselves.
923pub mod compile {
924 pub use crate::{ast, ir, lower, parser, token};
925}