async_snmp/
value.rs

1//! SNMP value types.
2//!
3//! The `Value` enum represents all SNMP data types including exceptions.
4
5use crate::ber::{Decoder, EncodeBuf, tag};
6use crate::error::{DecodeErrorKind, Error, Result};
7use crate::format::hex;
8use crate::oid::Oid;
9use bytes::Bytes;
10
11/// SNMP value.
12///
13/// Represents all SNMP data types including SMIv2 types and exception values.
14#[derive(Debug, Clone, PartialEq)]
15#[non_exhaustive]
16pub enum Value {
17    /// INTEGER (ASN.1 primitive, signed 32-bit)
18    Integer(i32),
19
20    /// OCTET STRING (arbitrary bytes).
21    ///
22    /// Per RFC 2578 (SMIv2), OCTET STRING values have a maximum size of 65535 octets.
23    /// This limit is **not enforced** during decoding to maintain permissive parsing
24    /// behavior. Applications that require strict compliance should validate size
25    /// after decoding.
26    OctetString(Bytes),
27
28    /// NULL
29    Null,
30
31    /// OBJECT IDENTIFIER
32    ObjectIdentifier(Oid),
33
34    /// IpAddress (4 bytes, big-endian)
35    IpAddress([u8; 4]),
36
37    /// Counter32 (unsigned 32-bit, wrapping)
38    Counter32(u32),
39
40    /// Gauge32 / Unsigned32 (unsigned 32-bit, non-wrapping)
41    Gauge32(u32),
42
43    /// TimeTicks (hundredths of seconds since epoch)
44    TimeTicks(u32),
45
46    /// Opaque (legacy, arbitrary bytes)
47    Opaque(Bytes),
48
49    /// Counter64 (unsigned 64-bit, wrapping).
50    ///
51    /// **SNMPv2c/v3 only.** Counter64 was introduced in SNMPv2 (RFC 2578) and is
52    /// not supported in SNMPv1. When sending Counter64 values to an SNMPv1 agent,
53    /// the value will be silently ignored or cause an error depending on the agent
54    /// implementation.
55    ///
56    /// If your application needs to support SNMPv1, avoid using Counter64 or
57    /// fall back to Counter32 (with potential overflow for high-bandwidth counters).
58    Counter64(u64),
59
60    /// noSuchObject exception - OID exists but no value
61    NoSuchObject,
62
63    /// noSuchInstance exception - Instance doesn't exist
64    NoSuchInstance,
65
66    /// endOfMibView exception - End of MIB reached during walk
67    EndOfMibView,
68
69    /// Unknown/unrecognized value type (for forward compatibility)
70    Unknown { tag: u8, data: Bytes },
71}
72
73impl Value {
74    /// Try to get as i32.
75    pub fn as_i32(&self) -> Option<i32> {
76        match self {
77            Value::Integer(v) => Some(*v),
78            _ => None,
79        }
80    }
81
82    /// Try to get as u32.
83    pub fn as_u32(&self) -> Option<u32> {
84        match self {
85            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v),
86            Value::Integer(v) if *v >= 0 => Some(*v as u32),
87            _ => None,
88        }
89    }
90
91    /// Try to get as u64.
92    pub fn as_u64(&self) -> Option<u64> {
93        match self {
94            Value::Counter64(v) => Some(*v),
95            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v as u64),
96            Value::Integer(v) if *v >= 0 => Some(*v as u64),
97            _ => None,
98        }
99    }
100
101    /// Try to get as bytes.
102    pub fn as_bytes(&self) -> Option<&[u8]> {
103        match self {
104            Value::OctetString(v) | Value::Opaque(v) => Some(v),
105            _ => None,
106        }
107    }
108
109    /// Try to get as string (UTF-8).
110    pub fn as_str(&self) -> Option<&str> {
111        self.as_bytes().and_then(|b| std::str::from_utf8(b).ok())
112    }
113
114    /// Try to get as OID.
115    pub fn as_oid(&self) -> Option<&Oid> {
116        match self {
117            Value::ObjectIdentifier(oid) => Some(oid),
118            _ => None,
119        }
120    }
121
122    /// Try to get as IP address.
123    pub fn as_ip(&self) -> Option<std::net::Ipv4Addr> {
124        match self {
125            Value::IpAddress(bytes) => Some(std::net::Ipv4Addr::from(*bytes)),
126            _ => None,
127        }
128    }
129
130    /// Check if this is an exception value.
131    pub fn is_exception(&self) -> bool {
132        matches!(
133            self,
134            Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView
135        )
136    }
137
138    /// Format an OctetString or Opaque value using RFC 2579 DISPLAY-HINT.
139    ///
140    /// Returns `None` if this is not an OctetString or Opaque value.
141    /// On invalid hint syntax, falls back to hex encoding.
142    ///
143    /// # Example
144    ///
145    /// ```
146    /// use async_snmp::Value;
147    /// use bytes::Bytes;
148    ///
149    /// let mac = Value::OctetString(Bytes::from_static(&[0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]));
150    /// assert_eq!(mac.format_with_hint("1x:"), Some("00:1a:2b:3c:4d:5e".into()));
151    ///
152    /// let integer = Value::Integer(42);
153    /// assert_eq!(integer.format_with_hint("1d"), None);
154    /// ```
155    pub fn format_with_hint(&self, hint: &str) -> Option<String> {
156        match self {
157            Value::OctetString(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
158            Value::Opaque(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
159            _ => None,
160        }
161    }
162
163    /// Encode to BER.
164    pub fn encode(&self, buf: &mut EncodeBuf) {
165        match self {
166            Value::Integer(v) => buf.push_integer(*v),
167            Value::OctetString(data) => buf.push_octet_string(data),
168            Value::Null => buf.push_null(),
169            Value::ObjectIdentifier(oid) => buf.push_oid(oid),
170            Value::IpAddress(addr) => buf.push_ip_address(*addr),
171            Value::Counter32(v) => buf.push_unsigned32(tag::application::COUNTER32, *v),
172            Value::Gauge32(v) => buf.push_unsigned32(tag::application::GAUGE32, *v),
173            Value::TimeTicks(v) => buf.push_unsigned32(tag::application::TIMETICKS, *v),
174            Value::Opaque(data) => {
175                buf.push_bytes(data);
176                buf.push_length(data.len());
177                buf.push_tag(tag::application::OPAQUE);
178            }
179            Value::Counter64(v) => buf.push_integer64(*v),
180            Value::NoSuchObject => {
181                buf.push_length(0);
182                buf.push_tag(tag::context::NO_SUCH_OBJECT);
183            }
184            Value::NoSuchInstance => {
185                buf.push_length(0);
186                buf.push_tag(tag::context::NO_SUCH_INSTANCE);
187            }
188            Value::EndOfMibView => {
189                buf.push_length(0);
190                buf.push_tag(tag::context::END_OF_MIB_VIEW);
191            }
192            Value::Unknown { tag: t, data } => {
193                buf.push_bytes(data);
194                buf.push_length(data.len());
195                buf.push_tag(*t);
196            }
197        }
198    }
199
200    /// Decode from BER.
201    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
202        let tag = decoder.read_tag()?;
203        let len = decoder.read_length()?;
204
205        match tag {
206            tag::universal::INTEGER => {
207                let value = decoder.read_integer_value(len)?;
208                Ok(Value::Integer(value))
209            }
210            tag::universal::OCTET_STRING => {
211                let data = decoder.read_bytes(len)?;
212                Ok(Value::OctetString(data))
213            }
214            tag::universal::NULL => {
215                if len != 0 {
216                    return Err(Error::decode(
217                        decoder.offset(),
218                        DecodeErrorKind::InvalidNull,
219                    ));
220                }
221                Ok(Value::Null)
222            }
223            tag::universal::OBJECT_IDENTIFIER => {
224                let oid = decoder.read_oid_value(len)?;
225                Ok(Value::ObjectIdentifier(oid))
226            }
227            tag::application::IP_ADDRESS => {
228                if len != 4 {
229                    return Err(Error::decode(
230                        decoder.offset(),
231                        DecodeErrorKind::InvalidIpAddressLength { length: len },
232                    ));
233                }
234                let data = decoder.read_bytes(4)?;
235                Ok(Value::IpAddress([data[0], data[1], data[2], data[3]]))
236            }
237            tag::application::COUNTER32 => {
238                let value = decoder.read_unsigned32_value(len)?;
239                Ok(Value::Counter32(value))
240            }
241            tag::application::GAUGE32 => {
242                let value = decoder.read_unsigned32_value(len)?;
243                Ok(Value::Gauge32(value))
244            }
245            tag::application::TIMETICKS => {
246                let value = decoder.read_unsigned32_value(len)?;
247                Ok(Value::TimeTicks(value))
248            }
249            tag::application::OPAQUE => {
250                let data = decoder.read_bytes(len)?;
251                Ok(Value::Opaque(data))
252            }
253            tag::application::COUNTER64 => {
254                let value = decoder.read_integer64_value(len)?;
255                Ok(Value::Counter64(value))
256            }
257            tag::context::NO_SUCH_OBJECT => {
258                if len != 0 {
259                    let _ = decoder.read_bytes(len)?;
260                }
261                Ok(Value::NoSuchObject)
262            }
263            tag::context::NO_SUCH_INSTANCE => {
264                if len != 0 {
265                    let _ = decoder.read_bytes(len)?;
266                }
267                Ok(Value::NoSuchInstance)
268            }
269            tag::context::END_OF_MIB_VIEW => {
270                if len != 0 {
271                    let _ = decoder.read_bytes(len)?;
272                }
273                Ok(Value::EndOfMibView)
274            }
275            // Reject constructed OCTET STRING (0x24).
276            // Net-snmp documents but does not parse constructed form; we follow suit.
277            tag::universal::OCTET_STRING_CONSTRUCTED => Err(Error::decode(
278                decoder.offset(),
279                DecodeErrorKind::ConstructedOctetString,
280            )),
281            _ => {
282                // Unknown tag - preserve for forward compatibility
283                let data = decoder.read_bytes(len)?;
284                Ok(Value::Unknown { tag, data })
285            }
286        }
287    }
288}
289
290impl std::fmt::Display for Value {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        match self {
293            Value::Integer(v) => write!(f, "{}", v),
294            Value::OctetString(data) => {
295                // Try to display as string if it's valid UTF-8
296                if let Ok(s) = std::str::from_utf8(data) {
297                    write!(f, "{}", s)
298                } else {
299                    write!(f, "0x{}", hex::encode(data))
300                }
301            }
302            Value::Null => write!(f, "NULL"),
303            Value::ObjectIdentifier(oid) => write!(f, "{}", oid),
304            Value::IpAddress(addr) => {
305                write!(f, "{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3])
306            }
307            Value::Counter32(v) => write!(f, "{}", v),
308            Value::Gauge32(v) => write!(f, "{}", v),
309            Value::TimeTicks(v) => {
310                // Display as time
311                let secs = v / 100;
312                let days = secs / 86400;
313                let hours = (secs % 86400) / 3600;
314                let mins = (secs % 3600) / 60;
315                let s = secs % 60;
316                write!(f, "{}d {}h {}m {}s", days, hours, mins, s)
317            }
318            Value::Opaque(data) => write!(f, "Opaque(0x{})", hex::encode(data)),
319            Value::Counter64(v) => write!(f, "{}", v),
320            Value::NoSuchObject => write!(f, "noSuchObject"),
321            Value::NoSuchInstance => write!(f, "noSuchInstance"),
322            Value::EndOfMibView => write!(f, "endOfMibView"),
323            Value::Unknown { tag, data } => {
324                write!(
325                    f,
326                    "Unknown(tag=0x{:02X}, data=0x{})",
327                    tag,
328                    hex::encode(data)
329                )
330            }
331        }
332    }
333}
334
335// Convenience conversions
336impl From<i32> for Value {
337    fn from(v: i32) -> Self {
338        Value::Integer(v)
339    }
340}
341
342impl From<&str> for Value {
343    fn from(s: &str) -> Self {
344        Value::OctetString(Bytes::copy_from_slice(s.as_bytes()))
345    }
346}
347
348impl From<String> for Value {
349    fn from(s: String) -> Self {
350        Value::OctetString(Bytes::from(s))
351    }
352}
353
354impl From<&[u8]> for Value {
355    fn from(data: &[u8]) -> Self {
356        Value::OctetString(Bytes::copy_from_slice(data))
357    }
358}
359
360impl From<Oid> for Value {
361    fn from(oid: Oid) -> Self {
362        Value::ObjectIdentifier(oid)
363    }
364}
365
366impl From<std::net::Ipv4Addr> for Value {
367    fn from(addr: std::net::Ipv4Addr) -> Self {
368        Value::IpAddress(addr.octets())
369    }
370}
371
372impl From<Bytes> for Value {
373    fn from(data: Bytes) -> Self {
374        Value::OctetString(data)
375    }
376}
377
378impl From<u64> for Value {
379    fn from(v: u64) -> Self {
380        Value::Counter64(v)
381    }
382}
383
384impl From<[u8; 4]> for Value {
385    fn from(addr: [u8; 4]) -> Self {
386        Value::IpAddress(addr)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    // AUDIT-003: Test that constructed OCTET STRING (0x24) is explicitly rejected.
395    // Net-snmp documents but does not parse constructed form; we reject it.
396    #[test]
397    fn test_reject_constructed_octet_string() {
398        // Constructed OCTET STRING has tag 0x24 (0x04 | 0x20)
399        // Create a fake BER-encoded constructed OCTET STRING: 0x24 0x03 0x04 0x01 0x41
400        // (constructed OCTET STRING containing primitive OCTET STRING "A")
401        let data = bytes::Bytes::from_static(&[0x24, 0x03, 0x04, 0x01, 0x41]);
402        let mut decoder = Decoder::new(data);
403        let result = Value::decode(&mut decoder);
404
405        assert!(
406            result.is_err(),
407            "constructed OCTET STRING (0x24) should be rejected"
408        );
409        let err = result.unwrap_err();
410        let err_msg = format!("{}", err);
411        assert!(
412            err_msg.contains("constructed OCTET STRING"),
413            "error message should mention 'constructed OCTET STRING', got: {}",
414            err_msg
415        );
416    }
417
418    #[test]
419    fn test_primitive_octet_string_accepted() {
420        // Primitive OCTET STRING (0x04) should be accepted
421        let data = bytes::Bytes::from_static(&[0x04, 0x03, 0x41, 0x42, 0x43]); // "ABC"
422        let mut decoder = Decoder::new(data);
423        let result = Value::decode(&mut decoder);
424
425        assert!(result.is_ok(), "primitive OCTET STRING should be accepted");
426        let value = result.unwrap();
427        assert_eq!(value.as_bytes(), Some(&b"ABC"[..]));
428    }
429
430    // ========================================================================
431    // Value Type Encoding/Decoding Tests
432    // ========================================================================
433
434    fn roundtrip(value: Value) -> Value {
435        let mut buf = EncodeBuf::new();
436        value.encode(&mut buf);
437        let data = buf.finish();
438        let mut decoder = Decoder::new(data);
439        Value::decode(&mut decoder).unwrap()
440    }
441
442    #[test]
443    fn test_integer_positive() {
444        let value = Value::Integer(42);
445        assert_eq!(roundtrip(value.clone()), value);
446    }
447
448    #[test]
449    fn test_integer_negative() {
450        let value = Value::Integer(-42);
451        assert_eq!(roundtrip(value.clone()), value);
452    }
453
454    #[test]
455    fn test_integer_zero() {
456        let value = Value::Integer(0);
457        assert_eq!(roundtrip(value.clone()), value);
458    }
459
460    #[test]
461    fn test_integer_min() {
462        let value = Value::Integer(i32::MIN);
463        assert_eq!(roundtrip(value.clone()), value);
464    }
465
466    #[test]
467    fn test_integer_max() {
468        let value = Value::Integer(i32::MAX);
469        assert_eq!(roundtrip(value.clone()), value);
470    }
471
472    #[test]
473    fn test_octet_string_ascii() {
474        let value = Value::OctetString(Bytes::from_static(b"hello world"));
475        assert_eq!(roundtrip(value.clone()), value);
476    }
477
478    #[test]
479    fn test_octet_string_binary() {
480        let value = Value::OctetString(Bytes::from_static(&[0x00, 0xFF, 0x80, 0x7F]));
481        assert_eq!(roundtrip(value.clone()), value);
482    }
483
484    #[test]
485    fn test_octet_string_empty() {
486        let value = Value::OctetString(Bytes::new());
487        assert_eq!(roundtrip(value.clone()), value);
488    }
489
490    #[test]
491    fn test_null() {
492        let value = Value::Null;
493        assert_eq!(roundtrip(value.clone()), value);
494    }
495
496    #[test]
497    fn test_object_identifier() {
498        let value = Value::ObjectIdentifier(crate::oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
499        assert_eq!(roundtrip(value.clone()), value);
500    }
501
502    #[test]
503    fn test_ip_address() {
504        let value = Value::IpAddress([192, 168, 1, 1]);
505        assert_eq!(roundtrip(value.clone()), value);
506    }
507
508    #[test]
509    fn test_ip_address_zero() {
510        let value = Value::IpAddress([0, 0, 0, 0]);
511        assert_eq!(roundtrip(value.clone()), value);
512    }
513
514    #[test]
515    fn test_ip_address_broadcast() {
516        let value = Value::IpAddress([255, 255, 255, 255]);
517        assert_eq!(roundtrip(value.clone()), value);
518    }
519
520    #[test]
521    fn test_counter32() {
522        let value = Value::Counter32(999999);
523        assert_eq!(roundtrip(value.clone()), value);
524    }
525
526    #[test]
527    fn test_counter32_zero() {
528        let value = Value::Counter32(0);
529        assert_eq!(roundtrip(value.clone()), value);
530    }
531
532    #[test]
533    fn test_counter32_max() {
534        let value = Value::Counter32(u32::MAX);
535        assert_eq!(roundtrip(value.clone()), value);
536    }
537
538    #[test]
539    fn test_gauge32() {
540        let value = Value::Gauge32(1000000000);
541        assert_eq!(roundtrip(value.clone()), value);
542    }
543
544    #[test]
545    fn test_gauge32_max() {
546        let value = Value::Gauge32(u32::MAX);
547        assert_eq!(roundtrip(value.clone()), value);
548    }
549
550    #[test]
551    fn test_timeticks() {
552        let value = Value::TimeTicks(123456);
553        assert_eq!(roundtrip(value.clone()), value);
554    }
555
556    #[test]
557    fn test_timeticks_max() {
558        let value = Value::TimeTicks(u32::MAX);
559        assert_eq!(roundtrip(value.clone()), value);
560    }
561
562    #[test]
563    fn test_opaque() {
564        let value = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
565        assert_eq!(roundtrip(value.clone()), value);
566    }
567
568    #[test]
569    fn test_opaque_empty() {
570        let value = Value::Opaque(Bytes::new());
571        assert_eq!(roundtrip(value.clone()), value);
572    }
573
574    #[test]
575    fn test_counter64() {
576        let value = Value::Counter64(123456789012345);
577        assert_eq!(roundtrip(value.clone()), value);
578    }
579
580    #[test]
581    fn test_counter64_zero() {
582        let value = Value::Counter64(0);
583        assert_eq!(roundtrip(value.clone()), value);
584    }
585
586    #[test]
587    fn test_counter64_max() {
588        let value = Value::Counter64(u64::MAX);
589        assert_eq!(roundtrip(value.clone()), value);
590    }
591
592    #[test]
593    fn test_no_such_object() {
594        let value = Value::NoSuchObject;
595        assert_eq!(roundtrip(value.clone()), value);
596    }
597
598    #[test]
599    fn test_no_such_instance() {
600        let value = Value::NoSuchInstance;
601        assert_eq!(roundtrip(value.clone()), value);
602    }
603
604    #[test]
605    fn test_end_of_mib_view() {
606        let value = Value::EndOfMibView;
607        assert_eq!(roundtrip(value.clone()), value);
608    }
609
610    #[test]
611    fn test_unknown_tag_preserved() {
612        // Tag 0x45 is application class but not a standard SNMP type
613        let data = Bytes::from_static(&[0x45, 0x03, 0x01, 0x02, 0x03]);
614        let mut decoder = Decoder::new(data);
615        let value = Value::decode(&mut decoder).unwrap();
616
617        match value {
618            Value::Unknown { tag, ref data } => {
619                assert_eq!(tag, 0x45);
620                assert_eq!(data.as_ref(), &[0x01, 0x02, 0x03]);
621            }
622            _ => panic!("expected Unknown variant"),
623        }
624
625        // Roundtrip should preserve
626        assert_eq!(roundtrip(value.clone()), value);
627    }
628
629    // ========================================================================
630    // Accessor Method Tests
631    // ========================================================================
632
633    #[test]
634    fn test_as_i32() {
635        assert_eq!(Value::Integer(42).as_i32(), Some(42));
636        assert_eq!(Value::Integer(-42).as_i32(), Some(-42));
637        assert_eq!(Value::Counter32(100).as_i32(), None);
638        assert_eq!(Value::Null.as_i32(), None);
639    }
640
641    #[test]
642    fn test_as_u32() {
643        assert_eq!(Value::Counter32(100).as_u32(), Some(100));
644        assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
645        assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
646        assert_eq!(Value::Integer(50).as_u32(), Some(50));
647        assert_eq!(Value::Integer(-1).as_u32(), None);
648        assert_eq!(Value::Counter64(100).as_u32(), None);
649    }
650
651    #[test]
652    fn test_as_u64() {
653        assert_eq!(Value::Counter64(100).as_u64(), Some(100));
654        assert_eq!(Value::Counter32(100).as_u64(), Some(100));
655        assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
656        assert_eq!(Value::TimeTicks(300).as_u64(), Some(300));
657        assert_eq!(Value::Integer(50).as_u64(), Some(50));
658        assert_eq!(Value::Integer(-1).as_u64(), None);
659    }
660
661    #[test]
662    fn test_as_bytes() {
663        let s = Value::OctetString(Bytes::from_static(b"test"));
664        assert_eq!(s.as_bytes(), Some(b"test".as_slice()));
665
666        let o = Value::Opaque(Bytes::from_static(b"data"));
667        assert_eq!(o.as_bytes(), Some(b"data".as_slice()));
668
669        assert_eq!(Value::Integer(1).as_bytes(), None);
670    }
671
672    #[test]
673    fn test_as_str() {
674        let s = Value::OctetString(Bytes::from_static(b"hello"));
675        assert_eq!(s.as_str(), Some("hello"));
676
677        // Invalid UTF-8 returns None
678        let invalid = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
679        assert_eq!(invalid.as_str(), None);
680
681        assert_eq!(Value::Integer(1).as_str(), None);
682    }
683
684    #[test]
685    fn test_as_oid() {
686        let oid = crate::oid!(1, 3, 6, 1);
687        let v = Value::ObjectIdentifier(oid.clone());
688        assert_eq!(v.as_oid(), Some(&oid));
689
690        assert_eq!(Value::Integer(1).as_oid(), None);
691    }
692
693    #[test]
694    fn test_as_ip() {
695        let v = Value::IpAddress([192, 168, 1, 1]);
696        assert_eq!(v.as_ip(), Some(std::net::Ipv4Addr::new(192, 168, 1, 1)));
697
698        assert_eq!(Value::Integer(1).as_ip(), None);
699    }
700
701    // ========================================================================
702    // is_exception() Tests
703    // ========================================================================
704
705    #[test]
706    fn test_is_exception() {
707        assert!(Value::NoSuchObject.is_exception());
708        assert!(Value::NoSuchInstance.is_exception());
709        assert!(Value::EndOfMibView.is_exception());
710
711        assert!(!Value::Integer(1).is_exception());
712        assert!(!Value::Null.is_exception());
713        assert!(!Value::OctetString(Bytes::new()).is_exception());
714    }
715
716    // ========================================================================
717    // Display Trait Tests
718    // ========================================================================
719
720    #[test]
721    fn test_display_integer() {
722        assert_eq!(format!("{}", Value::Integer(42)), "42");
723        assert_eq!(format!("{}", Value::Integer(-42)), "-42");
724    }
725
726    #[test]
727    fn test_display_octet_string_utf8() {
728        let v = Value::OctetString(Bytes::from_static(b"hello"));
729        assert_eq!(format!("{}", v), "hello");
730    }
731
732    #[test]
733    fn test_display_octet_string_binary() {
734        // Use bytes that are not valid UTF-8 (0xFF is never valid in UTF-8)
735        let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
736        assert_eq!(format!("{}", v), "0xfffe");
737    }
738
739    #[test]
740    fn test_display_null() {
741        assert_eq!(format!("{}", Value::Null), "NULL");
742    }
743
744    #[test]
745    fn test_display_ip_address() {
746        let v = Value::IpAddress([192, 168, 1, 1]);
747        assert_eq!(format!("{}", v), "192.168.1.1");
748    }
749
750    #[test]
751    fn test_display_counter32() {
752        assert_eq!(format!("{}", Value::Counter32(999)), "999");
753    }
754
755    #[test]
756    fn test_display_gauge32() {
757        assert_eq!(format!("{}", Value::Gauge32(1000)), "1000");
758    }
759
760    #[test]
761    fn test_display_timeticks() {
762        // 123456 hundredths = 1234.56 seconds
763        // = 0d 0h 20m 34s
764        let v = Value::TimeTicks(123456);
765        assert_eq!(format!("{}", v), "0d 0h 20m 34s");
766    }
767
768    #[test]
769    fn test_display_opaque() {
770        let v = Value::Opaque(Bytes::from_static(&[0xBE, 0xEF]));
771        assert_eq!(format!("{}", v), "Opaque(0xbeef)");
772    }
773
774    #[test]
775    fn test_display_counter64() {
776        assert_eq!(format!("{}", Value::Counter64(12345678)), "12345678");
777    }
778
779    #[test]
780    fn test_display_exceptions() {
781        assert_eq!(format!("{}", Value::NoSuchObject), "noSuchObject");
782        assert_eq!(format!("{}", Value::NoSuchInstance), "noSuchInstance");
783        assert_eq!(format!("{}", Value::EndOfMibView), "endOfMibView");
784    }
785
786    #[test]
787    fn test_display_unknown() {
788        let v = Value::Unknown {
789            tag: 0x99,
790            data: Bytes::from_static(&[0x01, 0x02]),
791        };
792        assert_eq!(format!("{}", v), "Unknown(tag=0x99, data=0x0102)");
793    }
794
795    // ========================================================================
796    // From Conversion Tests
797    // ========================================================================
798
799    #[test]
800    fn test_from_i32() {
801        let v: Value = 42i32.into();
802        assert_eq!(v, Value::Integer(42));
803    }
804
805    #[test]
806    fn test_from_str() {
807        let v: Value = "hello".into();
808        assert_eq!(v.as_str(), Some("hello"));
809    }
810
811    #[test]
812    fn test_from_string() {
813        let v: Value = String::from("hello").into();
814        assert_eq!(v.as_str(), Some("hello"));
815    }
816
817    #[test]
818    fn test_from_bytes_slice() {
819        let v: Value = (&[1u8, 2, 3][..]).into();
820        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
821    }
822
823    #[test]
824    fn test_from_oid() {
825        let oid = crate::oid!(1, 3, 6, 1);
826        let v: Value = oid.clone().into();
827        assert_eq!(v.as_oid(), Some(&oid));
828    }
829
830    #[test]
831    fn test_from_ipv4addr() {
832        let addr = std::net::Ipv4Addr::new(10, 0, 0, 1);
833        let v: Value = addr.into();
834        assert_eq!(v, Value::IpAddress([10, 0, 0, 1]));
835    }
836
837    #[test]
838    fn test_from_bytes() {
839        let data = Bytes::from_static(b"hello");
840        let v: Value = data.into();
841        assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
842    }
843
844    #[test]
845    fn test_from_u64() {
846        let v: Value = 12345678901234u64.into();
847        assert_eq!(v, Value::Counter64(12345678901234));
848    }
849
850    #[test]
851    fn test_from_ip_array() {
852        let v: Value = [192u8, 168, 1, 1].into();
853        assert_eq!(v, Value::IpAddress([192, 168, 1, 1]));
854    }
855
856    // ========================================================================
857    // Decode Error Tests
858    // ========================================================================
859
860    #[test]
861    fn test_decode_invalid_null_length() {
862        // NULL must have length 0
863        let data = Bytes::from_static(&[0x05, 0x01, 0x00]); // NULL with length 1
864        let mut decoder = Decoder::new(data);
865        let result = Value::decode(&mut decoder);
866        assert!(result.is_err());
867    }
868
869    #[test]
870    fn test_decode_invalid_ip_address_length() {
871        // IpAddress must have length 4
872        let data = Bytes::from_static(&[0x40, 0x03, 0x01, 0x02, 0x03]); // Only 3 bytes
873        let mut decoder = Decoder::new(data);
874        let result = Value::decode(&mut decoder);
875        assert!(result.is_err());
876    }
877
878    #[test]
879    fn test_decode_exception_with_content_accepted() {
880        // Per implementation, exceptions with non-zero length have content skipped
881        let data = Bytes::from_static(&[0x80, 0x01, 0xFF]); // NoSuchObject with 1 byte
882        let mut decoder = Decoder::new(data);
883        let result = Value::decode(&mut decoder);
884        assert!(result.is_ok());
885        assert_eq!(result.unwrap(), Value::NoSuchObject);
886    }
887}