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