1use crate::cli::args::{CommonArgs, OutputArgs, OutputFormat, V3Args};
6use crate::cli::hints;
7use crate::format::hex;
8use crate::{Oid, Value, VarBind, Version};
9use serde::Serialize;
10use std::io::{self, Write};
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<'a> {
56 pub target: &'a str,
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
131struct DecodedValue {
133 type_name: String,
134 display: String,
136 json_value: serde_json::Value,
138 formatted: Option<String>,
140 raw_hex: Option<String>,
142 size: Option<usize>,
144}
145
146fn decode_value(value: &Value, force_hex: bool) -> DecodedValue {
148 match value {
149 Value::Integer(v) => DecodedValue {
150 type_name: "INTEGER".into(),
151 display: v.to_string(),
152 json_value: (*v).into(),
153 formatted: None,
154 raw_hex: None,
155 size: None,
156 },
157
158 Value::OctetString(bytes) => {
159 let compact_hex = hex::encode(bytes);
160 let spaced_hex = format_hex_string(bytes);
161 let size = Some(bytes.len());
162
163 if force_hex || !hex::is_printable(bytes) {
164 DecodedValue {
165 type_name: "Hex-STRING".into(),
166 display: spaced_hex.clone(),
167 json_value: serde_json::Value::String(compact_hex.clone()),
168 formatted: Some(spaced_hex),
169 raw_hex: Some(compact_hex),
170 size,
171 }
172 } else {
173 let s = String::from_utf8_lossy(bytes).to_string();
174 DecodedValue {
175 type_name: "STRING".into(),
176 display: format!("\"{}\"", s),
177 json_value: serde_json::Value::String(s),
178 formatted: None,
179 raw_hex: Some(compact_hex),
180 size,
181 }
182 }
183 }
184
185 Value::Null => DecodedValue {
186 type_name: "NULL".into(),
187 display: "(null)".into(),
188 json_value: serde_json::Value::Null,
189 formatted: None,
190 raw_hex: None,
191 size: None,
192 },
193
194 Value::ObjectIdentifier(oid) => {
195 let s = format_oid(oid);
196 let hint = hints::lookup(oid);
197 let display = if let Some(h) = hint {
198 format!("{} ({})", s, h)
199 } else {
200 s.clone()
201 };
202 DecodedValue {
203 type_name: "OID".into(),
204 display,
205 json_value: serde_json::Value::String(s),
206 formatted: None,
207 raw_hex: None,
208 size: None,
209 }
210 }
211
212 Value::IpAddress(bytes) => {
213 let s = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]);
214 DecodedValue {
215 type_name: "IpAddress".into(),
216 display: s.clone(),
217 json_value: serde_json::Value::String(s),
218 formatted: None,
219 raw_hex: None,
220 size: None,
221 }
222 }
223
224 Value::Counter32(v) => DecodedValue {
225 type_name: "Counter32".into(),
226 display: v.to_string(),
227 json_value: (*v).into(),
228 formatted: None,
229 raw_hex: None,
230 size: None,
231 },
232
233 Value::Gauge32(v) => DecodedValue {
234 type_name: "Gauge32".into(),
235 display: v.to_string(),
236 json_value: (*v).into(),
237 formatted: None,
238 raw_hex: None,
239 size: None,
240 },
241
242 Value::UInteger32(v) => DecodedValue {
243 type_name: "UInteger32".into(),
244 display: v.to_string(),
245 json_value: (*v).into(),
246 formatted: None,
247 raw_hex: None,
248 size: None,
249 },
250
251 Value::TimeTicks(v) => {
252 let human = format_timeticks(*v);
253 DecodedValue {
254 type_name: "TimeTicks".into(),
255 display: format!("({}) {}", v, human),
256 json_value: (*v).into(),
257 formatted: Some(format!("({}) {}", v, human)),
258 raw_hex: None,
259 size: None,
260 }
261 }
262
263 Value::Opaque(bytes) => {
264 let compact_hex = hex::encode(bytes);
265 let spaced_hex = format_hex_string(bytes);
266 DecodedValue {
267 type_name: "Opaque".into(),
268 display: spaced_hex.clone(),
269 json_value: serde_json::Value::String(compact_hex.clone()),
270 formatted: Some(spaced_hex),
271 raw_hex: Some(compact_hex),
272 size: Some(bytes.len()),
273 }
274 }
275
276 Value::Nsap(bytes) => {
278 let compact_hex = hex::encode(bytes);
279 let spaced_hex = format_hex_string(bytes);
280
281 DecodedValue {
282 type_name: "Nsap".into(),
283 display: spaced_hex.clone(),
284 json_value: serde_json::Value::String(compact_hex.clone()),
285 formatted: Some(spaced_hex),
286 raw_hex: Some(compact_hex),
287 size: Some(bytes.len()),
288 }
289 }
290
291 Value::Counter64(v) => DecodedValue {
292 type_name: "Counter64".into(),
293 display: v.to_string(),
294 json_value: (*v).into(),
295 formatted: None,
296 raw_hex: None,
297 size: None,
298 },
299
300 Value::NoSuchObject => DecodedValue {
301 type_name: "NoSuchObject".into(),
302 display: "No Such Object available".into(),
303 json_value: serde_json::Value::Null,
304 formatted: Some("No Such Object available".into()),
305 raw_hex: None,
306 size: None,
307 },
308
309 Value::NoSuchInstance => DecodedValue {
310 type_name: "NoSuchInstance".into(),
311 display: "No Such Instance currently exists".into(),
312 json_value: serde_json::Value::Null,
313 formatted: Some("No Such Instance currently exists".into()),
314 raw_hex: None,
315 size: None,
316 },
317
318 Value::EndOfMibView => DecodedValue {
319 type_name: "EndOfMibView".into(),
320 display: "No more variables left in this MIB View".into(),
321 json_value: serde_json::Value::Null,
322 formatted: Some("No more variables left in this MIB View".into()),
323 raw_hex: None,
324 size: None,
325 },
326
327 Value::Unknown { tag, data } => {
328 let compact_hex = hex::encode(data);
329 let spaced_hex = format_hex_string(data);
330 DecodedValue {
331 type_name: format!("Unknown(0x{:02X})", tag),
332 display: spaced_hex.clone(),
333 json_value: serde_json::Value::String(compact_hex.clone()),
334 formatted: Some(spaced_hex),
335 raw_hex: Some(compact_hex),
336 size: Some(data.len()),
337 }
338 }
339 }
340}
341
342fn write_verbose_varbind<W: Write>(w: &mut W, vb: &VarBind, show_hints: bool) {
344 let hint = if show_hints {
346 hints::lookup(&vb.oid)
347 } else {
348 None
349 };
350 if let Some(h) = hint {
351 let _ = writeln!(w, " {} ({})", format_oid(&vb.oid), h);
352 } else {
353 let _ = writeln!(w, " {}", format_oid(&vb.oid));
354 }
355
356 let decoded = decode_value(&vb.value, false);
357
358 let _ = writeln!(w, " Type: {}", decoded.type_name);
359 let _ = writeln!(w, " Value: {}", decoded.display);
360
361 if let Some(ref raw) = decoded.raw_hex {
362 let _ = writeln!(w, " Raw: {}", raw);
363 }
364
365 if let Some(s) = decoded.size {
366 let _ = writeln!(w, " Size: {} bytes", s);
367 }
368}
369
370#[derive(Debug, Serialize)]
372pub struct OperationResult {
373 pub target: String,
374 pub version: String,
375 pub results: Vec<VarBindResult>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub timing_ms: Option<f64>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub retries: Option<u32>,
380}
381
382#[derive(Debug, Serialize)]
384pub struct VarBindResult {
385 pub oid: String,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub hint: Option<String>,
388 #[serde(rename = "type")]
389 pub value_type: String,
390 pub value: serde_json::Value,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub formatted: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub raw_hex: Option<String>,
395}
396
397pub trait VarBindFormatter {
402 fn format_oid(&self, oid: &Oid) -> String;
404 fn format_value(&self, oid: &Oid, value: &Value) -> String;
406}
407
408pub struct OutputContext<'a> {
410 pub format: OutputFormat,
411 pub show_hints: bool,
412 pub force_hex: bool,
413 pub show_timing: bool,
414 pub formatter: Option<&'a dyn VarBindFormatter>,
416}
417
418impl<'a> OutputContext<'a> {
419 pub fn new(format: OutputFormat) -> Self {
421 Self {
422 format,
423 show_hints: true,
424 force_hex: false,
425 show_timing: false,
426 formatter: None,
427 }
428 }
429
430 pub fn from_args(output: &OutputArgs) -> Self {
432 Self {
433 format: output.format,
434 show_hints: !output.no_hints,
435 force_hex: output.hex,
436 show_timing: output.timing,
437 formatter: None,
438 }
439 }
440
441 pub fn write_results(
443 &self,
444 target: &str,
445 version: Version,
446 varbinds: &[VarBind],
447 elapsed: Option<Duration>,
448 retries: Option<u32>,
449 ) -> io::Result<()> {
450 let result = self.build_result(target, version, varbinds, elapsed, retries);
451 let mut stdout = io::stdout().lock();
452
453 match self.format {
454 OutputFormat::Human => self.write_human(&mut stdout, &result),
455 OutputFormat::Json => self.write_json(&mut stdout, &result),
456 OutputFormat::Raw => self.write_raw(&mut stdout, &result),
457 }
458 }
459
460 fn build_result(
461 &self,
462 target: &str,
463 version: Version,
464 varbinds: &[VarBind],
465 elapsed: Option<Duration>,
466 retries: Option<u32>,
467 ) -> OperationResult {
468 let results = varbinds.iter().map(|vb| self.format_varbind(vb)).collect();
469
470 OperationResult {
471 target: target.to_string(),
472 version: format!("{:?}", version),
473 results,
474 timing_ms: elapsed.map(|d| d.as_secs_f64() * 1000.0),
475 retries,
476 }
477 }
478
479 fn format_varbind(&self, vb: &VarBind) -> VarBindResult {
480 if let Some(fmt) = self.formatter {
481 return self.format_varbind_with_formatter(fmt, vb);
482 }
483
484 let oid_str = format_oid(&vb.oid);
485 let hint = if self.show_hints {
486 hints::lookup(&vb.oid).map(String::from)
487 } else {
488 None
489 };
490
491 let decoded = decode_value(&vb.value, self.force_hex);
492
493 VarBindResult {
494 oid: oid_str,
495 hint,
496 value_type: decoded.type_name,
497 value: decoded.json_value,
498 formatted: decoded.formatted,
499 raw_hex: decoded.raw_hex,
500 }
501 }
502
503 fn format_varbind_with_formatter(
504 &self,
505 fmt: &dyn VarBindFormatter,
506 vb: &VarBind,
507 ) -> VarBindResult {
508 let oid_str = fmt.format_oid(&vb.oid);
509 let formatted_value = fmt.format_value(&vb.oid, &vb.value);
510 let decoded = decode_value(&vb.value, self.force_hex);
511
512 VarBindResult {
513 oid: oid_str,
514 hint: None, value_type: decoded.type_name,
516 value: decoded.json_value,
517 formatted: Some(formatted_value),
518 raw_hex: decoded.raw_hex,
519 }
520 }
521
522 fn write_human<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
523 for vb in &result.results {
524 if let Some(ref hint) = vb.hint {
526 write!(w, "{} ({})", vb.oid, hint)?;
527 } else {
528 write!(w, "{}", vb.oid)?;
529 }
530
531 write!(w, " = {}: ", vb.value_type)?;
533
534 if let Some(ref formatted) = vb.formatted {
536 writeln!(w, "{}", formatted)?;
537 } else {
538 match &vb.value {
539 serde_json::Value::String(s) => writeln!(w, "\"{}\"", s)?,
540 serde_json::Value::Null => writeln!(w)?,
541 other => writeln!(w, "{}", other)?,
542 }
543 }
544 }
545
546 if self.show_timing
547 && let Some(ms) = result.timing_ms
548 {
549 if let Some(retries) = result.retries {
550 writeln!(w, "\nTiming: {:.1}ms ({} retries)", ms, retries)?;
551 } else {
552 writeln!(w, "\nTiming: {:.1}ms", ms)?;
553 }
554 }
555
556 Ok(())
557 }
558
559 fn write_json<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
560 let json = serde_json::to_string_pretty(result).map_err(io::Error::other)?;
561 writeln!(w, "{}", json)
562 }
563
564 fn write_raw<W: Write>(&self, w: &mut W, result: &OperationResult) -> io::Result<()> {
565 for vb in &result.results {
566 let value_str = match &vb.value {
567 serde_json::Value::String(s) => s.clone(),
568 serde_json::Value::Null => String::new(),
569 other => other.to_string(),
570 };
571 writeln!(w, "{}\t{}", vb.oid, value_str)?;
572 }
573 Ok(())
574 }
575}
576
577fn format_oid(oid: &Oid) -> String {
579 oid.arcs()
580 .iter()
581 .map(|a| a.to_string())
582 .collect::<Vec<_>>()
583 .join(".")
584}
585
586fn format_hex_string(bytes: &[u8]) -> String {
588 crate::format::format_hex_display(bytes)
589}
590
591fn format_timeticks(centiseconds: u32) -> String {
593 crate::format::format_timeticks(centiseconds)
594}
595
596pub fn build_security_info(v3: &V3Args, common: &CommonArgs) -> SecurityInfo {
598 if v3.is_v3() {
599 SecurityInfo::V3 {
600 username: v3.username.clone().unwrap_or_default(),
601 auth_protocol: v3.auth_protocol.map(|p| format!("{}", p)),
602 priv_protocol: v3.priv_protocol.map(|p| format!("{}", p)),
603 }
604 } else {
605 SecurityInfo::Community(common.community.clone())
606 }
607}
608
609pub fn write_error(err: &crate::Error) {
611 eprintln!("Error: {}", err);
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn test_format_timeticks() {
620 assert_eq!(format_timeticks(12345678), "1d 10:17:36.78");
622
623 assert_eq!(format_timeticks(360000), "01:00:00.00");
625
626 assert_eq!(format_timeticks(0), "00:00:00.00");
628 }
629
630 #[test]
631 fn test_is_printable() {
632 assert!(hex::is_printable(b"Hello World"));
633 assert!(hex::is_printable(b"Line 1\nLine 2"));
634 assert!(hex::is_printable(b""));
635 assert!(!hex::is_printable(&[0x00, 0x01, 0x02]));
636 assert!(!hex::is_printable(&[0x80, 0x81]));
637 }
638
639 #[test]
640 fn test_hex_encode() {
641 assert_eq!(hex::encode(&[0x00, 0x1A, 0x2B]), "001a2b");
642 }
643
644 #[test]
645 fn test_format_hex_string() {
646 assert_eq!(format_hex_string(&[0x00, 0x1A, 0x2B]), "00 1A 2B");
647 }
648}