1use crate::format::hex;
14use crate::{Oid, Value, VarBind};
15use mib_rs::mib::display_hint::HexCase;
16use smallvec::SmallVec;
17
18pub use mib_rs::{Access, DiagnosticConfig, Kind, Loader, Mib, ResolveOidError, source};
20
21pub fn resolve_oid(mib: &Mib, name: &str) -> Result<Oid, ResolveOidError> {
27 mib.resolve_oid(name).map(|oid| Oid::from(&oid))
28}
29
30pub fn format_oid(mib: &Mib, oid: &Oid) -> String {
35 mib.format_oid(&oid.to_mib_oid())
36}
37
38pub 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#[derive(Debug)]
58pub struct VarBindInfo<'a> {
59 pub object_name: &'a str,
61 pub module_name: &'a str,
63 pub suffix: SmallVec<[u32; 4]>,
65 pub units: &'a str,
67 pub access: Access,
69 pub kind: Kind,
71 pub formatted_value: String,
73}
74
75pub 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
100pub 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
117fn format_object_value(mib: &Mib, object: &mib_rs::Object<'_>, value: &Value) -> String {
119 match value {
120 Value::Integer(v) => {
121 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 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 if let Some(formatted) = object.format_octets(bytes, HexCase::Lower) {
136 return formatted;
137 }
138 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 Value::UInteger32(v) => format!("{}", v),
156
157 Value::Opaque(bytes) => {
158 if let Some(formatted) = object.format_octets(bytes, HexCase::Lower) {
160 return formatted;
161 }
162 format_hex(bytes)
163 }
164
165 Value::Nsap(bytes) => {
166 if let Some(formatted) = object.format_octets(bytes, HexCase::Lower) {
169 return formatted;
170 }
171 format_hex(bytes)
172 }
173
174 Value::IpAddress(bytes) => {
175 format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
176 }
177
178 Value::Null => "NULL".to_string(),
179
180 Value::NoSuchObject => "noSuchObject".to_string(),
182 Value::NoSuchInstance => "noSuchInstance".to_string(),
183 Value::EndOfMibView => "endOfMibView".to_string(),
184
185 Value::Unknown { tag, data } => {
186 format!("Unknown(0x{:02X}): {}", tag, format_hex(data))
187 }
188 }
189}
190
191fn format_hex(bytes: &[u8]) -> String {
193 crate::format::format_hex_display(bytes)
194}
195
196#[cfg(feature = "cli")]
197impl crate::cli::output::VarBindFormatter for Mib {
198 fn format_oid(&self, oid: &Oid) -> String {
199 format_oid(self, oid)
200 }
201
202 fn format_value(&self, oid: &Oid, value: &Value) -> String {
203 format_value(self, oid, value)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::oid;
211
212 fn test_mib() -> Mib {
213 let source = source::memory(
214 "TEST-MIB",
215 r#"TEST-MIB DEFINITIONS ::= BEGIN
216IMPORTS
217 MODULE-IDENTITY, OBJECT-TYPE, Integer32, enterprises
218 FROM SNMPv2-SMI
219 DisplayString
220 FROM SNMPv2-TC;
221
222testMib MODULE-IDENTITY
223 LAST-UPDATED "202603210000Z"
224 ORGANIZATION "Test"
225 CONTACT-INFO "Test"
226 DESCRIPTION "Test module."
227 ::= { enterprises 99999 }
228
229testScalar OBJECT-TYPE
230 SYNTAX DisplayString
231 MAX-ACCESS read-only
232 STATUS current
233 DESCRIPTION "A test scalar."
234 ::= { testMib 1 }
235
236testStatus OBJECT-TYPE
237 SYNTAX INTEGER { up(1), down(2), testing(3) }
238 MAX-ACCESS read-only
239 STATUS current
240 DESCRIPTION "A test status."
241 ::= { testMib 2 }
242
243END
244"#,
245 );
246
247 Loader::new()
248 .source(source)
249 .modules(["TEST-MIB"])
250 .load()
251 .expect("test MIB should load")
252 }
253
254 #[test]
255 fn test_resolve_oid() {
256 let mib = test_mib();
257 let oid = resolve_oid(&mib, "testScalar.0").unwrap();
258 let expected = resolve_oid(&mib, "testScalar").unwrap().child(0);
259 assert_eq!(oid, expected);
260 }
261
262 #[test]
263 fn test_format_oid_symbolic() {
264 let mib = test_mib();
265 let oid = resolve_oid(&mib, "testScalar.0").unwrap();
266 let formatted = format_oid(&mib, &oid);
267 assert!(formatted.contains("testScalar"), "got: {}", formatted);
268 }
269
270 #[test]
271 fn test_format_varbind_string() {
272 let mib = test_mib();
273 let oid = resolve_oid(&mib, "testScalar.0").unwrap();
274 let vb = VarBind::new(oid, Value::OctetString(bytes::Bytes::from_static(b"hello")));
275 let formatted = format_varbind(&mib, &vb);
276 assert!(formatted.contains("testScalar"), "got: {}", formatted);
277 assert!(formatted.contains("hello"), "got: {}", formatted);
278 }
279
280 #[test]
281 fn test_format_varbind_enum() {
282 let mib = test_mib();
283 let oid = resolve_oid(&mib, "testStatus.0").unwrap();
284 let vb = VarBind::new(oid, Value::Integer(1));
285 let formatted = format_varbind(&mib, &vb);
286 assert!(formatted.contains("up(1)"), "got: {}", formatted);
287 }
288
289 #[test]
290 fn test_describe_varbind() {
291 let mib = test_mib();
292 let oid = resolve_oid(&mib, "testScalar.0").unwrap();
293 let vb = VarBind::new(oid, Value::OctetString(bytes::Bytes::from_static(b"hello")));
294 let info = describe_varbind(&mib, &vb).expect("should describe");
295 assert_eq!(info.object_name, "testScalar");
296 assert_eq!(info.module_name, "TEST-MIB");
297 assert_eq!(info.suffix.as_slice(), &[0]);
298 }
299
300 #[test]
301 fn test_oid_conversion_roundtrip() {
302 let snmp_oid = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
303 let mib_oid = snmp_oid.to_mib_oid();
304 let back: Oid = Oid::from(&mib_oid);
305 assert_eq!(snmp_oid, back);
306 }
307
308 #[test]
309 fn test_describe_unknown_oid_returns_none() {
310 let mib = test_mib();
311 let vb = VarBind::new(oid!(1, 3, 6, 1, 99, 99, 99), Value::Integer(42));
313 let _ = describe_varbind(&mib, &vb);
316 }
317}