1use crate::cli::args::OutputFormat;
6use crate::cli::hints;
7use crate::{Oid, Value, VarBind, Version};
8use serde::Serialize;
9use std::io::{self, Write};
10use std::net::SocketAddr;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy)]
15pub enum OperationType {
16 Get,
17 GetNext,
18 GetBulk {
19 non_repeaters: i32,
20 max_repetitions: i32,
21 },
22 Set,
23 Walk,
24 BulkWalk {
25 max_repetitions: i32,
26 },
27}
28
29impl std::fmt::Display for OperationType {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::Get => write!(f, "GET"),
33 Self::GetNext => write!(f, "GETNEXT"),
34 Self::GetBulk { .. } => write!(f, "GETBULK"),
35 Self::Set => write!(f, "SET"),
36 Self::Walk => write!(f, "WALK (GETNEXT)"),
37 Self::BulkWalk { .. } => write!(f, "WALK (GETBULK)"),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub enum SecurityInfo {
45 Community(String),
46 V3 {
47 username: String,
48 auth_protocol: Option<String>,
49 priv_protocol: Option<String>,
50 },
51}
52
53#[derive(Debug)]
55pub struct RequestInfo {
56 pub target: SocketAddr,
57 pub version: Version,
58 pub security: SecurityInfo,
59 pub operation: OperationType,
60 pub oids: Vec<Oid>,
61}
62
63pub fn write_verbose_request(info: &RequestInfo) {
65 let mut stderr = std::io::stderr().lock();
66 let _ = writeln!(stderr, "--- Request ---");
67 let _ = writeln!(stderr, "Target: {}", info.target);
68 let _ = writeln!(stderr, "Version: {:?}", info.version);
69
70 match &info.security {
71 SecurityInfo::Community(c) => {
72 let _ = writeln!(stderr, "Community: {}", c);
73 }
74 SecurityInfo::V3 {
75 username,
76 auth_protocol,
77 priv_protocol,
78 } => {
79 let _ = writeln!(stderr, "Username: {}", username);
80 if let Some(auth) = auth_protocol {
81 let _ = writeln!(stderr, "Auth: {}", auth);
82 }
83 if let Some(priv_p) = priv_protocol {
84 let _ = writeln!(stderr, "Privacy: {}", priv_p);
85 }
86 }
87 }
88
89 let _ = writeln!(stderr, "Operation: {}", info.operation);
90
91 if let OperationType::GetBulk {
92 non_repeaters,
93 max_repetitions,
94 } = info.operation
95 {
96 let _ = writeln!(stderr, " Non-repeaters: {}", non_repeaters);
97 let _ = writeln!(stderr, " Max-repetitions: {}", max_repetitions);
98 } else if let OperationType::BulkWalk { max_repetitions } = info.operation {
99 let _ = writeln!(stderr, " Max-repetitions: {}", max_repetitions);
100 }
101
102 let _ = writeln!(stderr, "OIDs: {} total", info.oids.len());
103 for oid in &info.oids {
104 let hint = hints::lookup(oid);
105 if let Some(h) = hint {
106 let _ = writeln!(stderr, " {} ({})", oid, h);
107 } else {
108 let _ = writeln!(stderr, " {}", oid);
109 }
110 }
111 let _ = writeln!(stderr);
112}
113
114pub fn write_verbose_response(varbinds: &[VarBind], elapsed: Duration, show_hints: bool) {
116 let mut stderr = std::io::stderr().lock();
117 let _ = writeln!(stderr, "--- Response ---");
118 let _ = writeln!(stderr, "Results: {} varbind(s)", varbinds.len());
119 let _ = writeln!(stderr, "Time: {:.2}ms", elapsed.as_secs_f64() * 1000.0);
120 let _ = writeln!(stderr);
121
122 for vb in varbinds {
123 write_verbose_varbind(&mut stderr, vb, show_hints);
124 }
125
126 if !varbinds.is_empty() {
127 let _ = writeln!(stderr);
128 }
129}
130
131fn write_verbose_varbind<W: Write>(w: &mut W, vb: &VarBind, show_hints: bool) {
133 let hint = if show_hints {
135 hints::lookup(&vb.oid)
136 } else {
137 None
138 };
139 if let Some(h) = hint {
140 let _ = writeln!(w, " {} ({})", format_oid(&vb.oid), h);
141 } else {
142 let _ = writeln!(w, " {}", format_oid(&vb.oid));
143 }
144
145 let (type_name, decoded, raw_hex, size) = format_verbose_value(&vb.value);
147
148 let _ = writeln!(w, " Type: {}", type_name);
149 let _ = writeln!(w, " Value: {}", decoded);
150
151 if let Some(hex) = raw_hex {
152 let _ = writeln!(w, " Raw: {}", hex);
153 }
154
155 if let Some(s) = size {
156 let _ = writeln!(w, " Size: {} bytes", s);
157 }
158}
159
160fn format_verbose_value(value: &Value) -> (String, String, Option<String>, Option<usize>) {
162 match value {
163 Value::Integer(v) => ("INTEGER".into(), format!("{}", v), None, None),
164
165 Value::OctetString(bytes) => {
166 let raw_hex = format_hex_string(bytes);
167 let size = Some(bytes.len());
168
169 if is_printable(bytes) {
170 let decoded = String::from_utf8_lossy(bytes).to_string();
171 (
172 "STRING".into(),
173 format!("\"{}\"", decoded),
174 Some(raw_hex),
175 size,
176 )
177 } else {
178 ("Hex-STRING".into(), raw_hex.clone(), Some(raw_hex), size)
179 }
180 }
181
182 Value::Null => ("NULL".into(), "(null)".into(), None, None),
183
184 Value::ObjectIdentifier(oid) => {
185 let s = format_oid(oid);
186 let hint = hints::lookup(oid);
187 let decoded = if let Some(h) = hint {
188 format!("{} ({})", s, h)
189 } else {
190 s
191 };
192 ("OID".into(), decoded, None, None)
193 }
194
195 Value::IpAddress(bytes) => {
196 let s = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
197 ("IpAddress".into(), s, None, None)
198 }
199
200 Value::Counter32(v) => ("Counter32".into(), format!("{}", v), None, None),
201
202 Value::Gauge32(v) => ("Gauge32".into(), format!("{}", v), None, None),
203
204 Value::TimeTicks(v) => {
205 let formatted = format_timeticks(*v);
206 (
207 "TimeTicks".into(),
208 format!("{} ({})", v, formatted),
209 None,
210 None,
211 )
212 }
213
214 Value::Opaque(bytes) => {
215 let raw_hex = format_hex_string(bytes);
216 (
217 "Opaque".into(),
218 raw_hex.clone(),
219 Some(raw_hex),
220 Some(bytes.len()),
221 )
222 }
223
224 Value::Counter64(v) => ("Counter64".into(), format!("{}", v), None, None),
225
226 Value::NoSuchObject => (
227 "NoSuchObject".into(),
228 "No Such Object available".into(),
229 None,
230 None,
231 ),
232
233 Value::NoSuchInstance => (
234 "NoSuchInstance".into(),
235 "No Such Instance currently exists".into(),
236 None,
237 None,
238 ),
239
240 Value::EndOfMibView => (
241 "EndOfMibView".into(),
242 "No more variables left in this MIB View".into(),
243 None,
244 None,
245 ),
246
247 Value::Unknown { tag, data } => {
248 let raw_hex = format_hex_string(data);
249 (
250 format!("Unknown(0x{:02X})", tag),
251 raw_hex.clone(),
252 Some(raw_hex),
253 Some(data.len()),
254 )
255 }
256 }
257}
258
259#[derive(Debug, Serialize)]
261pub struct OperationResult {
262 pub target: String,
263 pub version: String,
264 pub results: Vec<VarBindResult>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub timing_ms: Option<f64>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub retries: Option<u32>,
269}
270
271#[derive(Debug, Serialize)]
273pub struct VarBindResult {
274 pub oid: String,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub hint: Option<String>,
277 #[serde(rename = "type")]
278 pub value_type: String,
279 pub value: serde_json::Value,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub formatted: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub raw_hex: Option<String>,
284}
285
286pub trait VarBindFormatter {
291 fn format_oid(&self, oid: &Oid) -> String;
293 fn format_value(&self, oid: &Oid, value: &Value) -> String;
295}
296
297pub struct OutputContext<'a> {
299 pub format: OutputFormat,
300 pub show_hints: bool,
301 pub force_hex: bool,
302 pub show_timing: bool,
303 pub formatter: Option<&'a dyn VarBindFormatter>,
305}
306
307impl<'a> OutputContext<'a> {
308 pub fn new(format: OutputFormat) -> Self {
310 Self {
311 format,
312 show_hints: true,
313 force_hex: false,
314 show_timing: false,
315 formatter: None,
316 }
317 }
318
319 pub fn write_results(
321 &self,
322 target: SocketAddr,
323 version: Version,
324 varbinds: &[VarBind],
325 elapsed: Option<Duration>,
326 retries: Option<u32>,
327 ) -> io::Result<()> {
328 let result = self.build_result(target, version, varbinds, elapsed, retries);
329 let mut stdout = io::stdout().lock();
330
331 match self.format {
332 OutputFormat::Human => self.write_human(&mut stdout, &result),
333 OutputFormat::Json => self.write_json(&mut stdout, &result),
334 OutputFormat::Raw => self.write_raw(&mut stdout, &result),
335 }
336 }
337
338 fn build_result(
339 &self,
340 target: SocketAddr,
341 version: Version,
342 varbinds: &[VarBind],
343 elapsed: Option<Duration>,
344 retries: Option<u32>,
345 ) -> OperationResult {
346 let results = varbinds.iter().map(|vb| self.format_varbind(vb)).collect();
347
348 OperationResult {
349 target: target.to_string(),
350 version: format!("{:?}", version),
351 results,
352 timing_ms: elapsed.map(|d| d.as_secs_f64() * 1000.0),
353 retries,
354 }
355 }
356
357 fn format_varbind(&self, vb: &VarBind) -> VarBindResult {
358 if let Some(fmt) = self.formatter {
359 return self.format_varbind_with_formatter(fmt, vb);
360 }
361
362 let oid_str = format_oid(&vb.oid);
363 let hint = if self.show_hints {
364 hints::lookup(&vb.oid).map(String::from)
365 } else {
366 None
367 };
368
369 let (value_type, value, formatted, raw_hex) = format_value(&vb.value, self.force_hex);
370
371 VarBindResult {
372 oid: oid_str,
373 hint,
374 value_type,
375 value,
376 formatted,
377 raw_hex,
378 }
379 }
380
381 fn format_varbind_with_formatter(
382 &self,
383 fmt: &dyn VarBindFormatter,
384 vb: &VarBind,
385 ) -> VarBindResult {
386 let oid_str = fmt.format_oid(&vb.oid);
387 let formatted_value = fmt.format_value(&vb.oid, &vb.value);
388 let (value_type, value, _, raw_hex) = format_value(&vb.value, self.force_hex);
389
390 VarBindResult {
391 oid: oid_str,
392 hint: None, value_type,
394 value,
395 formatted: Some(formatted_value),
396 raw_hex,
397 }
398 }
399
400 fn write_human<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
401 for vb in &result.results {
402 if let Some(ref hint) = vb.hint {
404 write!(w, "{} ({})", vb.oid, hint)?;
405 } else {
406 write!(w, "{}", vb.oid)?;
407 }
408
409 write!(w, " = {}: ", vb.value_type)?;
411
412 if let Some(ref formatted) = vb.formatted {
414 writeln!(w, "{}", formatted)?;
415 } else {
416 match &vb.value {
417 serde_json::Value::String(s) => writeln!(w, "\"{}\"", s)?,
418 serde_json::Value::Null => writeln!(w)?,
419 other => writeln!(w, "{}", other)?,
420 }
421 }
422 }
423
424 if self.show_timing
425 && let Some(ms) = result.timing_ms
426 {
427 if let Some(retries) = result.retries {
428 writeln!(w, "\nTiming: {:.1}ms ({} retries)", ms, retries)?;
429 } else {
430 writeln!(w, "\nTiming: {:.1}ms", ms)?;
431 }
432 }
433
434 Ok(())
435 }
436
437 fn write_json<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
438 let json = serde_json::to_string_pretty(result).map_err(io::Error::other)?;
439 writeln!(w, "{}", json)
440 }
441
442 fn write_raw<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
443 for vb in &result.results {
444 let value_str = match &vb.value {
445 serde_json::Value::String(s) => s.clone(),
446 serde_json::Value::Null => String::new(),
447 other => other.to_string(),
448 };
449 writeln!(w, "{}\t{}", vb.oid, value_str)?;
450 }
451 Ok(())
452 }
453}
454
455fn format_oid(oid: &Oid) -> String {
457 oid.arcs()
458 .iter()
459 .map(|a| a.to_string())
460 .collect::<Vec<_>>()
461 .join(".")
462}
463
464fn format_value(
466 value: &Value,
467 force_hex: bool,
468) -> (String, serde_json::Value, Option<String>, Option<String>) {
469 match value {
470 Value::Integer(v) => ("INTEGER".into(), (*v).into(), None, None),
471
472 Value::OctetString(bytes) => {
473 let raw_hex = Some(hex_string(bytes));
474
475 if force_hex || !is_printable(bytes) {
476 let formatted = format_hex_string(bytes);
477 (
478 "Hex-STRING".into(),
479 serde_json::Value::String(raw_hex.clone().unwrap()),
480 Some(formatted),
481 raw_hex,
482 )
483 } else {
484 let s = String::from_utf8_lossy(bytes);
485 (
486 "STRING".into(),
487 serde_json::Value::String(s.to_string()),
488 None,
489 raw_hex,
490 )
491 }
492 }
493
494 Value::Null => ("NULL".into(), serde_json::Value::Null, None, None),
495
496 Value::ObjectIdentifier(oid) => {
497 let s = format_oid(oid);
498 ("OID".into(), serde_json::Value::String(s), None, None)
499 }
500
501 Value::IpAddress(bytes) => {
502 let s = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
503 ("IpAddress".into(), serde_json::Value::String(s), None, None)
504 }
505
506 Value::Counter32(v) => ("Counter32".into(), (*v).into(), None, None),
507
508 Value::Gauge32(v) => ("Gauge32".into(), (*v).into(), None, None),
509
510 Value::TimeTicks(v) => {
511 let formatted = format_timeticks(*v);
512 (
513 "TimeTicks".into(),
514 (*v).into(),
515 Some(format!("({}) {}", v, formatted)),
516 None,
517 )
518 }
519
520 Value::Opaque(bytes) => {
521 let hex = hex_string(bytes);
522 (
523 "Opaque".into(),
524 serde_json::Value::String(hex.clone()),
525 Some(format_hex_string(bytes)),
526 Some(hex),
527 )
528 }
529
530 Value::Counter64(v) => ("Counter64".into(), (*v).into(), None, None),
531
532 Value::NoSuchObject => (
533 "NoSuchObject".into(),
534 serde_json::Value::Null,
535 Some("No Such Object available".into()),
536 None,
537 ),
538
539 Value::NoSuchInstance => (
540 "NoSuchInstance".into(),
541 serde_json::Value::Null,
542 Some("No Such Instance currently exists".into()),
543 None,
544 ),
545
546 Value::EndOfMibView => (
547 "EndOfMibView".into(),
548 serde_json::Value::Null,
549 Some("No more variables left in this MIB View".into()),
550 None,
551 ),
552
553 Value::Unknown { tag, data } => {
554 let hex = hex_string(data);
555 (
556 format!("Unknown(0x{:02X})", tag),
557 serde_json::Value::String(hex.clone()),
558 Some(format_hex_string(data)),
559 Some(hex),
560 )
561 }
562 }
563}
564
565fn is_printable(bytes: &[u8]) -> bool {
567 if bytes.is_empty() {
568 return true;
569 }
570
571 if let Ok(s) = std::str::from_utf8(bytes) {
573 s.chars()
575 .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
576 } else {
577 false
578 }
579}
580
581fn hex_string(bytes: &[u8]) -> String {
583 bytes.iter().map(|b| format!("{:02x}", b)).collect()
584}
585
586fn format_hex_string(bytes: &[u8]) -> String {
588 crate::fmt::format_hex_display(bytes)
589}
590
591fn format_timeticks(centiseconds: u32) -> String {
593 crate::fmt::format_timeticks(centiseconds)
594}
595
596pub fn write_error(err: &crate::Error) {
598 eprintln!("Error: {}", err);
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_format_timeticks() {
607 assert_eq!(format_timeticks(12345678), "1d 10:17:36.78");
609
610 assert_eq!(format_timeticks(360000), "01:00:00.00");
612
613 assert_eq!(format_timeticks(0), "00:00:00.00");
615 }
616
617 #[test]
618 fn test_is_printable() {
619 assert!(is_printable(b"Hello World"));
620 assert!(is_printable(b"Line 1\nLine 2"));
621 assert!(is_printable(b""));
622 assert!(!is_printable(&[0x00, 0x01, 0x02]));
623 assert!(!is_printable(&[0x80, 0x81]));
624 }
625
626 #[test]
627 fn test_hex_string() {
628 assert_eq!(hex_string(&[0x00, 0x1A, 0x2B]), "001a2b");
629 }
630
631 #[test]
632 fn test_format_hex_string() {
633 assert_eq!(format_hex_string(&[0x00, 0x1A, 0x2B]), "00 1A 2B");
634 }
635}