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 - the requested OID exists in the MIB but has no value.
61    ///
62    /// This exception indicates that the agent recognizes the OID (it's a valid
63    /// MIB object), but there is no instance available. This commonly occurs when
64    /// requesting a table column OID without an index.
65    ///
66    /// # Example
67    ///
68    /// ```
69    /// use async_snmp::Value;
70    ///
71    /// let response = Value::NoSuchObject;
72    /// assert!(response.is_exception());
73    ///
74    /// // When handling responses, check for exceptions:
75    /// match response {
76    ///     Value::NoSuchObject => println!("OID exists but has no value"),
77    ///     _ => {}
78    /// }
79    /// ```
80    NoSuchObject,
81
82    /// noSuchInstance exception - the specific instance does not exist.
83    ///
84    /// This exception indicates that while the MIB object exists, the specific
85    /// instance (index) requested does not. This commonly occurs when querying
86    /// a table row that doesn't exist.
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use async_snmp::Value;
92    ///
93    /// let response = Value::NoSuchInstance;
94    /// assert!(response.is_exception());
95    /// ```
96    NoSuchInstance,
97
98    /// endOfMibView exception - end of the MIB has been reached.
99    ///
100    /// This exception is returned during GETNEXT/GETBULK operations when
101    /// there are no more OIDs lexicographically greater than the requested OID.
102    /// This is the normal termination condition for SNMP walks.
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// use async_snmp::Value;
108    ///
109    /// let response = Value::EndOfMibView;
110    /// assert!(response.is_exception());
111    ///
112    /// // Commonly used to detect end of walk
113    /// if matches!(response, Value::EndOfMibView) {
114    ///     println!("Walk complete - reached end of MIB");
115    /// }
116    /// ```
117    EndOfMibView,
118
119    /// Unknown/unrecognized value type (for forward compatibility)
120    Unknown { tag: u8, data: Bytes },
121}
122
123impl Value {
124    /// Try to get as i32.
125    ///
126    /// Returns `Some(i32)` for [`Value::Integer`], `None` otherwise.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use async_snmp::Value;
132    ///
133    /// let v = Value::Integer(42);
134    /// assert_eq!(v.as_i32(), Some(42));
135    ///
136    /// let v = Value::Integer(-100);
137    /// assert_eq!(v.as_i32(), Some(-100));
138    ///
139    /// // Counter32 is not an Integer
140    /// let v = Value::Counter32(42);
141    /// assert_eq!(v.as_i32(), None);
142    /// ```
143    pub fn as_i32(&self) -> Option<i32> {
144        match self {
145            Value::Integer(v) => Some(*v),
146            _ => None,
147        }
148    }
149
150    /// Try to get as u32.
151    ///
152    /// Returns `Some(u32)` for [`Value::Counter32`], [`Value::Gauge32`],
153    /// [`Value::TimeTicks`], or non-negative [`Value::Integer`]. Returns `None` otherwise.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use async_snmp::Value;
159    ///
160    /// // Works for Counter32, Gauge32, TimeTicks
161    /// assert_eq!(Value::Counter32(100).as_u32(), Some(100));
162    /// assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
163    /// assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
164    ///
165    /// // Works for non-negative integers
166    /// assert_eq!(Value::Integer(50).as_u32(), Some(50));
167    ///
168    /// // Returns None for negative integers
169    /// assert_eq!(Value::Integer(-1).as_u32(), None);
170    ///
171    /// // Counter64 returns None (use as_u64 instead)
172    /// assert_eq!(Value::Counter64(100).as_u32(), None);
173    /// ```
174    pub fn as_u32(&self) -> Option<u32> {
175        match self {
176            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v),
177            Value::Integer(v) if *v >= 0 => Some(*v as u32),
178            _ => None,
179        }
180    }
181
182    /// Try to get as u64.
183    ///
184    /// Returns `Some(u64)` for [`Value::Counter64`], or any 32-bit unsigned type
185    /// ([`Value::Counter32`], [`Value::Gauge32`], [`Value::TimeTicks`]), or
186    /// non-negative [`Value::Integer`]. Returns `None` otherwise.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use async_snmp::Value;
192    ///
193    /// // Counter64 is the primary use case
194    /// assert_eq!(Value::Counter64(10_000_000_000).as_u64(), Some(10_000_000_000));
195    ///
196    /// // Also works for 32-bit unsigned types
197    /// assert_eq!(Value::Counter32(100).as_u64(), Some(100));
198    /// assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
199    ///
200    /// // Non-negative integers work
201    /// assert_eq!(Value::Integer(50).as_u64(), Some(50));
202    ///
203    /// // Negative integers return None
204    /// assert_eq!(Value::Integer(-1).as_u64(), None);
205    /// ```
206    pub fn as_u64(&self) -> Option<u64> {
207        match self {
208            Value::Counter64(v) => Some(*v),
209            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v as u64),
210            Value::Integer(v) if *v >= 0 => Some(*v as u64),
211            _ => None,
212        }
213    }
214
215    /// Try to get as bytes.
216    ///
217    /// Returns `Some(&[u8])` for [`Value::OctetString`] or [`Value::Opaque`].
218    /// Returns `None` otherwise.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use async_snmp::Value;
224    /// use bytes::Bytes;
225    ///
226    /// let v = Value::OctetString(Bytes::from_static(b"hello"));
227    /// assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
228    ///
229    /// // Works for Opaque too
230    /// let v = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
231    /// assert_eq!(v.as_bytes(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
232    ///
233    /// // Other types return None
234    /// assert_eq!(Value::Integer(42).as_bytes(), None);
235    /// ```
236    pub fn as_bytes(&self) -> Option<&[u8]> {
237        match self {
238            Value::OctetString(v) | Value::Opaque(v) => Some(v),
239            _ => None,
240        }
241    }
242
243    /// Try to get as string (UTF-8).
244    ///
245    /// Returns `Some(&str)` if the value is an [`Value::OctetString`] or [`Value::Opaque`]
246    /// containing valid UTF-8. Returns `None` for other types or invalid UTF-8.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use async_snmp::Value;
252    /// use bytes::Bytes;
253    ///
254    /// let v = Value::OctetString(Bytes::from_static(b"Linux router1 5.4.0"));
255    /// assert_eq!(v.as_str(), Some("Linux router1 5.4.0"));
256    ///
257    /// // Invalid UTF-8 returns None
258    /// let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
259    /// assert_eq!(v.as_str(), None);
260    ///
261    /// // Binary data with valid UTF-8 bytes still works, but use as_bytes() for clarity
262    /// let binary = Value::OctetString(Bytes::from_static(&[0x80, 0x81, 0x82]));
263    /// assert_eq!(binary.as_str(), None); // Invalid UTF-8 sequence
264    /// assert!(binary.as_bytes().is_some());
265    /// ```
266    pub fn as_str(&self) -> Option<&str> {
267        self.as_bytes().and_then(|b| std::str::from_utf8(b).ok())
268    }
269
270    /// Try to get as OID.
271    ///
272    /// Returns `Some(&Oid)` for [`Value::ObjectIdentifier`], `None` otherwise.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use async_snmp::{Value, oid};
278    ///
279    /// let v = Value::ObjectIdentifier(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0));
280    /// let oid = v.as_oid().unwrap();
281    /// assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.2.0");
282    ///
283    /// // Other types return None
284    /// assert_eq!(Value::Integer(42).as_oid(), None);
285    /// ```
286    pub fn as_oid(&self) -> Option<&Oid> {
287        match self {
288            Value::ObjectIdentifier(oid) => Some(oid),
289            _ => None,
290        }
291    }
292
293    /// Try to get as IP address.
294    ///
295    /// Returns `Some(Ipv4Addr)` for [`Value::IpAddress`], `None` otherwise.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use async_snmp::Value;
301    /// use std::net::Ipv4Addr;
302    ///
303    /// let v = Value::IpAddress([192, 168, 1, 1]);
304    /// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(192, 168, 1, 1)));
305    ///
306    /// // Other types return None
307    /// assert_eq!(Value::Integer(42).as_ip(), None);
308    /// ```
309    pub fn as_ip(&self) -> Option<std::net::Ipv4Addr> {
310        match self {
311            Value::IpAddress(bytes) => Some(std::net::Ipv4Addr::from(*bytes)),
312            _ => None,
313        }
314    }
315
316    /// Check if this is an exception value.
317    pub fn is_exception(&self) -> bool {
318        matches!(
319            self,
320            Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView
321        )
322    }
323
324    /// Format an OctetString or Opaque value using RFC 2579 DISPLAY-HINT.
325    ///
326    /// Returns `None` if this is not an OctetString or Opaque value.
327    /// On invalid hint syntax, falls back to hex encoding.
328    ///
329    /// # Example
330    ///
331    /// ```
332    /// use async_snmp::Value;
333    /// use bytes::Bytes;
334    ///
335    /// let mac = Value::OctetString(Bytes::from_static(&[0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]));
336    /// assert_eq!(mac.format_with_hint("1x:"), Some("00:1a:2b:3c:4d:5e".into()));
337    ///
338    /// let integer = Value::Integer(42);
339    /// assert_eq!(integer.format_with_hint("1d"), None);
340    /// ```
341    pub fn format_with_hint(&self, hint: &str) -> Option<String> {
342        match self {
343            Value::OctetString(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
344            Value::Opaque(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
345            _ => None,
346        }
347    }
348
349    /// Encode to BER.
350    pub fn encode(&self, buf: &mut EncodeBuf) {
351        match self {
352            Value::Integer(v) => buf.push_integer(*v),
353            Value::OctetString(data) => buf.push_octet_string(data),
354            Value::Null => buf.push_null(),
355            Value::ObjectIdentifier(oid) => buf.push_oid(oid),
356            Value::IpAddress(addr) => buf.push_ip_address(*addr),
357            Value::Counter32(v) => buf.push_unsigned32(tag::application::COUNTER32, *v),
358            Value::Gauge32(v) => buf.push_unsigned32(tag::application::GAUGE32, *v),
359            Value::TimeTicks(v) => buf.push_unsigned32(tag::application::TIMETICKS, *v),
360            Value::Opaque(data) => {
361                buf.push_bytes(data);
362                buf.push_length(data.len());
363                buf.push_tag(tag::application::OPAQUE);
364            }
365            Value::Counter64(v) => buf.push_integer64(*v),
366            Value::NoSuchObject => {
367                buf.push_length(0);
368                buf.push_tag(tag::context::NO_SUCH_OBJECT);
369            }
370            Value::NoSuchInstance => {
371                buf.push_length(0);
372                buf.push_tag(tag::context::NO_SUCH_INSTANCE);
373            }
374            Value::EndOfMibView => {
375                buf.push_length(0);
376                buf.push_tag(tag::context::END_OF_MIB_VIEW);
377            }
378            Value::Unknown { tag: t, data } => {
379                buf.push_bytes(data);
380                buf.push_length(data.len());
381                buf.push_tag(*t);
382            }
383        }
384    }
385
386    /// Decode from BER.
387    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
388        let tag = decoder.read_tag()?;
389        let len = decoder.read_length()?;
390
391        match tag {
392            tag::universal::INTEGER => {
393                let value = decoder.read_integer_value(len)?;
394                Ok(Value::Integer(value))
395            }
396            tag::universal::OCTET_STRING => {
397                let data = decoder.read_bytes(len)?;
398                Ok(Value::OctetString(data))
399            }
400            tag::universal::NULL => {
401                if len != 0 {
402                    return Err(Error::decode(
403                        decoder.offset(),
404                        DecodeErrorKind::InvalidNull,
405                    ));
406                }
407                Ok(Value::Null)
408            }
409            tag::universal::OBJECT_IDENTIFIER => {
410                let oid = decoder.read_oid_value(len)?;
411                Ok(Value::ObjectIdentifier(oid))
412            }
413            tag::application::IP_ADDRESS => {
414                if len != 4 {
415                    return Err(Error::decode(
416                        decoder.offset(),
417                        DecodeErrorKind::InvalidIpAddressLength { length: len },
418                    ));
419                }
420                let data = decoder.read_bytes(4)?;
421                Ok(Value::IpAddress([data[0], data[1], data[2], data[3]]))
422            }
423            tag::application::COUNTER32 => {
424                let value = decoder.read_unsigned32_value(len)?;
425                Ok(Value::Counter32(value))
426            }
427            tag::application::GAUGE32 => {
428                let value = decoder.read_unsigned32_value(len)?;
429                Ok(Value::Gauge32(value))
430            }
431            tag::application::TIMETICKS => {
432                let value = decoder.read_unsigned32_value(len)?;
433                Ok(Value::TimeTicks(value))
434            }
435            tag::application::OPAQUE => {
436                let data = decoder.read_bytes(len)?;
437                Ok(Value::Opaque(data))
438            }
439            tag::application::COUNTER64 => {
440                let value = decoder.read_integer64_value(len)?;
441                Ok(Value::Counter64(value))
442            }
443            tag::context::NO_SUCH_OBJECT => {
444                if len != 0 {
445                    let _ = decoder.read_bytes(len)?;
446                }
447                Ok(Value::NoSuchObject)
448            }
449            tag::context::NO_SUCH_INSTANCE => {
450                if len != 0 {
451                    let _ = decoder.read_bytes(len)?;
452                }
453                Ok(Value::NoSuchInstance)
454            }
455            tag::context::END_OF_MIB_VIEW => {
456                if len != 0 {
457                    let _ = decoder.read_bytes(len)?;
458                }
459                Ok(Value::EndOfMibView)
460            }
461            // Reject constructed OCTET STRING (0x24).
462            // Net-snmp documents but does not parse constructed form; we follow suit.
463            tag::universal::OCTET_STRING_CONSTRUCTED => Err(Error::decode(
464                decoder.offset(),
465                DecodeErrorKind::ConstructedOctetString,
466            )),
467            _ => {
468                // Unknown tag - preserve for forward compatibility
469                let data = decoder.read_bytes(len)?;
470                Ok(Value::Unknown { tag, data })
471            }
472        }
473    }
474}
475
476impl std::fmt::Display for Value {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        match self {
479            Value::Integer(v) => write!(f, "{}", v),
480            Value::OctetString(data) => {
481                // Try to display as string if it's valid UTF-8
482                if let Ok(s) = std::str::from_utf8(data) {
483                    write!(f, "{}", s)
484                } else {
485                    write!(f, "0x{}", hex::encode(data))
486                }
487            }
488            Value::Null => write!(f, "NULL"),
489            Value::ObjectIdentifier(oid) => write!(f, "{}", oid),
490            Value::IpAddress(addr) => {
491                write!(f, "{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3])
492            }
493            Value::Counter32(v) => write!(f, "{}", v),
494            Value::Gauge32(v) => write!(f, "{}", v),
495            Value::TimeTicks(v) => {
496                // Display as time
497                let secs = v / 100;
498                let days = secs / 86400;
499                let hours = (secs % 86400) / 3600;
500                let mins = (secs % 3600) / 60;
501                let s = secs % 60;
502                write!(f, "{}d {}h {}m {}s", days, hours, mins, s)
503            }
504            Value::Opaque(data) => write!(f, "Opaque(0x{})", hex::encode(data)),
505            Value::Counter64(v) => write!(f, "{}", v),
506            Value::NoSuchObject => write!(f, "noSuchObject"),
507            Value::NoSuchInstance => write!(f, "noSuchInstance"),
508            Value::EndOfMibView => write!(f, "endOfMibView"),
509            Value::Unknown { tag, data } => {
510                write!(
511                    f,
512                    "Unknown(tag=0x{:02X}, data=0x{})",
513                    tag,
514                    hex::encode(data)
515                )
516            }
517        }
518    }
519}
520
521/// Convenience conversions for creating [`Value`] from common Rust types.
522///
523/// # Examples
524///
525/// ```
526/// use async_snmp::Value;
527/// use bytes::Bytes;
528///
529/// // From integers
530/// let v: Value = 42i32.into();
531/// assert_eq!(v.as_i32(), Some(42));
532///
533/// // From strings (creates OctetString)
534/// let v: Value = "hello".into();
535/// assert_eq!(v.as_str(), Some("hello"));
536///
537/// // From String
538/// let v: Value = String::from("world").into();
539/// assert_eq!(v.as_str(), Some("world"));
540///
541/// // From byte slices
542/// let v: Value = (&[1u8, 2, 3][..]).into();
543/// assert_eq!(v.as_bytes(), Some(&[1, 2, 3][..]));
544///
545/// // From Bytes
546/// let v: Value = Bytes::from_static(b"data").into();
547/// assert_eq!(v.as_bytes(), Some(b"data".as_slice()));
548///
549/// // From u64 (creates Counter64)
550/// let v: Value = 10_000_000_000u64.into();
551/// assert_eq!(v.as_u64(), Some(10_000_000_000));
552///
553/// // From Ipv4Addr
554/// use std::net::Ipv4Addr;
555/// let v: Value = Ipv4Addr::new(10, 0, 0, 1).into();
556/// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(10, 0, 0, 1)));
557///
558/// // From [u8; 4] (creates IpAddress)
559/// let v: Value = [192u8, 168, 1, 1].into();
560/// assert!(matches!(v, Value::IpAddress([192, 168, 1, 1])));
561/// ```
562impl From<i32> for Value {
563    fn from(v: i32) -> Self {
564        Value::Integer(v)
565    }
566}
567
568impl From<&str> for Value {
569    fn from(s: &str) -> Self {
570        Value::OctetString(Bytes::copy_from_slice(s.as_bytes()))
571    }
572}
573
574impl From<String> for Value {
575    fn from(s: String) -> Self {
576        Value::OctetString(Bytes::from(s))
577    }
578}
579
580impl From<&[u8]> for Value {
581    fn from(data: &[u8]) -> Self {
582        Value::OctetString(Bytes::copy_from_slice(data))
583    }
584}
585
586impl From<Oid> for Value {
587    fn from(oid: Oid) -> Self {
588        Value::ObjectIdentifier(oid)
589    }
590}
591
592impl From<std::net::Ipv4Addr> for Value {
593    fn from(addr: std::net::Ipv4Addr) -> Self {
594        Value::IpAddress(addr.octets())
595    }
596}
597
598impl From<Bytes> for Value {
599    fn from(data: Bytes) -> Self {
600        Value::OctetString(data)
601    }
602}
603
604impl From<u64> for Value {
605    fn from(v: u64) -> Self {
606        Value::Counter64(v)
607    }
608}
609
610impl From<[u8; 4]> for Value {
611    fn from(addr: [u8; 4]) -> Self {
612        Value::IpAddress(addr)
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    // AUDIT-003: Test that constructed OCTET STRING (0x24) is explicitly rejected.
621    // Net-snmp documents but does not parse constructed form; we reject it.
622    #[test]
623    fn test_reject_constructed_octet_string() {
624        // Constructed OCTET STRING has tag 0x24 (0x04 | 0x20)
625        // Create a fake BER-encoded constructed OCTET STRING: 0x24 0x03 0x04 0x01 0x41
626        // (constructed OCTET STRING containing primitive OCTET STRING "A")
627        let data = bytes::Bytes::from_static(&[0x24, 0x03, 0x04, 0x01, 0x41]);
628        let mut decoder = Decoder::new(data);
629        let result = Value::decode(&mut decoder);
630
631        assert!(
632            result.is_err(),
633            "constructed OCTET STRING (0x24) should be rejected"
634        );
635        let err = result.unwrap_err();
636        let err_msg = format!("{}", err);
637        assert!(
638            err_msg.contains("constructed OCTET STRING"),
639            "error message should mention 'constructed OCTET STRING', got: {}",
640            err_msg
641        );
642    }
643
644    #[test]
645    fn test_primitive_octet_string_accepted() {
646        // Primitive OCTET STRING (0x04) should be accepted
647        let data = bytes::Bytes::from_static(&[0x04, 0x03, 0x41, 0x42, 0x43]); // "ABC"
648        let mut decoder = Decoder::new(data);
649        let result = Value::decode(&mut decoder);
650
651        assert!(result.is_ok(), "primitive OCTET STRING should be accepted");
652        let value = result.unwrap();
653        assert_eq!(value.as_bytes(), Some(&b"ABC"[..]));
654    }
655
656    // ========================================================================
657    // Value Type Encoding/Decoding Tests
658    // ========================================================================
659
660    fn roundtrip(value: Value) -> Value {
661        let mut buf = EncodeBuf::new();
662        value.encode(&mut buf);
663        let data = buf.finish();
664        let mut decoder = Decoder::new(data);
665        Value::decode(&mut decoder).unwrap()
666    }
667
668    #[test]
669    fn test_integer_positive() {
670        let value = Value::Integer(42);
671        assert_eq!(roundtrip(value.clone()), value);
672    }
673
674    #[test]
675    fn test_integer_negative() {
676        let value = Value::Integer(-42);
677        assert_eq!(roundtrip(value.clone()), value);
678    }
679
680    #[test]
681    fn test_integer_zero() {
682        let value = Value::Integer(0);
683        assert_eq!(roundtrip(value.clone()), value);
684    }
685
686    #[test]
687    fn test_integer_min() {
688        let value = Value::Integer(i32::MIN);
689        assert_eq!(roundtrip(value.clone()), value);
690    }
691
692    #[test]
693    fn test_integer_max() {
694        let value = Value::Integer(i32::MAX);
695        assert_eq!(roundtrip(value.clone()), value);
696    }
697
698    #[test]
699    fn test_octet_string_ascii() {
700        let value = Value::OctetString(Bytes::from_static(b"hello world"));
701        assert_eq!(roundtrip(value.clone()), value);
702    }
703
704    #[test]
705    fn test_octet_string_binary() {
706        let value = Value::OctetString(Bytes::from_static(&[0x00, 0xFF, 0x80, 0x7F]));
707        assert_eq!(roundtrip(value.clone()), value);
708    }
709
710    #[test]
711    fn test_octet_string_empty() {
712        let value = Value::OctetString(Bytes::new());
713        assert_eq!(roundtrip(value.clone()), value);
714    }
715
716    #[test]
717    fn test_null() {
718        let value = Value::Null;
719        assert_eq!(roundtrip(value.clone()), value);
720    }
721
722    #[test]
723    fn test_object_identifier() {
724        let value = Value::ObjectIdentifier(crate::oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
725        assert_eq!(roundtrip(value.clone()), value);
726    }
727
728    #[test]
729    fn test_ip_address() {
730        let value = Value::IpAddress([192, 168, 1, 1]);
731        assert_eq!(roundtrip(value.clone()), value);
732    }
733
734    #[test]
735    fn test_ip_address_zero() {
736        let value = Value::IpAddress([0, 0, 0, 0]);
737        assert_eq!(roundtrip(value.clone()), value);
738    }
739
740    #[test]
741    fn test_ip_address_broadcast() {
742        let value = Value::IpAddress([255, 255, 255, 255]);
743        assert_eq!(roundtrip(value.clone()), value);
744    }
745
746    #[test]
747    fn test_counter32() {
748        let value = Value::Counter32(999999);
749        assert_eq!(roundtrip(value.clone()), value);
750    }
751
752    #[test]
753    fn test_counter32_zero() {
754        let value = Value::Counter32(0);
755        assert_eq!(roundtrip(value.clone()), value);
756    }
757
758    #[test]
759    fn test_counter32_max() {
760        let value = Value::Counter32(u32::MAX);
761        assert_eq!(roundtrip(value.clone()), value);
762    }
763
764    #[test]
765    fn test_gauge32() {
766        let value = Value::Gauge32(1000000000);
767        assert_eq!(roundtrip(value.clone()), value);
768    }
769
770    #[test]
771    fn test_gauge32_max() {
772        let value = Value::Gauge32(u32::MAX);
773        assert_eq!(roundtrip(value.clone()), value);
774    }
775
776    #[test]
777    fn test_timeticks() {
778        let value = Value::TimeTicks(123456);
779        assert_eq!(roundtrip(value.clone()), value);
780    }
781
782    #[test]
783    fn test_timeticks_max() {
784        let value = Value::TimeTicks(u32::MAX);
785        assert_eq!(roundtrip(value.clone()), value);
786    }
787
788    #[test]
789    fn test_opaque() {
790        let value = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
791        assert_eq!(roundtrip(value.clone()), value);
792    }
793
794    #[test]
795    fn test_opaque_empty() {
796        let value = Value::Opaque(Bytes::new());
797        assert_eq!(roundtrip(value.clone()), value);
798    }
799
800    #[test]
801    fn test_counter64() {
802        let value = Value::Counter64(123456789012345);
803        assert_eq!(roundtrip(value.clone()), value);
804    }
805
806    #[test]
807    fn test_counter64_zero() {
808        let value = Value::Counter64(0);
809        assert_eq!(roundtrip(value.clone()), value);
810    }
811
812    #[test]
813    fn test_counter64_max() {
814        let value = Value::Counter64(u64::MAX);
815        assert_eq!(roundtrip(value.clone()), value);
816    }
817
818    #[test]
819    fn test_no_such_object() {
820        let value = Value::NoSuchObject;
821        assert_eq!(roundtrip(value.clone()), value);
822    }
823
824    #[test]
825    fn test_no_such_instance() {
826        let value = Value::NoSuchInstance;
827        assert_eq!(roundtrip(value.clone()), value);
828    }
829
830    #[test]
831    fn test_end_of_mib_view() {
832        let value = Value::EndOfMibView;
833        assert_eq!(roundtrip(value.clone()), value);
834    }
835
836    #[test]
837    fn test_unknown_tag_preserved() {
838        // Tag 0x45 is application class but not a standard SNMP type
839        let data = Bytes::from_static(&[0x45, 0x03, 0x01, 0x02, 0x03]);
840        let mut decoder = Decoder::new(data);
841        let value = Value::decode(&mut decoder).unwrap();
842
843        match value {
844            Value::Unknown { tag, ref data } => {
845                assert_eq!(tag, 0x45);
846                assert_eq!(data.as_ref(), &[0x01, 0x02, 0x03]);
847            }
848            _ => panic!("expected Unknown variant"),
849        }
850
851        // Roundtrip should preserve
852        assert_eq!(roundtrip(value.clone()), value);
853    }
854
855    // ========================================================================
856    // Accessor Method Tests
857    // ========================================================================
858
859    #[test]
860    fn test_as_i32() {
861        assert_eq!(Value::Integer(42).as_i32(), Some(42));
862        assert_eq!(Value::Integer(-42).as_i32(), Some(-42));
863        assert_eq!(Value::Counter32(100).as_i32(), None);
864        assert_eq!(Value::Null.as_i32(), None);
865    }
866
867    #[test]
868    fn test_as_u32() {
869        assert_eq!(Value::Counter32(100).as_u32(), Some(100));
870        assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
871        assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
872        assert_eq!(Value::Integer(50).as_u32(), Some(50));
873        assert_eq!(Value::Integer(-1).as_u32(), None);
874        assert_eq!(Value::Counter64(100).as_u32(), None);
875    }
876
877    #[test]
878    fn test_as_u64() {
879        assert_eq!(Value::Counter64(100).as_u64(), Some(100));
880        assert_eq!(Value::Counter32(100).as_u64(), Some(100));
881        assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
882        assert_eq!(Value::TimeTicks(300).as_u64(), Some(300));
883        assert_eq!(Value::Integer(50).as_u64(), Some(50));
884        assert_eq!(Value::Integer(-1).as_u64(), None);
885    }
886
887    #[test]
888    fn test_as_bytes() {
889        let s = Value::OctetString(Bytes::from_static(b"test"));
890        assert_eq!(s.as_bytes(), Some(b"test".as_slice()));
891
892        let o = Value::Opaque(Bytes::from_static(b"data"));
893        assert_eq!(o.as_bytes(), Some(b"data".as_slice()));
894
895        assert_eq!(Value::Integer(1).as_bytes(), None);
896    }
897
898    #[test]
899    fn test_as_str() {
900        let s = Value::OctetString(Bytes::from_static(b"hello"));
901        assert_eq!(s.as_str(), Some("hello"));
902
903        // Invalid UTF-8 returns None
904        let invalid = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
905        assert_eq!(invalid.as_str(), None);
906
907        assert_eq!(Value::Integer(1).as_str(), None);
908    }
909
910    #[test]
911    fn test_as_oid() {
912        let oid = crate::oid!(1, 3, 6, 1);
913        let v = Value::ObjectIdentifier(oid.clone());
914        assert_eq!(v.as_oid(), Some(&oid));
915
916        assert_eq!(Value::Integer(1).as_oid(), None);
917    }
918
919    #[test]
920    fn test_as_ip() {
921        let v = Value::IpAddress([192, 168, 1, 1]);
922        assert_eq!(v.as_ip(), Some(std::net::Ipv4Addr::new(192, 168, 1, 1)));
923
924        assert_eq!(Value::Integer(1).as_ip(), None);
925    }
926
927    // ========================================================================
928    // is_exception() Tests
929    // ========================================================================
930
931    #[test]
932    fn test_is_exception() {
933        assert!(Value::NoSuchObject.is_exception());
934        assert!(Value::NoSuchInstance.is_exception());
935        assert!(Value::EndOfMibView.is_exception());
936
937        assert!(!Value::Integer(1).is_exception());
938        assert!(!Value::Null.is_exception());
939        assert!(!Value::OctetString(Bytes::new()).is_exception());
940    }
941
942    // ========================================================================
943    // Display Trait Tests
944    // ========================================================================
945
946    #[test]
947    fn test_display_integer() {
948        assert_eq!(format!("{}", Value::Integer(42)), "42");
949        assert_eq!(format!("{}", Value::Integer(-42)), "-42");
950    }
951
952    #[test]
953    fn test_display_octet_string_utf8() {
954        let v = Value::OctetString(Bytes::from_static(b"hello"));
955        assert_eq!(format!("{}", v), "hello");
956    }
957
958    #[test]
959    fn test_display_octet_string_binary() {
960        // Use bytes that are not valid UTF-8 (0xFF is never valid in UTF-8)
961        let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
962        assert_eq!(format!("{}", v), "0xfffe");
963    }
964
965    #[test]
966    fn test_display_null() {
967        assert_eq!(format!("{}", Value::Null), "NULL");
968    }
969
970    #[test]
971    fn test_display_ip_address() {
972        let v = Value::IpAddress([192, 168, 1, 1]);
973        assert_eq!(format!("{}", v), "192.168.1.1");
974    }
975
976    #[test]
977    fn test_display_counter32() {
978        assert_eq!(format!("{}", Value::Counter32(999)), "999");
979    }
980
981    #[test]
982    fn test_display_gauge32() {
983        assert_eq!(format!("{}", Value::Gauge32(1000)), "1000");
984    }
985
986    #[test]
987    fn test_display_timeticks() {
988        // 123456 hundredths = 1234.56 seconds
989        // = 0d 0h 20m 34s
990        let v = Value::TimeTicks(123456);
991        assert_eq!(format!("{}", v), "0d 0h 20m 34s");
992    }
993
994    #[test]
995    fn test_display_opaque() {
996        let v = Value::Opaque(Bytes::from_static(&[0xBE, 0xEF]));
997        assert_eq!(format!("{}", v), "Opaque(0xbeef)");
998    }
999
1000    #[test]
1001    fn test_display_counter64() {
1002        assert_eq!(format!("{}", Value::Counter64(12345678)), "12345678");
1003    }
1004
1005    #[test]
1006    fn test_display_exceptions() {
1007        assert_eq!(format!("{}", Value::NoSuchObject), "noSuchObject");
1008        assert_eq!(format!("{}", Value::NoSuchInstance), "noSuchInstance");
1009        assert_eq!(format!("{}", Value::EndOfMibView), "endOfMibView");
1010    }
1011
1012    #[test]
1013    fn test_display_unknown() {
1014        let v = Value::Unknown {
1015            tag: 0x99,
1016            data: Bytes::from_static(&[0x01, 0x02]),
1017        };
1018        assert_eq!(format!("{}", v), "Unknown(tag=0x99, data=0x0102)");
1019    }
1020
1021    // ========================================================================
1022    // From Conversion Tests
1023    // ========================================================================
1024
1025    #[test]
1026    fn test_from_i32() {
1027        let v: Value = 42i32.into();
1028        assert_eq!(v, Value::Integer(42));
1029    }
1030
1031    #[test]
1032    fn test_from_str() {
1033        let v: Value = "hello".into();
1034        assert_eq!(v.as_str(), Some("hello"));
1035    }
1036
1037    #[test]
1038    fn test_from_string() {
1039        let v: Value = String::from("hello").into();
1040        assert_eq!(v.as_str(), Some("hello"));
1041    }
1042
1043    #[test]
1044    fn test_from_bytes_slice() {
1045        let v: Value = (&[1u8, 2, 3][..]).into();
1046        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
1047    }
1048
1049    #[test]
1050    fn test_from_oid() {
1051        let oid = crate::oid!(1, 3, 6, 1);
1052        let v: Value = oid.clone().into();
1053        assert_eq!(v.as_oid(), Some(&oid));
1054    }
1055
1056    #[test]
1057    fn test_from_ipv4addr() {
1058        let addr = std::net::Ipv4Addr::new(10, 0, 0, 1);
1059        let v: Value = addr.into();
1060        assert_eq!(v, Value::IpAddress([10, 0, 0, 1]));
1061    }
1062
1063    #[test]
1064    fn test_from_bytes() {
1065        let data = Bytes::from_static(b"hello");
1066        let v: Value = data.into();
1067        assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
1068    }
1069
1070    #[test]
1071    fn test_from_u64() {
1072        let v: Value = 12345678901234u64.into();
1073        assert_eq!(v, Value::Counter64(12345678901234));
1074    }
1075
1076    #[test]
1077    fn test_from_ip_array() {
1078        let v: Value = [192u8, 168, 1, 1].into();
1079        assert_eq!(v, Value::IpAddress([192, 168, 1, 1]));
1080    }
1081
1082    // ========================================================================
1083    // Decode Error Tests
1084    // ========================================================================
1085
1086    #[test]
1087    fn test_decode_invalid_null_length() {
1088        // NULL must have length 0
1089        let data = Bytes::from_static(&[0x05, 0x01, 0x00]); // NULL with length 1
1090        let mut decoder = Decoder::new(data);
1091        let result = Value::decode(&mut decoder);
1092        assert!(result.is_err());
1093    }
1094
1095    #[test]
1096    fn test_decode_invalid_ip_address_length() {
1097        // IpAddress must have length 4
1098        let data = Bytes::from_static(&[0x40, 0x03, 0x01, 0x02, 0x03]); // Only 3 bytes
1099        let mut decoder = Decoder::new(data);
1100        let result = Value::decode(&mut decoder);
1101        assert!(result.is_err());
1102    }
1103
1104    #[test]
1105    fn test_decode_exception_with_content_accepted() {
1106        // Per implementation, exceptions with non-zero length have content skipped
1107        let data = Bytes::from_static(&[0x80, 0x01, 0xFF]); // NoSuchObject with 1 byte
1108        let mut decoder = Decoder::new(data);
1109        let result = Value::decode(&mut decoder);
1110        assert!(result.is_ok());
1111        assert_eq!(result.unwrap(), Value::NoSuchObject);
1112    }
1113}