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