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