Skip to main content

async_snmp/
mib_support.rs

1//! MIB integration helpers for async-snmp.
2//!
3//! This module provides functions that use a loaded [`Mib`] to resolve
4//! OID names, format OIDs symbolically, and render SNMP values using MIB
5//! metadata (enum labels, display hints, type information).
6//!
7//! This is part of the public API, gated on the `mib` feature. All functions
8//! take a `&Mib` reference and are stateless.
9//!
10//! Key mib-rs types are re-exported here so users can depend on `async-snmp`
11//! alone without adding `mib-rs` as a direct dependency.
12
13use crate::format::hex;
14use crate::{Oid, Value, VarBind};
15use mib_rs::mib::display_hint::HexCase;
16use smallvec::SmallVec;
17
18// Re-export core mib-rs types so users don't need a direct mib-rs dependency.
19pub use mib_rs::{Access, DiagnosticConfig, Kind, Loader, Mib, ResolveOidError, source};
20
21/// Resolve a name like "sysDescr.0" or "IF-MIB::ifTable" to an async-snmp OID.
22///
23/// Accepts the same query formats as [`Mib::resolve_oid`]: plain names,
24/// qualified names (`MODULE::name`), instance OIDs (`name.suffix`), and numeric
25/// dotted-decimal strings.
26pub fn resolve_oid(mib: &Mib, name: &str) -> Result<Oid, ResolveOidError> {
27    mib.resolve_oid(name).map(|oid| Oid::from(&oid))
28}
29
30/// Format a numeric OID as "MODULE::name.suffix" using MIB metadata.
31///
32/// If the OID (or a prefix of it) matches a known node, the result uses
33/// symbolic form. Otherwise falls back to dotted-decimal.
34pub fn format_oid(mib: &Mib, oid: &Oid) -> String {
35    mib.format_oid(&oid.to_mib_oid())
36}
37
38/// Format a VarBind using MIB metadata: OID name + formatted value.
39///
40/// The OID is formatted via [`format_oid`] to produce `MODULE::name.suffix`.
41/// The value is formatted using MIB type information (enum labels, display
42/// hints) when the OID matches an OBJECT-TYPE definition. When no Object
43/// is found, falls back to the value's `Display` impl.
44///
45/// Output like "IF-MIB::ifDescr.1 = eth0"
46pub fn format_varbind(mib: &Mib, vb: &VarBind) -> String {
47    let oid_str = format_oid(mib, &vb.oid);
48    let value_str = format_value(mib, &vb.oid, &vb.value);
49    format!("{} = {}", oid_str, value_str)
50}
51
52/// Richer metadata about a VarBind for programmatic use.
53///
54/// The struct borrows `object_name`, `module_name`, and `units` from the
55/// `Mib`, while `suffix` and `formatted_value` are owned. Callers cannot
56/// detach the struct from the `Mib` lifetime.
57#[derive(Debug)]
58pub struct VarBindInfo<'a> {
59    /// The object name (e.g., "ifDescr").
60    pub object_name: &'a str,
61    /// The module that defines the object (e.g., "IF-MIB").
62    pub module_name: &'a str,
63    /// The instance suffix arcs after the object OID.
64    pub suffix: SmallVec<[u32; 4]>,
65    /// The UNITS clause from the object definition.
66    pub units: &'a str,
67    /// The MAX-ACCESS of the object.
68    pub access: Access,
69    /// The object kind (scalar, column, table, etc.).
70    pub kind: Kind,
71    /// The value formatted using MIB metadata.
72    pub formatted_value: String,
73}
74
75/// Get structured metadata about a VarBind using MIB information.
76///
77/// Returns `None` if the OID does not match any OBJECT-TYPE definition.
78/// Bare nodes (OID registrations without an OBJECT-TYPE) return `None`.
79pub fn describe_varbind<'a>(mib: &'a Mib, vb: &VarBind) -> Option<VarBindInfo<'a>> {
80    let mib_oid = vb.oid.to_mib_oid();
81    let lookup = mib.lookup_instance(&mib_oid);
82    let node = lookup.node();
83    let object = node.object()?;
84
85    let module_name = object.module().map(|m| m.name()).unwrap_or("");
86
87    let formatted_value = format_object_value(mib, &object, &vb.value);
88
89    Some(VarBindInfo {
90        object_name: object.name(),
91        module_name,
92        suffix: SmallVec::from_slice(lookup.suffix()),
93        units: object.units(),
94        access: object.access(),
95        kind: object.kind(),
96        formatted_value,
97    })
98}
99
100/// Format a value using MIB metadata for the given OID.
101///
102/// Looks up the OID in the MIB to find type information (enum labels,
103/// display hints), and uses it to produce a human-readable string.
104/// Falls back to the value's `Display` impl when no OBJECT-TYPE matches.
105pub fn format_value(mib: &Mib, oid: &Oid, value: &Value) -> String {
106    let mib_oid = oid.to_mib_oid();
107    let lookup = mib.lookup_instance(&mib_oid);
108    let node = lookup.node();
109
110    if let Some(object) = node.object() {
111        format_object_value(mib, &object, value)
112    } else {
113        value.to_string()
114    }
115}
116
117/// Format a value using an Object's type metadata.
118fn format_object_value(mib: &Mib, object: &mib_rs::Object<'_>, value: &Value) -> String {
119    match value {
120        Value::Integer(v) => {
121            // Check for enum labels first
122            let enums = object.effective_enums();
123            if let Some(nv) = enums.iter().find(|nv| nv.value == *v as i64) {
124                return format!("{}({})", nv.label, v);
125            }
126            // Try integer display hint
127            if let Some(formatted) = object.format_integer(*v as i64, HexCase::Lower) {
128                return formatted;
129            }
130            format!("{}", v)
131        }
132
133        Value::OctetString(bytes) => {
134            // Try display hint formatting
135            if let Some(formatted) = object.format_octets(bytes, HexCase::Lower) {
136                return formatted;
137            }
138            // Fall back to UTF-8, then hex
139            if hex::is_printable(bytes) {
140                return String::from_utf8_lossy(bytes).into_owned();
141            }
142            format_hex(bytes)
143        }
144
145        Value::ObjectIdentifier(oid) => format_oid(mib, oid),
146
147        Value::TimeTicks(v) => {
148            let formatted = crate::format::format_timeticks(*v);
149            format!("({}) {}", v, formatted)
150        }
151
152        Value::Counter32(v) => format!("{}", v),
153        Value::Counter64(v) => format!("{}", v),
154        Value::Gauge32(v) => format!("{}", v),
155
156        Value::Opaque(bytes) => {
157            // Try display hint formatting (same as OctetString)
158            if let Some(formatted) = object.format_octets(bytes, HexCase::Lower) {
159                return formatted;
160            }
161            format_hex(bytes)
162        }
163
164        Value::IpAddress(bytes) => {
165            format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
166        }
167
168        Value::Null => "NULL".to_string(),
169
170        // Exception values pass through
171        Value::NoSuchObject => "noSuchObject".to_string(),
172        Value::NoSuchInstance => "noSuchInstance".to_string(),
173        Value::EndOfMibView => "endOfMibView".to_string(),
174
175        Value::Unknown { tag, data } => {
176            format!("Unknown(0x{:02X}): {}", tag, format_hex(data))
177        }
178    }
179}
180
181/// Format bytes as space-separated uppercase hex for display.
182fn format_hex(bytes: &[u8]) -> String {
183    crate::format::format_hex_display(bytes)
184}
185
186#[cfg(feature = "cli")]
187impl crate::cli::output::VarBindFormatter for Mib {
188    fn format_oid(&self, oid: &Oid) -> String {
189        format_oid(self, oid)
190    }
191
192    fn format_value(&self, oid: &Oid, value: &Value) -> String {
193        format_value(self, oid, value)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::oid;
201
202    fn test_mib() -> Mib {
203        let source = source::memory(
204            "TEST-MIB",
205            r#"TEST-MIB DEFINITIONS ::= BEGIN
206IMPORTS
207    MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
208        FROM SNMPv2-SMI
209    DisplayString
210        FROM SNMPv2-TC;
211
212testMib MODULE-IDENTITY
213    LAST-UPDATED "202603210000Z"
214    ORGANIZATION "Test"
215    CONTACT-INFO "Test"
216    DESCRIPTION "Test module."
217    ::= { enterprises 99999 }
218
219testScalar OBJECT-TYPE
220    SYNTAX DisplayString
221    MAX-ACCESS read-only
222    STATUS current
223    DESCRIPTION "A test scalar."
224    ::= { testMib 1 }
225
226testStatus OBJECT-TYPE
227    SYNTAX INTEGER { up(1), down(2), testing(3) }
228    MAX-ACCESS read-only
229    STATUS current
230    DESCRIPTION "A test status."
231    ::= { testMib 2 }
232
233END
234"#,
235        );
236
237        Loader::new()
238            .source(source)
239            .modules(["TEST-MIB"])
240            .load()
241            .expect("test MIB should load")
242    }
243
244    #[test]
245    fn test_resolve_oid() {
246        let mib = test_mib();
247        let oid = resolve_oid(&mib, "testScalar.0").unwrap();
248        let expected = resolve_oid(&mib, "testScalar").unwrap().child(0);
249        assert_eq!(oid, expected);
250    }
251
252    #[test]
253    fn test_format_oid_symbolic() {
254        let mib = test_mib();
255        let oid = resolve_oid(&mib, "testScalar.0").unwrap();
256        let formatted = format_oid(&mib, &oid);
257        assert!(formatted.contains("testScalar"), "got: {}", formatted);
258    }
259
260    #[test]
261    fn test_format_varbind_string() {
262        let mib = test_mib();
263        let oid = resolve_oid(&mib, "testScalar.0").unwrap();
264        let vb = VarBind::new(oid, Value::OctetString(bytes::Bytes::from_static(b"hello")));
265        let formatted = format_varbind(&mib, &vb);
266        assert!(formatted.contains("testScalar"), "got: {}", formatted);
267        assert!(formatted.contains("hello"), "got: {}", formatted);
268    }
269
270    #[test]
271    fn test_format_varbind_enum() {
272        let mib = test_mib();
273        let oid = resolve_oid(&mib, "testStatus.0").unwrap();
274        let vb = VarBind::new(oid, Value::Integer(1));
275        let formatted = format_varbind(&mib, &vb);
276        assert!(formatted.contains("up(1)"), "got: {}", formatted);
277    }
278
279    #[test]
280    fn test_describe_varbind() {
281        let mib = test_mib();
282        let oid = resolve_oid(&mib, "testScalar.0").unwrap();
283        let vb = VarBind::new(oid, Value::OctetString(bytes::Bytes::from_static(b"hello")));
284        let info = describe_varbind(&mib, &vb).expect("should describe");
285        assert_eq!(info.object_name, "testScalar");
286        assert_eq!(info.module_name, "TEST-MIB");
287        assert_eq!(info.suffix.as_slice(), &[0]);
288    }
289
290    #[test]
291    fn test_oid_conversion_roundtrip() {
292        let snmp_oid = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
293        let mib_oid = snmp_oid.to_mib_oid();
294        let back: Oid = Oid::from(&mib_oid);
295        assert_eq!(snmp_oid, back);
296    }
297
298    #[test]
299    fn test_describe_unknown_oid_returns_none() {
300        let mib = test_mib();
301        // An OID that doesn't match any Object
302        let vb = VarBind::new(oid!(1, 3, 6, 1, 99, 99, 99), Value::Integer(42));
303        // This may or may not return None depending on the OID tree
304        // but at minimum it shouldn't panic
305        let _ = describe_varbind(&mib, &vb);
306    }
307}