Skip to main content

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}