Skip to main content

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/// RFC 2579 `RowStatus` textual convention.
13///
14/// Used by SNMP tables to control row creation, modification, and deletion.
15/// The state machine for `RowStatus` is defined in RFC 2579 Section 7.1.
16///
17/// # State Transitions
18///
19/// | Current State | Set to | Result |
20/// |--------------|--------|--------|
21/// | (none) | createAndGo | row created in `active` state |
22/// | (none) | createAndWait | row created in `notInService` or `notReady` |
23/// | notInService | active | row becomes operational |
24/// | notReady | active | error (row must first be notInService) |
25/// | active | notInService | row becomes inactive |
26/// | any | destroy | row is deleted |
27///
28/// # Example
29///
30/// ```
31/// use async_snmp::{Value, RowStatus};
32///
33/// // Reading a RowStatus column
34/// let value = Value::Integer(1);
35/// assert_eq!(value.as_row_status(), Some(RowStatus::Active));
36///
37/// // Creating a value to write
38/// let create: Value = RowStatus::CreateAndGo.into();
39/// assert_eq!(create, Value::Integer(4));
40/// ```
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum RowStatus {
43    /// Row is operational and available for use.
44    Active = 1,
45    /// Row exists but is not operational (e.g., being modified).
46    NotInService = 2,
47    /// Row exists but required columns are missing or invalid.
48    NotReady = 3,
49    /// Request to create a new row that immediately becomes active.
50    CreateAndGo = 4,
51    /// Request to create a new row that starts in notInService/notReady.
52    CreateAndWait = 5,
53    /// Request to delete an existing row.
54    Destroy = 6,
55}
56
57impl TryFrom<i32> for RowStatus {
58    type Error = i32;
59
60    fn try_from(value: i32) -> std::result::Result<Self, i32> {
61        match value {
62            1 => Ok(Self::Active),
63            2 => Ok(Self::NotInService),
64            3 => Ok(Self::NotReady),
65            4 => Ok(Self::CreateAndGo),
66            5 => Ok(Self::CreateAndWait),
67            6 => Ok(Self::Destroy),
68            _ => Err(value),
69        }
70    }
71}
72
73impl RowStatus {
74    /// Create from raw SNMP integer value.
75    ///
76    /// Returns `None` for values outside the valid range (1-6).
77    #[must_use]
78    pub fn from_i32(value: i32) -> Option<Self> {
79        Self::try_from(value).ok()
80    }
81}
82
83impl From<RowStatus> for Value {
84    fn from(status: RowStatus) -> Self {
85        Value::Integer(status as i32)
86    }
87}
88
89impl std::fmt::Display for RowStatus {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::Active => write!(f, "active"),
93            Self::NotInService => write!(f, "notInService"),
94            Self::NotReady => write!(f, "notReady"),
95            Self::CreateAndGo => write!(f, "createAndGo"),
96            Self::CreateAndWait => write!(f, "createAndWait"),
97            Self::Destroy => write!(f, "destroy"),
98        }
99    }
100}
101
102/// RFC 2579 `StorageType` textual convention.
103///
104/// Describes how an SNMP row's data is stored and persisted.
105///
106/// # Persistence Levels
107///
108/// | Type | Survives Reboot | Writable |
109/// |------|-----------------|----------|
110/// | other | undefined | varies |
111/// | volatile | no | yes |
112/// | nonVolatile | yes | yes |
113/// | permanent | yes | limited |
114/// | readOnly | yes | no |
115///
116/// # Example
117///
118/// ```
119/// use async_snmp::{Value, StorageType};
120///
121/// // Reading a StorageType column
122/// let value = Value::Integer(3);
123/// assert_eq!(value.as_storage_type(), Some(StorageType::NonVolatile));
124///
125/// // Creating a value to write
126/// let storage: Value = StorageType::Volatile.into();
127/// assert_eq!(storage, Value::Integer(2));
128/// ```
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum StorageType {
131    /// Implementation-specific storage.
132    Other = 1,
133    /// Lost on reboot; can be modified.
134    Volatile = 2,
135    /// Survives reboot; can be modified.
136    NonVolatile = 3,
137    /// Survives reboot; limited modifications allowed.
138    Permanent = 4,
139    /// Survives reboot; cannot be modified.
140    ReadOnly = 5,
141}
142
143impl TryFrom<i32> for StorageType {
144    type Error = i32;
145
146    fn try_from(value: i32) -> std::result::Result<Self, i32> {
147        match value {
148            1 => Ok(Self::Other),
149            2 => Ok(Self::Volatile),
150            3 => Ok(Self::NonVolatile),
151            4 => Ok(Self::Permanent),
152            5 => Ok(Self::ReadOnly),
153            _ => Err(value),
154        }
155    }
156}
157
158impl StorageType {
159    /// Create from raw SNMP integer value.
160    ///
161    /// Returns `None` for values outside the valid range (1-5).
162    #[must_use]
163    pub fn from_i32(value: i32) -> Option<Self> {
164        Self::try_from(value).ok()
165    }
166}
167
168impl From<StorageType> for Value {
169    fn from(storage: StorageType) -> Self {
170        Value::Integer(storage as i32)
171    }
172}
173
174impl std::fmt::Display for StorageType {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            Self::Other => write!(f, "other"),
178            Self::Volatile => write!(f, "volatile"),
179            Self::NonVolatile => write!(f, "nonVolatile"),
180            Self::Permanent => write!(f, "permanent"),
181            Self::ReadOnly => write!(f, "readOnly"),
182        }
183    }
184}
185
186/// SNMP value.
187///
188/// Represents all SNMP data types including `SMIv2` types and exception values.
189#[derive(Debug, Clone, PartialEq, Eq, Hash)]
190#[non_exhaustive]
191pub enum Value {
192    /// INTEGER (ASN.1 primitive, signed 32-bit)
193    Integer(i32),
194
195    /// OCTET STRING (arbitrary bytes).
196    ///
197    /// Per RFC 2578 (`SMIv2`), OCTET STRING values have a maximum size of 65535 octets.
198    /// This limit is **not enforced** during decoding to maintain permissive parsing
199    /// behavior. Applications that require strict compliance should validate size
200    /// after decoding.
201    OctetString(Bytes),
202
203    /// NULL
204    Null,
205
206    /// OBJECT IDENTIFIER
207    ObjectIdentifier(Oid),
208
209    /// `IpAddress` (4 bytes, big-endian)
210    IpAddress([u8; 4]),
211
212    /// Counter32 (unsigned 32-bit, wrapping)
213    Counter32(u32),
214
215    /// Gauge32 / Unsigned32 (unsigned 32-bit, non-wrapping)
216    Gauge32(u32),
217
218    /// `TimeTicks` (hundredths of seconds since epoch)
219    TimeTicks(u32),
220
221    /// Opaque (legacy, arbitrary bytes)
222    Opaque(Bytes),
223
224    /// Counter64 (unsigned 64-bit, wrapping).
225    ///
226    /// **SNMPv2c/v3 only.** Counter64 was introduced in `SNMPv2` (RFC 2578) and is
227    /// not supported in `SNMPv1`. When sending Counter64 values to an `SNMPv1` agent,
228    /// the value will be silently ignored or cause an error depending on the agent
229    /// implementation.
230    ///
231    /// If your application needs to support `SNMPv1`, avoid using Counter64 or
232    /// fall back to Counter32 (with potential overflow for high-bandwidth counters).
233    Counter64(u64),
234
235    /// noSuchObject exception - the requested OID exists in the MIB but has no value.
236    ///
237    /// This exception indicates that the agent recognizes the OID (it's a valid
238    /// MIB object), but there is no instance available. This commonly occurs when
239    /// requesting a table column OID without an index.
240    ///
241    /// # Example
242    ///
243    /// ```
244    /// use async_snmp::Value;
245    ///
246    /// let response = Value::NoSuchObject;
247    /// assert!(response.is_exception());
248    ///
249    /// // When handling responses, check for exceptions:
250    /// match response {
251    ///     Value::NoSuchObject => println!("OID exists but has no value"),
252    ///     _ => {}
253    /// }
254    /// ```
255    NoSuchObject,
256
257    /// noSuchInstance exception - the specific instance does not exist.
258    ///
259    /// This exception indicates that while the MIB object exists, the specific
260    /// instance (index) requested does not. This commonly occurs when querying
261    /// a table row that doesn't exist.
262    ///
263    /// # Example
264    ///
265    /// ```
266    /// use async_snmp::Value;
267    ///
268    /// let response = Value::NoSuchInstance;
269    /// assert!(response.is_exception());
270    /// ```
271    NoSuchInstance,
272
273    /// endOfMibView exception - end of the MIB has been reached.
274    ///
275    /// This exception is returned during GETNEXT/GETBULK operations when
276    /// there are no more OIDs lexicographically greater than the requested OID.
277    /// This is the normal termination condition for SNMP walks.
278    ///
279    /// # Example
280    ///
281    /// ```
282    /// use async_snmp::Value;
283    ///
284    /// let response = Value::EndOfMibView;
285    /// assert!(response.is_exception());
286    ///
287    /// // Commonly used to detect end of walk
288    /// if matches!(response, Value::EndOfMibView) {
289    ///     println!("Walk complete - reached end of MIB");
290    /// }
291    /// ```
292    EndOfMibView,
293
294    /// Unknown/unrecognized value type (for forward compatibility)
295    Unknown { tag: u8, data: Bytes },
296}
297
298impl Value {
299    /// Try to get as i32.
300    ///
301    /// Returns `Some(i32)` for [`Value::Integer`], `None` otherwise.
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use async_snmp::Value;
307    ///
308    /// let v = Value::Integer(42);
309    /// assert_eq!(v.as_i32(), Some(42));
310    ///
311    /// let v = Value::Integer(-100);
312    /// assert_eq!(v.as_i32(), Some(-100));
313    ///
314    /// // Counter32 is not an Integer
315    /// let v = Value::Counter32(42);
316    /// assert_eq!(v.as_i32(), None);
317    /// ```
318    pub fn as_i32(&self) -> Option<i32> {
319        match self {
320            Value::Integer(v) => Some(*v),
321            _ => None,
322        }
323    }
324
325    /// Try to get as u32.
326    ///
327    /// Returns `Some(u32)` for [`Value::Counter32`], [`Value::Gauge32`],
328    /// [`Value::TimeTicks`], or non-negative [`Value::Integer`]. Returns `None` otherwise.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use async_snmp::Value;
334    ///
335    /// // Works for Counter32, Gauge32, TimeTicks
336    /// assert_eq!(Value::Counter32(100).as_u32(), Some(100));
337    /// assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
338    /// assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
339    ///
340    /// // Works for non-negative integers
341    /// assert_eq!(Value::Integer(50).as_u32(), Some(50));
342    ///
343    /// // Returns None for negative integers
344    /// assert_eq!(Value::Integer(-1).as_u32(), None);
345    ///
346    /// // Counter64 returns None (use as_u64 instead)
347    /// assert_eq!(Value::Counter64(100).as_u32(), None);
348    /// ```
349    pub fn as_u32(&self) -> Option<u32> {
350        match self {
351            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v),
352            Value::Integer(v) if *v >= 0 => Some(*v as u32),
353            _ => None,
354        }
355    }
356
357    /// Try to get as u64.
358    ///
359    /// Returns `Some(u64)` for [`Value::Counter64`], or any 32-bit unsigned type
360    /// ([`Value::Counter32`], [`Value::Gauge32`], [`Value::TimeTicks`]), or
361    /// non-negative [`Value::Integer`]. Returns `None` otherwise.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use async_snmp::Value;
367    ///
368    /// // Counter64 is the primary use case
369    /// assert_eq!(Value::Counter64(10_000_000_000).as_u64(), Some(10_000_000_000));
370    ///
371    /// // Also works for 32-bit unsigned types
372    /// assert_eq!(Value::Counter32(100).as_u64(), Some(100));
373    /// assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
374    ///
375    /// // Non-negative integers work
376    /// assert_eq!(Value::Integer(50).as_u64(), Some(50));
377    ///
378    /// // Negative integers return None
379    /// assert_eq!(Value::Integer(-1).as_u64(), None);
380    /// ```
381    pub fn as_u64(&self) -> Option<u64> {
382        match self {
383            Value::Counter64(v) => Some(*v),
384            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(u64::from(*v)),
385            Value::Integer(v) if *v >= 0 => Some(*v as u64),
386            _ => None,
387        }
388    }
389
390    /// Try to get as bytes.
391    ///
392    /// Returns `Some(&[u8])` for [`Value::OctetString`] or [`Value::Opaque`].
393    /// Returns `None` otherwise.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// use async_snmp::Value;
399    /// use bytes::Bytes;
400    ///
401    /// let v = Value::OctetString(Bytes::from_static(b"hello"));
402    /// assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
403    ///
404    /// // Works for Opaque too
405    /// let v = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
406    /// assert_eq!(v.as_bytes(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
407    ///
408    /// // Other types return None
409    /// assert_eq!(Value::Integer(42).as_bytes(), None);
410    /// ```
411    pub fn as_bytes(&self) -> Option<&[u8]> {
412        match self {
413            Value::OctetString(v) | Value::Opaque(v) => Some(v),
414            _ => None,
415        }
416    }
417
418    /// Try to get as string (UTF-8).
419    ///
420    /// Returns `Some(&str)` if the value is an [`Value::OctetString`] or [`Value::Opaque`]
421    /// containing valid UTF-8. Returns `None` for other types or invalid UTF-8.
422    ///
423    /// # Examples
424    ///
425    /// ```
426    /// use async_snmp::Value;
427    /// use bytes::Bytes;
428    ///
429    /// let v = Value::OctetString(Bytes::from_static(b"Linux router1 5.4.0"));
430    /// assert_eq!(v.as_str(), Some("Linux router1 5.4.0"));
431    ///
432    /// // Invalid UTF-8 returns None
433    /// let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
434    /// assert_eq!(v.as_str(), None);
435    ///
436    /// // Binary data with valid UTF-8 bytes still works, but use as_bytes() for clarity
437    /// let binary = Value::OctetString(Bytes::from_static(&[0x80, 0x81, 0x82]));
438    /// assert_eq!(binary.as_str(), None); // Invalid UTF-8 sequence
439    /// assert!(binary.as_bytes().is_some());
440    /// ```
441    pub fn as_str(&self) -> Option<&str> {
442        self.as_bytes().and_then(|b| std::str::from_utf8(b).ok())
443    }
444
445    /// Try to get as OID.
446    ///
447    /// Returns `Some(&Oid)` for [`Value::ObjectIdentifier`], `None` otherwise.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// use async_snmp::{Value, oid};
453    ///
454    /// let v = Value::ObjectIdentifier(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0));
455    /// let oid = v.as_oid().unwrap();
456    /// assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.2.0");
457    ///
458    /// // Other types return None
459    /// assert_eq!(Value::Integer(42).as_oid(), None);
460    /// ```
461    pub fn as_oid(&self) -> Option<&Oid> {
462        match self {
463            Value::ObjectIdentifier(oid) => Some(oid),
464            _ => None,
465        }
466    }
467
468    /// Try to get as IP address.
469    ///
470    /// Returns `Some(Ipv4Addr)` for [`Value::IpAddress`], `None` otherwise.
471    ///
472    /// # Examples
473    ///
474    /// ```
475    /// use async_snmp::Value;
476    /// use std::net::Ipv4Addr;
477    ///
478    /// let v = Value::IpAddress([192, 168, 1, 1]);
479    /// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(192, 168, 1, 1)));
480    ///
481    /// // Other types return None
482    /// assert_eq!(Value::Integer(42).as_ip(), None);
483    /// ```
484    pub fn as_ip(&self) -> Option<std::net::Ipv4Addr> {
485        match self {
486            Value::IpAddress(bytes) => Some(std::net::Ipv4Addr::from(*bytes)),
487            _ => None,
488        }
489    }
490
491    /// Extract any numeric value as f64.
492    ///
493    /// Useful for metrics systems and graphing where all values become f64.
494    /// Counter64 values above 2^53 may lose precision.
495    ///
496    /// # Examples
497    ///
498    /// ```
499    /// use async_snmp::Value;
500    ///
501    /// assert_eq!(Value::Integer(42).as_f64(), Some(42.0));
502    /// assert_eq!(Value::Counter32(1000).as_f64(), Some(1000.0));
503    /// assert_eq!(Value::Counter64(10_000_000_000).as_f64(), Some(10_000_000_000.0));
504    /// assert_eq!(Value::Null.as_f64(), None);
505    /// ```
506    #[expect(
507        clippy::cast_precision_loss,
508        reason = "we intentionally bend over backwards to make an f64 available even for 64-bit ints"
509    )]
510    pub fn as_f64(&self) -> Option<f64> {
511        match self {
512            Value::Integer(v) => Some(f64::from(*v)),
513            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(f64::from(*v)),
514            Value::Counter64(v) => Some(*v as f64),
515            _ => None,
516        }
517    }
518
519    /// Extract Counter64 as f64 with wrapping at 2^53.
520    ///
521    /// Prevents precision loss for large counters. IEEE 754 double-precision
522    /// floats have a 53-bit mantissa, so Counter64 values above 2^53 lose
523    /// precision when converted directly. This method wraps at the mantissa
524    /// limit, preserving precision for rate calculations.
525    ///
526    /// Use when computing rates where precision matters more than absolute
527    /// magnitude. For Counter32 and other types, behaves identically to `as_f64()`.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use async_snmp::Value;
533    ///
534    /// // Small values behave the same as as_f64()
535    /// assert_eq!(Value::Counter64(1000).as_f64_wrapped(), Some(1000.0));
536    ///
537    /// // Large Counter64 wraps at 2^53
538    /// let large = 1u64 << 54; // 2^54
539    /// let wrapped = Value::Counter64(large).as_f64_wrapped().unwrap();
540    /// assert!(wrapped < large as f64); // Wrapped to smaller value
541    /// ```
542    #[expect(
543        clippy::cast_precision_loss,
544        reason = "we intentionally bend over backwards to make an f64 available even for 64-bit ints"
545    )]
546    pub fn as_f64_wrapped(&self) -> Option<f64> {
547        const MANTISSA_LIMIT: u64 = 1 << 53;
548        match self {
549            Value::Counter64(v) => Some((*v % MANTISSA_LIMIT) as f64),
550            _ => self.as_f64(),
551        }
552    }
553
554    /// Extract integer with implied decimal places.
555    ///
556    /// Many SNMP sensors report fixed-point values as integers with an
557    /// implied decimal point. This method applies the scaling directly,
558    /// returning a usable f64 value.
559    ///
560    /// This complements `format_with_hint("d-2")` which returns a String
561    /// for display. Use `as_decimal()` when you need the numeric value
562    /// for computation or metrics.
563    ///
564    /// # Examples
565    ///
566    /// ```
567    /// use async_snmp::Value;
568    ///
569    /// // Temperature 2350 with places=2 → 23.50
570    /// assert_eq!(Value::Integer(2350).as_decimal(2), Some(23.50));
571    ///
572    /// // Percentage 9999 with places=2 → 99.99
573    /// assert_eq!(Value::Integer(9999).as_decimal(2), Some(99.99));
574    ///
575    /// // Voltage 12500 with places=3 → 12.500
576    /// assert_eq!(Value::Integer(12500).as_decimal(3), Some(12.5));
577    ///
578    /// // Non-numeric types return None
579    /// assert_eq!(Value::Null.as_decimal(2), None);
580    /// ```
581    pub fn as_decimal(&self, places: u8) -> Option<f64> {
582        let divisor = 10f64.powi(i32::from(places));
583        self.as_f64().map(|v| v / divisor)
584    }
585
586    /// `TimeTicks` as Duration (hundredths of seconds).
587    ///
588    /// `TimeTicks` represents time in hundredths of a second. This method
589    /// converts to `std::time::Duration` for idiomatic Rust time handling.
590    ///
591    /// Common use: sysUpTime, interface last-change timestamps.
592    ///
593    /// # Examples
594    ///
595    /// ```
596    /// use async_snmp::Value;
597    /// use std::time::Duration;
598    ///
599    /// // 360000 ticks = 3600 seconds = 1 hour
600    /// let uptime = Value::TimeTicks(360000);
601    /// assert_eq!(uptime.as_duration(), Some(Duration::from_secs(3600)));
602    ///
603    /// // Non-TimeTicks return None
604    /// assert_eq!(Value::Integer(100).as_duration(), None);
605    /// ```
606    pub fn as_duration(&self) -> Option<std::time::Duration> {
607        match self {
608            Value::TimeTicks(v) => Some(std::time::Duration::from_millis(u64::from(*v) * 10)),
609            _ => None,
610        }
611    }
612
613    /// Extract IEEE 754 float from Opaque value (net-snmp extension).
614    ///
615    /// Decodes the two-layer ASN.1 structure used by net-snmp to encode floats
616    /// inside Opaque values: extension tag (0x9f) + float type (0x78) + length (4)
617    /// + 4 bytes IEEE 754 big-endian float.
618    ///
619    /// This is a non-standard extension supported by net-snmp for agents that
620    /// need to report floating-point values. Standard SNMP uses implied decimal
621    /// points via DISPLAY-HINT instead.
622    ///
623    /// # Examples
624    ///
625    /// ```
626    /// use async_snmp::Value;
627    /// use bytes::Bytes;
628    ///
629    /// // Opaque-encoded float for pi
630    /// let data = Bytes::from_static(&[0x9f, 0x78, 0x04, 0x40, 0x49, 0x0f, 0xdb]);
631    /// let value = Value::Opaque(data);
632    /// let pi = value.as_opaque_float().unwrap();
633    /// assert!((pi - std::f32::consts::PI).abs() < 0.0001);
634    ///
635    /// // Non-Opaque or wrong format returns None
636    /// assert_eq!(Value::Integer(42).as_opaque_float(), None);
637    /// ```
638    pub fn as_opaque_float(&self) -> Option<f32> {
639        match self {
640            Value::Opaque(data)
641                if data.len() >= 7
642                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
643                && data[1] == 0x78       // ASN_OPAQUE_FLOAT
644                && data[2] == 0x04 =>
645            {
646                // length = 4
647                let bytes: [u8; 4] = data[3..7].try_into().ok()?;
648                Some(f32::from_be_bytes(bytes))
649            }
650            _ => None,
651        }
652    }
653
654    /// Extract IEEE 754 double from Opaque value (net-snmp extension).
655    ///
656    /// Decodes the two-layer ASN.1 structure used by net-snmp to encode doubles
657    /// inside Opaque values: extension tag (0x9f) + double type (0x79) + length (8)
658    /// + 8 bytes IEEE 754 big-endian double.
659    ///
660    /// # Examples
661    ///
662    /// ```
663    /// use async_snmp::Value;
664    /// use bytes::Bytes;
665    ///
666    /// // Opaque-encoded double for pi ≈ 3.141592653589793
667    /// let data = Bytes::from_static(&[
668    ///     0x9f, 0x79, 0x08,  // extension tag, double type, length
669    ///     0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2d, 0x18  // IEEE 754 double
670    /// ]);
671    /// let value = Value::Opaque(data);
672    /// let pi = value.as_opaque_double().unwrap();
673    /// assert!((pi - std::f64::consts::PI).abs() < 1e-10);
674    /// ```
675    pub fn as_opaque_double(&self) -> Option<f64> {
676        match self {
677            Value::Opaque(data)
678                if data.len() >= 11
679                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
680                && data[1] == 0x79       // ASN_OPAQUE_DOUBLE
681                && data[2] == 0x08 =>
682            {
683                // length = 8
684                let bytes: [u8; 8] = data[3..11].try_into().ok()?;
685                Some(f64::from_be_bytes(bytes))
686            }
687            _ => None,
688        }
689    }
690
691    /// Extract Counter64 from Opaque value (net-snmp extension for `SNMPv1`).
692    ///
693    /// `SNMPv1` doesn't support Counter64 natively. net-snmp encodes 64-bit
694    /// counters inside Opaque for `SNMPv1` compatibility using extension tag
695    /// (0x9f) + counter64 type (0x76) + length + big-endian bytes.
696    ///
697    /// # Examples
698    ///
699    /// ```
700    /// use async_snmp::Value;
701    /// use bytes::Bytes;
702    ///
703    /// // Opaque-encoded Counter64 with value 0x0123456789ABCDEF
704    /// let data = Bytes::from_static(&[
705    ///     0x9f, 0x76, 0x08,  // extension tag, counter64 type, length
706    ///     0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
707    /// ]);
708    /// let value = Value::Opaque(data);
709    /// assert_eq!(value.as_opaque_counter64(), Some(0x0123456789ABCDEF));
710    /// ```
711    pub fn as_opaque_counter64(&self) -> Option<u64> {
712        self.as_opaque_unsigned(0x76)
713    }
714
715    /// Extract signed 64-bit integer from Opaque value (net-snmp extension).
716    ///
717    /// Uses extension tag (0x9f) + i64 type (0x7a) + length + big-endian bytes.
718    ///
719    /// # Examples
720    ///
721    /// ```
722    /// use async_snmp::Value;
723    /// use bytes::Bytes;
724    ///
725    /// // Opaque-encoded I64 with value -1 (0xFFFFFFFFFFFFFFFF)
726    /// let data = Bytes::from_static(&[
727    ///     0x9f, 0x7a, 0x08,
728    ///     0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
729    /// ]);
730    /// let value = Value::Opaque(data);
731    /// assert_eq!(value.as_opaque_i64(), Some(-1i64));
732    /// ```
733    pub fn as_opaque_i64(&self) -> Option<i64> {
734        match self {
735            Value::Opaque(data)
736                if data.len() >= 4
737                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
738                && data[1] == 0x7a =>
739            {
740                // ASN_OPAQUE_I64
741                let len = data[2] as usize;
742                if data.len() < 3 + len || len == 0 || len > 8 {
743                    return None;
744                }
745                let bytes = &data[3..3 + len];
746                // Sign-extend from the actual length
747                let is_negative = bytes[0] & 0x80 != 0;
748                let mut value: i64 = if is_negative { -1 } else { 0 };
749                for &byte in bytes {
750                    value = (value << 8) | i64::from(byte);
751                }
752                Some(value)
753            }
754            _ => None,
755        }
756    }
757
758    /// Extract unsigned 64-bit integer from Opaque value (net-snmp extension).
759    ///
760    /// Uses extension tag (0x9f) + u64 type (0x7b) + length + big-endian bytes.
761    ///
762    /// # Examples
763    ///
764    /// ```
765    /// use async_snmp::Value;
766    /// use bytes::Bytes;
767    ///
768    /// // Opaque-encoded U64 with value 0x0123456789ABCDEF
769    /// let data = Bytes::from_static(&[
770    ///     0x9f, 0x7b, 0x08,
771    ///     0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
772    /// ]);
773    /// let value = Value::Opaque(data);
774    /// assert_eq!(value.as_opaque_u64(), Some(0x0123456789ABCDEF));
775    /// ```
776    pub fn as_opaque_u64(&self) -> Option<u64> {
777        self.as_opaque_unsigned(0x7b)
778    }
779
780    /// Helper for extracting unsigned 64-bit values from Opaque (Counter64, U64).
781    fn as_opaque_unsigned(&self, expected_type: u8) -> Option<u64> {
782        match self {
783            Value::Opaque(data)
784                if data.len() >= 4
785                && data[0] == 0x9f           // ASN_OPAQUE_TAG1 (extension)
786                && data[1] == expected_type =>
787            {
788                let len = data[2] as usize;
789                if data.len() < 3 + len || len == 0 || len > 8 {
790                    return None;
791                }
792                let bytes = &data[3..3 + len];
793                let mut value: u64 = 0;
794                for &byte in bytes {
795                    value = (value << 8) | u64::from(byte);
796                }
797                Some(value)
798            }
799            _ => None,
800        }
801    }
802
803    /// Extract RFC 2579 `TruthValue` as bool.
804    ///
805    /// `TruthValue` is an INTEGER with: true(1), false(2).
806    /// Returns `None` for non-Integer values or values outside {1, 2}.
807    ///
808    /// # Examples
809    ///
810    /// ```
811    /// use async_snmp::Value;
812    ///
813    /// assert_eq!(Value::Integer(1).as_truth_value(), Some(true));
814    /// assert_eq!(Value::Integer(2).as_truth_value(), Some(false));
815    ///
816    /// // Invalid values return None
817    /// assert_eq!(Value::Integer(0).as_truth_value(), None);
818    /// assert_eq!(Value::Integer(3).as_truth_value(), None);
819    /// assert_eq!(Value::Null.as_truth_value(), None);
820    /// ```
821    pub fn as_truth_value(&self) -> Option<bool> {
822        match self {
823            Value::Integer(1) => Some(true),
824            Value::Integer(2) => Some(false),
825            _ => None,
826        }
827    }
828
829    /// Extract RFC 2579 `RowStatus`.
830    ///
831    /// Returns `None` for non-Integer values or values outside {1-6}.
832    ///
833    /// # Examples
834    ///
835    /// ```
836    /// use async_snmp::{Value, RowStatus};
837    ///
838    /// assert_eq!(Value::Integer(1).as_row_status(), Some(RowStatus::Active));
839    /// assert_eq!(Value::Integer(6).as_row_status(), Some(RowStatus::Destroy));
840    ///
841    /// // Invalid values return None
842    /// assert_eq!(Value::Integer(0).as_row_status(), None);
843    /// assert_eq!(Value::Integer(7).as_row_status(), None);
844    /// assert_eq!(Value::Null.as_row_status(), None);
845    /// ```
846    pub fn as_row_status(&self) -> Option<RowStatus> {
847        match self {
848            Value::Integer(v) => RowStatus::from_i32(*v),
849            _ => None,
850        }
851    }
852
853    /// Extract RFC 2579 `StorageType`.
854    ///
855    /// Returns `None` for non-Integer values or values outside {1-5}.
856    ///
857    /// # Examples
858    ///
859    /// ```
860    /// use async_snmp::{Value, StorageType};
861    ///
862    /// assert_eq!(Value::Integer(2).as_storage_type(), Some(StorageType::Volatile));
863    /// assert_eq!(Value::Integer(3).as_storage_type(), Some(StorageType::NonVolatile));
864    ///
865    /// // Invalid values return None
866    /// assert_eq!(Value::Integer(0).as_storage_type(), None);
867    /// assert_eq!(Value::Integer(6).as_storage_type(), None);
868    /// assert_eq!(Value::Null.as_storage_type(), None);
869    /// ```
870    pub fn as_storage_type(&self) -> Option<StorageType> {
871        match self {
872            Value::Integer(v) => StorageType::from_i32(*v),
873            _ => None,
874        }
875    }
876
877    /// Check if this is an exception value.
878    pub fn is_exception(&self) -> bool {
879        matches!(
880            self,
881            Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView
882        )
883    }
884
885    /// Returns the total BER-encoded size (tag + length + content).
886    pub(crate) fn ber_encoded_size(&self) -> usize {
887        use crate::ber::{
888            integer_content_len, length_encoded_len, unsigned32_content_len, unsigned64_content_len,
889        };
890
891        match self {
892            Value::Integer(v) => {
893                let content_len = integer_content_len(*v);
894                1 + length_encoded_len(content_len) + content_len
895            }
896            Value::OctetString(data) | Value::Opaque(data) | Value::Unknown { data, .. } => {
897                let content_len = data.len();
898                1 + length_encoded_len(content_len) + content_len
899            }
900            Value::Null | Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView => 2, // tag + length(0)
901            Value::ObjectIdentifier(oid) => oid.ber_encoded_size(),
902            Value::IpAddress(_) => 6, // tag + length(4) + 4 bytes
903            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => {
904                let content_len = unsigned32_content_len(*v);
905                1 + length_encoded_len(content_len) + content_len
906            }
907            Value::Counter64(v) => {
908                let content_len = unsigned64_content_len(*v);
909                1 + length_encoded_len(content_len) + content_len
910            }
911        }
912    }
913
914    /// Format an `OctetString`, Opaque, or Integer value using RFC 2579 DISPLAY-HINT.
915    ///
916    /// For `OctetString` and Opaque, uses the OCTET STRING hint format (e.g., "1x:").
917    /// For Integer, uses the INTEGER hint format (e.g., "d-2" for decimal places).
918    ///
919    /// Returns `None` for other value types or invalid hint syntax.
920    /// On invalid OCTET STRING hint syntax, falls back to hex encoding.
921    ///
922    /// # Example
923    ///
924    /// ```
925    /// use async_snmp::Value;
926    /// use bytes::Bytes;
927    ///
928    /// // OctetString: MAC address
929    /// let mac = Value::OctetString(Bytes::from_static(&[0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]));
930    /// assert_eq!(mac.format_with_hint("1x:"), Some("00:1a:2b:3c:4d:5e".into()));
931    ///
932    /// // Integer: Temperature with 2 decimal places
933    /// let temp = Value::Integer(2350);
934    /// assert_eq!(temp.format_with_hint("d-2"), Some("23.50".into()));
935    ///
936    /// // Integer: Hex format
937    /// let int = Value::Integer(255);
938    /// assert_eq!(int.format_with_hint("x"), Some("ff".into()));
939    /// ```
940    pub fn format_with_hint(&self, hint: &str) -> Option<String> {
941        match self {
942            Value::OctetString(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
943            Value::Opaque(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
944            Value::Integer(v) => crate::format::display_hint::apply_integer(hint, *v),
945            _ => None,
946        }
947    }
948
949    /// Encode to BER.
950    pub fn encode(&self, buf: &mut EncodeBuf) {
951        match self {
952            Value::Integer(v) => buf.push_integer(*v),
953            Value::OctetString(data) => buf.push_octet_string(data),
954            Value::Null => buf.push_null(),
955            Value::ObjectIdentifier(oid) => buf.push_oid(oid),
956            Value::IpAddress(addr) => buf.push_ip_address(*addr),
957            Value::Counter32(v) => buf.push_unsigned32(tag::application::COUNTER32, *v),
958            Value::Gauge32(v) => buf.push_unsigned32(tag::application::GAUGE32, *v),
959            Value::TimeTicks(v) => buf.push_unsigned32(tag::application::TIMETICKS, *v),
960            Value::Opaque(data) => {
961                buf.push_bytes(data);
962                buf.push_length(data.len());
963                buf.push_tag(tag::application::OPAQUE);
964            }
965            Value::Counter64(v) => buf.push_integer64(*v),
966            Value::NoSuchObject => {
967                buf.push_length(0);
968                buf.push_tag(tag::context::NO_SUCH_OBJECT);
969            }
970            Value::NoSuchInstance => {
971                buf.push_length(0);
972                buf.push_tag(tag::context::NO_SUCH_INSTANCE);
973            }
974            Value::EndOfMibView => {
975                buf.push_length(0);
976                buf.push_tag(tag::context::END_OF_MIB_VIEW);
977            }
978            Value::Unknown { tag: t, data } => {
979                buf.push_bytes(data);
980                buf.push_length(data.len());
981                buf.push_tag(*t);
982            }
983        }
984    }
985
986    /// Decode from BER.
987    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
988        let tag = decoder.read_tag()?;
989        let len = decoder.read_length()?;
990
991        match tag {
992            tag::universal::INTEGER => {
993                let value = decoder.read_integer_value(len)?;
994                Ok(Value::Integer(value))
995            }
996            tag::universal::OCTET_STRING => {
997                let available = decoder.remaining();
998                let len = if len > available {
999                    // Some devices (notably MikroTik) send OctetString values
1000                    // where the declared length exceeds the enclosing varbind
1001                    // SEQUENCE boundary by 1-2 bytes (firmware bug). Trust the
1002                    // SEQUENCE boundary over the value length field and clamp.
1003                    tracing::warn!(
1004                        target: "async_snmp::value",
1005                        { snmp.offset = %decoder.offset(), declared = len, available },
1006                        "OctetString length exceeds varbind SEQUENCE boundary, clamping to available bytes"
1007                    );
1008                    available
1009                } else {
1010                    len
1011                };
1012                let data = decoder.read_bytes(len)?;
1013                Ok(Value::OctetString(data))
1014            }
1015            tag::universal::NULL => {
1016                if len != 0 {
1017                    tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), kind = %DecodeErrorKind::InvalidNull }, "decode error");
1018                    return Err(Error::MalformedResponse {
1019                        target: UNKNOWN_TARGET,
1020                    }
1021                    .boxed());
1022                }
1023                Ok(Value::Null)
1024            }
1025            tag::universal::OBJECT_IDENTIFIER => {
1026                let oid = decoder.read_oid_value(len)?;
1027                Ok(Value::ObjectIdentifier(oid))
1028            }
1029            tag::application::IP_ADDRESS => {
1030                if len != 4 {
1031                    tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), length = len, kind = %DecodeErrorKind::InvalidIpAddressLength { length: len } }, "decode error");
1032                    return Err(Error::MalformedResponse {
1033                        target: UNKNOWN_TARGET,
1034                    }
1035                    .boxed());
1036                }
1037                let data = decoder.read_bytes(4)?;
1038                Ok(Value::IpAddress([data[0], data[1], data[2], data[3]]))
1039            }
1040            tag::application::COUNTER32 => {
1041                let value = decoder.read_unsigned32_value(len)?;
1042                Ok(Value::Counter32(value))
1043            }
1044            tag::application::GAUGE32 => {
1045                let value = decoder.read_unsigned32_value(len)?;
1046                Ok(Value::Gauge32(value))
1047            }
1048            tag::application::TIMETICKS => {
1049                let value = decoder.read_unsigned32_value(len)?;
1050                Ok(Value::TimeTicks(value))
1051            }
1052            tag::application::OPAQUE => {
1053                let available = decoder.remaining();
1054                let len = if len > available {
1055                    tracing::warn!(
1056                        target: "async_snmp::value",
1057                        { snmp.offset = %decoder.offset(), declared = len, available },
1058                        "Opaque length exceeds varbind SEQUENCE boundary, clamping to available bytes"
1059                    );
1060                    available
1061                } else {
1062                    len
1063                };
1064                let data = decoder.read_bytes(len)?;
1065                Ok(Value::Opaque(data))
1066            }
1067            tag::application::NSAP => {
1068                let data = decoder.read_bytes(len)?;
1069                Ok(Value::OctetString(data))
1070            }
1071            tag::application::COUNTER64 => {
1072                let value = decoder.read_integer64_value(len)?;
1073                Ok(Value::Counter64(value))
1074            }
1075            tag::application::UINTEGER32 => {
1076                let value = decoder.read_unsigned32_value(len)?;
1077                Ok(Value::Gauge32(value))
1078            }
1079            tag::context::NO_SUCH_OBJECT => {
1080                if len != 0 {
1081                    let _ = decoder.read_bytes(len)?;
1082                }
1083                Ok(Value::NoSuchObject)
1084            }
1085            tag::context::NO_SUCH_INSTANCE => {
1086                if len != 0 {
1087                    let _ = decoder.read_bytes(len)?;
1088                }
1089                Ok(Value::NoSuchInstance)
1090            }
1091            tag::context::END_OF_MIB_VIEW => {
1092                if len != 0 {
1093                    let _ = decoder.read_bytes(len)?;
1094                }
1095                Ok(Value::EndOfMibView)
1096            }
1097            // Reject constructed OCTET STRING (0x24).
1098            // Net-snmp documents but does not parse constructed form; we follow suit.
1099            tag::universal::OCTET_STRING_CONSTRUCTED => {
1100                tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), kind = %DecodeErrorKind::ConstructedOctetString }, "decode error");
1101                Err(Error::MalformedResponse {
1102                    target: UNKNOWN_TARGET,
1103                }
1104                .boxed())
1105            }
1106            _ => {
1107                // Unknown tag - preserve for forward compatibility
1108                let data = decoder.read_bytes(len)?;
1109                Ok(Value::Unknown { tag, data })
1110            }
1111        }
1112    }
1113}
1114
1115impl std::fmt::Display for Value {
1116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1117        match self {
1118            Value::Integer(v) => write!(f, "{v}"),
1119            Value::OctetString(data) => {
1120                // Try to display as string if it's valid UTF-8
1121                if let Ok(s) = std::str::from_utf8(data) {
1122                    write!(f, "{s}")
1123                } else {
1124                    write!(f, "0x{}", hex::encode(data))
1125                }
1126            }
1127            Value::Null => write!(f, "NULL"),
1128            Value::ObjectIdentifier(oid) => write!(f, "{oid}"),
1129            Value::IpAddress(addr) => {
1130                write!(f, "{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3])
1131            }
1132            Value::Counter32(v) => write!(f, "{v}"),
1133            Value::Gauge32(v) => write!(f, "{v}"),
1134            Value::TimeTicks(v) => {
1135                write!(f, "{}", crate::format::format_timeticks(*v))
1136            }
1137            Value::Opaque(data) => write!(f, "Opaque(0x{})", hex::encode(data)),
1138            Value::Counter64(v) => write!(f, "{v}"),
1139            Value::NoSuchObject => write!(f, "noSuchObject"),
1140            Value::NoSuchInstance => write!(f, "noSuchInstance"),
1141            Value::EndOfMibView => write!(f, "endOfMibView"),
1142            Value::Unknown { tag, data } => {
1143                write!(
1144                    f,
1145                    "Unknown(tag=0x{:02X}, data=0x{})",
1146                    tag,
1147                    hex::encode(data)
1148                )
1149            }
1150        }
1151    }
1152}
1153
1154/// Convenience conversions for creating [`Value`] from common Rust types.
1155///
1156/// # Examples
1157///
1158/// ```
1159/// use async_snmp::Value;
1160/// use bytes::Bytes;
1161///
1162/// // From integers
1163/// let v: Value = 42i32.into();
1164/// assert_eq!(v.as_i32(), Some(42));
1165///
1166/// // From strings (creates OctetString)
1167/// let v: Value = "hello".into();
1168/// assert_eq!(v.as_str(), Some("hello"));
1169///
1170/// // From String
1171/// let v: Value = String::from("world").into();
1172/// assert_eq!(v.as_str(), Some("world"));
1173///
1174/// // From byte slices
1175/// let v: Value = (&[1u8, 2, 3][..]).into();
1176/// assert_eq!(v.as_bytes(), Some(&[1, 2, 3][..]));
1177///
1178/// // From Bytes
1179/// let v: Value = Bytes::from_static(b"data").into();
1180/// assert_eq!(v.as_bytes(), Some(b"data".as_slice()));
1181///
1182/// // From u64 (creates Counter64)
1183/// let v: Value = 10_000_000_000u64.into();
1184/// assert_eq!(v.as_u64(), Some(10_000_000_000));
1185///
1186/// // From Ipv4Addr
1187/// use std::net::Ipv4Addr;
1188/// let v: Value = Ipv4Addr::new(10, 0, 0, 1).into();
1189/// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(10, 0, 0, 1)));
1190///
1191/// // From [u8; 4] (creates IpAddress)
1192/// let v: Value = [192u8, 168, 1, 1].into();
1193/// assert!(matches!(v, Value::IpAddress([192, 168, 1, 1])));
1194/// ```
1195impl From<i32> for Value {
1196    fn from(v: i32) -> Self {
1197        Value::Integer(v)
1198    }
1199}
1200
1201impl From<&str> for Value {
1202    fn from(s: &str) -> Self {
1203        Value::OctetString(Bytes::copy_from_slice(s.as_bytes()))
1204    }
1205}
1206
1207impl From<String> for Value {
1208    fn from(s: String) -> Self {
1209        Value::OctetString(Bytes::from(s))
1210    }
1211}
1212
1213impl From<&[u8]> for Value {
1214    fn from(data: &[u8]) -> Self {
1215        Value::OctetString(Bytes::copy_from_slice(data))
1216    }
1217}
1218
1219impl From<Oid> for Value {
1220    fn from(oid: Oid) -> Self {
1221        Value::ObjectIdentifier(oid)
1222    }
1223}
1224
1225impl From<std::net::Ipv4Addr> for Value {
1226    fn from(addr: std::net::Ipv4Addr) -> Self {
1227        Value::IpAddress(addr.octets())
1228    }
1229}
1230
1231impl From<Bytes> for Value {
1232    fn from(data: Bytes) -> Self {
1233        Value::OctetString(data)
1234    }
1235}
1236
1237impl From<u64> for Value {
1238    fn from(v: u64) -> Self {
1239        Value::Counter64(v)
1240    }
1241}
1242
1243/// Converts a 4-byte array into [`Value::IpAddress`].
1244///
1245/// The bytes are interpreted as a big-endian IPv4 address, matching the `IpAddress`
1246/// SNMP type (RFC 2578 Section 7.1.5). Use this when you already have an address
1247/// in `[u8; 4]` form (e.g., from `Ipv4Addr::octets()`).
1248impl From<[u8; 4]> for Value {
1249    fn from(addr: [u8; 4]) -> Self {
1250        Value::IpAddress(addr)
1251    }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256    use super::*;
1257
1258    // AUDIT-003: Test that constructed OCTET STRING (0x24) is explicitly rejected.
1259    // Net-snmp documents but does not parse constructed form; we reject it.
1260    #[test]
1261    fn test_reject_constructed_octet_string() {
1262        // Constructed OCTET STRING has tag 0x24 (0x04 | 0x20)
1263        // Create a fake BER-encoded constructed OCTET STRING: 0x24 0x03 0x04 0x01 0x41
1264        // (constructed OCTET STRING containing primitive OCTET STRING "A")
1265        let data = bytes::Bytes::from_static(&[0x24, 0x03, 0x04, 0x01, 0x41]);
1266        let mut decoder = Decoder::new(data);
1267        let result = Value::decode(&mut decoder);
1268
1269        assert!(
1270            result.is_err(),
1271            "constructed OCTET STRING (0x24) should be rejected"
1272        );
1273        // Verify error is MalformedResponse (detailed error kind is logged via tracing)
1274        let err = result.unwrap_err();
1275        assert!(
1276            matches!(&*err, crate::Error::MalformedResponse { .. }),
1277            "expected MalformedResponse error, got: {err:?}"
1278        );
1279    }
1280
1281    #[test]
1282    fn test_primitive_octet_string_accepted() {
1283        // Primitive OCTET STRING (0x04) should be accepted
1284        let data = bytes::Bytes::from_static(&[0x04, 0x03, 0x41, 0x42, 0x43]); // "ABC"
1285        let mut decoder = Decoder::new(data);
1286        let result = Value::decode(&mut decoder);
1287
1288        assert!(result.is_ok(), "primitive OCTET STRING should be accepted");
1289        let value = result.unwrap();
1290        assert_eq!(value.as_bytes(), Some(&b"ABC"[..]));
1291    }
1292
1293    // ========================================================================
1294    // Value Type Encoding/Decoding Tests
1295    // ========================================================================
1296
1297    fn roundtrip(value: &Value) -> Value {
1298        let mut buf = EncodeBuf::new();
1299        value.encode(&mut buf);
1300        let data = buf.finish();
1301        let mut decoder = Decoder::new(data);
1302        Value::decode(&mut decoder).unwrap()
1303    }
1304
1305    #[test]
1306    fn test_integer_positive() {
1307        let value = Value::Integer(42);
1308        assert_eq!(roundtrip(&value), value);
1309    }
1310
1311    #[test]
1312    fn test_integer_negative() {
1313        let value = Value::Integer(-42);
1314        assert_eq!(roundtrip(&value), value);
1315    }
1316
1317    #[test]
1318    fn test_integer_zero() {
1319        let value = Value::Integer(0);
1320        assert_eq!(roundtrip(&value), value);
1321    }
1322
1323    #[test]
1324    fn test_integer_min() {
1325        let value = Value::Integer(i32::MIN);
1326        assert_eq!(roundtrip(&value), value);
1327    }
1328
1329    #[test]
1330    fn test_integer_max() {
1331        let value = Value::Integer(i32::MAX);
1332        assert_eq!(roundtrip(&value), value);
1333    }
1334
1335    #[test]
1336    fn test_octet_string_ascii() {
1337        let value = Value::OctetString(Bytes::from_static(b"hello world"));
1338        assert_eq!(roundtrip(&value), value);
1339    }
1340
1341    #[test]
1342    fn test_octet_string_binary() {
1343        let value = Value::OctetString(Bytes::from_static(&[0x00, 0xFF, 0x80, 0x7F]));
1344        assert_eq!(roundtrip(&value), value);
1345    }
1346
1347    #[test]
1348    fn test_octet_string_empty() {
1349        let value = Value::OctetString(Bytes::new());
1350        assert_eq!(roundtrip(&value), value);
1351    }
1352
1353    #[test]
1354    fn test_null() {
1355        let value = Value::Null;
1356        assert_eq!(roundtrip(&value), value);
1357    }
1358
1359    #[test]
1360    fn test_object_identifier() {
1361        let value = Value::ObjectIdentifier(crate::oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
1362        assert_eq!(roundtrip(&value), value);
1363    }
1364
1365    #[test]
1366    fn test_ip_address() {
1367        let value = Value::IpAddress([192, 168, 1, 1]);
1368        assert_eq!(roundtrip(&value), value);
1369    }
1370
1371    #[test]
1372    fn test_ip_address_zero() {
1373        let value = Value::IpAddress([0, 0, 0, 0]);
1374        assert_eq!(roundtrip(&value), value);
1375    }
1376
1377    #[test]
1378    fn test_ip_address_broadcast() {
1379        let value = Value::IpAddress([255, 255, 255, 255]);
1380        assert_eq!(roundtrip(&value), value);
1381    }
1382
1383    #[test]
1384    fn test_counter32() {
1385        let value = Value::Counter32(999_999);
1386        assert_eq!(roundtrip(&value), value);
1387    }
1388
1389    #[test]
1390    fn test_counter32_zero() {
1391        let value = Value::Counter32(0);
1392        assert_eq!(roundtrip(&value), value);
1393    }
1394
1395    #[test]
1396    fn test_counter32_max() {
1397        let value = Value::Counter32(u32::MAX);
1398        assert_eq!(roundtrip(&value), value);
1399    }
1400
1401    #[test]
1402    fn test_gauge32() {
1403        let value = Value::Gauge32(1_000_000_000);
1404        assert_eq!(roundtrip(&value), value);
1405    }
1406
1407    #[test]
1408    fn test_gauge32_max() {
1409        let value = Value::Gauge32(u32::MAX);
1410        assert_eq!(roundtrip(&value), value);
1411    }
1412
1413    #[test]
1414    fn test_timeticks() {
1415        let value = Value::TimeTicks(123_456);
1416        assert_eq!(roundtrip(&value), value);
1417    }
1418
1419    #[test]
1420    fn test_timeticks_max() {
1421        let value = Value::TimeTicks(u32::MAX);
1422        assert_eq!(roundtrip(&value), value);
1423    }
1424
1425    #[test]
1426    fn test_opaque() {
1427        let value = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
1428        assert_eq!(roundtrip(&value), value);
1429    }
1430
1431    #[test]
1432    fn test_opaque_empty() {
1433        let value = Value::Opaque(Bytes::new());
1434        assert_eq!(roundtrip(&value), value);
1435    }
1436
1437    #[test]
1438    fn test_counter64() {
1439        let value = Value::Counter64(123_456_789_012_345);
1440        assert_eq!(roundtrip(&value), value);
1441    }
1442
1443    #[test]
1444    fn test_counter64_zero() {
1445        let value = Value::Counter64(0);
1446        assert_eq!(roundtrip(&value), value);
1447    }
1448
1449    #[test]
1450    fn test_counter64_max() {
1451        let value = Value::Counter64(u64::MAX);
1452        assert_eq!(roundtrip(&value), value);
1453    }
1454
1455    #[test]
1456    fn test_no_such_object() {
1457        let value = Value::NoSuchObject;
1458        assert_eq!(roundtrip(&value), value);
1459    }
1460
1461    #[test]
1462    fn test_no_such_instance() {
1463        let value = Value::NoSuchInstance;
1464        assert_eq!(roundtrip(&value), value);
1465    }
1466
1467    #[test]
1468    fn test_end_of_mib_view() {
1469        let value = Value::EndOfMibView;
1470        assert_eq!(roundtrip(&value), value);
1471    }
1472
1473    #[test]
1474    fn test_unknown_tag_preserved() {
1475        // Tag 0x48 is application class but not a standard SNMP type
1476        let data = Bytes::from_static(&[0x48, 0x03, 0x01, 0x02, 0x03]);
1477        let mut decoder = Decoder::new(data);
1478        let value = Value::decode(&mut decoder).unwrap();
1479
1480        match value {
1481            Value::Unknown { tag, ref data } => {
1482                assert_eq!(tag, 0x48);
1483                assert_eq!(data.as_ref(), &[0x01, 0x02, 0x03]);
1484            }
1485            _ => panic!("expected Unknown variant"),
1486        }
1487
1488        // Roundtrip should preserve
1489        assert_eq!(roundtrip(&value), value);
1490    }
1491
1492    #[test]
1493    fn test_nsap_decodes_as_octet_string() {
1494        // Historic NSAP type (0x45) decodes as OctetString
1495        let data = Bytes::from_static(&[0x45, 0x03, 0x01, 0x02, 0x03]);
1496        let mut decoder = Decoder::new(data);
1497        let value = Value::decode(&mut decoder).unwrap();
1498        assert_eq!(
1499            value,
1500            Value::OctetString(Bytes::from_static(&[0x01, 0x02, 0x03]))
1501        );
1502    }
1503
1504    #[test]
1505    fn test_uinteger32_decodes_as_gauge32() {
1506        // Historic UInteger32 type (0x47) decodes as Gauge32
1507        let data = Bytes::from_static(&[0x47, 0x01, 0x2a]);
1508        let mut decoder = Decoder::new(data);
1509        let value = Value::decode(&mut decoder).unwrap();
1510        assert_eq!(value, Value::Gauge32(42));
1511    }
1512
1513    // ========================================================================
1514    // Accessor Method Tests
1515    // ========================================================================
1516
1517    #[test]
1518    fn test_as_i32() {
1519        assert_eq!(Value::Integer(42).as_i32(), Some(42));
1520        assert_eq!(Value::Integer(-42).as_i32(), Some(-42));
1521        assert_eq!(Value::Counter32(100).as_i32(), None);
1522        assert_eq!(Value::Null.as_i32(), None);
1523    }
1524
1525    #[test]
1526    fn test_as_u32() {
1527        assert_eq!(Value::Counter32(100).as_u32(), Some(100));
1528        assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
1529        assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
1530        assert_eq!(Value::Integer(50).as_u32(), Some(50));
1531        assert_eq!(Value::Integer(-1).as_u32(), None);
1532        assert_eq!(Value::Counter64(100).as_u32(), None);
1533    }
1534
1535    #[test]
1536    fn test_as_u64() {
1537        assert_eq!(Value::Counter64(100).as_u64(), Some(100));
1538        assert_eq!(Value::Counter32(100).as_u64(), Some(100));
1539        assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
1540        assert_eq!(Value::TimeTicks(300).as_u64(), Some(300));
1541        assert_eq!(Value::Integer(50).as_u64(), Some(50));
1542        assert_eq!(Value::Integer(-1).as_u64(), None);
1543    }
1544
1545    #[test]
1546    fn test_as_bytes() {
1547        let s = Value::OctetString(Bytes::from_static(b"test"));
1548        assert_eq!(s.as_bytes(), Some(b"test".as_slice()));
1549
1550        let o = Value::Opaque(Bytes::from_static(b"data"));
1551        assert_eq!(o.as_bytes(), Some(b"data".as_slice()));
1552
1553        assert_eq!(Value::Integer(1).as_bytes(), None);
1554    }
1555
1556    #[test]
1557    fn test_as_str() {
1558        let s = Value::OctetString(Bytes::from_static(b"hello"));
1559        assert_eq!(s.as_str(), Some("hello"));
1560
1561        // Invalid UTF-8 returns None
1562        let invalid = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
1563        assert_eq!(invalid.as_str(), None);
1564
1565        assert_eq!(Value::Integer(1).as_str(), None);
1566    }
1567
1568    #[test]
1569    fn test_as_oid() {
1570        let oid = crate::oid!(1, 3, 6, 1);
1571        let v = Value::ObjectIdentifier(oid.clone());
1572        assert_eq!(v.as_oid(), Some(&oid));
1573
1574        assert_eq!(Value::Integer(1).as_oid(), None);
1575    }
1576
1577    #[test]
1578    fn test_as_ip() {
1579        let v = Value::IpAddress([192, 168, 1, 1]);
1580        assert_eq!(v.as_ip(), Some(std::net::Ipv4Addr::new(192, 168, 1, 1)));
1581
1582        assert_eq!(Value::Integer(1).as_ip(), None);
1583    }
1584
1585    // ========================================================================
1586    // is_exception() Tests
1587    // ========================================================================
1588
1589    #[test]
1590    fn test_is_exception() {
1591        assert!(Value::NoSuchObject.is_exception());
1592        assert!(Value::NoSuchInstance.is_exception());
1593        assert!(Value::EndOfMibView.is_exception());
1594
1595        assert!(!Value::Integer(1).is_exception());
1596        assert!(!Value::Null.is_exception());
1597        assert!(!Value::OctetString(Bytes::new()).is_exception());
1598    }
1599
1600    // ========================================================================
1601    // Display Trait Tests
1602    // ========================================================================
1603
1604    #[test]
1605    fn test_display_integer() {
1606        assert_eq!(format!("{}", Value::Integer(42)), "42");
1607        assert_eq!(format!("{}", Value::Integer(-42)), "-42");
1608    }
1609
1610    #[test]
1611    fn test_display_octet_string_utf8() {
1612        let v = Value::OctetString(Bytes::from_static(b"hello"));
1613        assert_eq!(format!("{v}"), "hello");
1614    }
1615
1616    #[test]
1617    fn test_display_octet_string_binary() {
1618        // Use bytes that are not valid UTF-8 (0xFF is never valid in UTF-8)
1619        let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
1620        assert_eq!(format!("{v}"), "0xfffe");
1621    }
1622
1623    #[test]
1624    fn test_display_null() {
1625        assert_eq!(format!("{}", Value::Null), "NULL");
1626    }
1627
1628    #[test]
1629    fn test_display_ip_address() {
1630        let v = Value::IpAddress([192, 168, 1, 1]);
1631        assert_eq!(format!("{v}"), "192.168.1.1");
1632    }
1633
1634    #[test]
1635    fn test_display_counter32() {
1636        assert_eq!(format!("{}", Value::Counter32(999)), "999");
1637    }
1638
1639    #[test]
1640    fn test_display_gauge32() {
1641        assert_eq!(format!("{}", Value::Gauge32(1000)), "1000");
1642    }
1643
1644    #[test]
1645    fn test_display_timeticks() {
1646        // 123456 hundredths = 1234.56 seconds = 20m 34.56s
1647        let v = Value::TimeTicks(123_456);
1648        assert_eq!(format!("{v}"), "00:20:34.56");
1649    }
1650
1651    #[test]
1652    fn test_display_opaque() {
1653        let v = Value::Opaque(Bytes::from_static(&[0xBE, 0xEF]));
1654        assert_eq!(format!("{v}"), "Opaque(0xbeef)");
1655    }
1656
1657    #[test]
1658    fn test_display_counter64() {
1659        assert_eq!(format!("{}", Value::Counter64(1234_5678)), "12345678");
1660    }
1661
1662    #[test]
1663    fn test_display_exceptions() {
1664        assert_eq!(format!("{}", Value::NoSuchObject), "noSuchObject");
1665        assert_eq!(format!("{}", Value::NoSuchInstance), "noSuchInstance");
1666        assert_eq!(format!("{}", Value::EndOfMibView), "endOfMibView");
1667    }
1668
1669    #[test]
1670    fn test_display_unknown() {
1671        let v = Value::Unknown {
1672            tag: 0x99,
1673            data: Bytes::from_static(&[0x01, 0x02]),
1674        };
1675        assert_eq!(format!("{v}"), "Unknown(tag=0x99, data=0x0102)");
1676    }
1677
1678    // ========================================================================
1679    // From Conversion Tests
1680    // ========================================================================
1681
1682    #[test]
1683    fn test_from_i32() {
1684        let v: Value = 42i32.into();
1685        assert_eq!(v, Value::Integer(42));
1686    }
1687
1688    #[test]
1689    fn test_from_str() {
1690        let v: Value = "hello".into();
1691        assert_eq!(v.as_str(), Some("hello"));
1692    }
1693
1694    #[test]
1695    fn test_from_string() {
1696        let v: Value = String::from("hello").into();
1697        assert_eq!(v.as_str(), Some("hello"));
1698    }
1699
1700    #[test]
1701    fn test_from_bytes_slice() {
1702        let v: Value = (&[1u8, 2, 3][..]).into();
1703        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
1704    }
1705
1706    #[test]
1707    fn test_from_oid() {
1708        let oid = crate::oid!(1, 3, 6, 1);
1709        let v: Value = oid.clone().into();
1710        assert_eq!(v.as_oid(), Some(&oid));
1711    }
1712
1713    #[test]
1714    fn test_from_ipv4addr() {
1715        let addr = std::net::Ipv4Addr::new(10, 0, 0, 1);
1716        let v: Value = addr.into();
1717        assert_eq!(v, Value::IpAddress([10, 0, 0, 1]));
1718    }
1719
1720    #[test]
1721    fn test_from_bytes() {
1722        let data = Bytes::from_static(b"hello");
1723        let v: Value = data.into();
1724        assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
1725    }
1726
1727    #[test]
1728    fn test_from_u64() {
1729        let v: Value = 12_345_678_901_234_u64.into();
1730        assert_eq!(v, Value::Counter64(12_345_678_901_234));
1731    }
1732
1733    #[test]
1734    fn test_from_ip_array() {
1735        let v: Value = [192u8, 168, 1, 1].into();
1736        assert_eq!(v, Value::IpAddress([192, 168, 1, 1]));
1737    }
1738
1739    // ========================================================================
1740    // Eq and Hash Tests
1741    // ========================================================================
1742
1743    #[test]
1744    fn test_value_eq_and_hash() {
1745        use std::collections::HashSet;
1746
1747        let mut set = HashSet::new();
1748        set.insert(Value::Integer(42));
1749        set.insert(Value::Integer(42)); // Duplicate
1750        set.insert(Value::Integer(100));
1751
1752        assert_eq!(set.len(), 2);
1753        assert!(set.contains(&Value::Integer(42)));
1754        assert!(set.contains(&Value::Integer(100)));
1755    }
1756
1757    // ========================================================================
1758    // Decode Error Tests
1759    // ========================================================================
1760
1761    #[test]
1762    fn test_decode_invalid_null_length() {
1763        // NULL must have length 0
1764        let data = Bytes::from_static(&[0x05, 0x01, 0x00]); // NULL with length 1
1765        let mut decoder = Decoder::new(data);
1766        let result = Value::decode(&mut decoder);
1767        assert!(result.is_err());
1768    }
1769
1770    #[test]
1771    fn test_decode_invalid_ip_address_length() {
1772        // IpAddress must have length 4
1773        let data = Bytes::from_static(&[0x40, 0x03, 0x01, 0x02, 0x03]); // Only 3 bytes
1774        let mut decoder = Decoder::new(data);
1775        let result = Value::decode(&mut decoder);
1776        assert!(result.is_err());
1777    }
1778
1779    #[test]
1780    fn test_decode_exception_with_content_accepted() {
1781        // Per implementation, exceptions with non-zero length have content skipped
1782        let data = Bytes::from_static(&[0x80, 0x01, 0xFF]); // NoSuchObject with 1 byte
1783        let mut decoder = Decoder::new(data);
1784        let result = Value::decode(&mut decoder);
1785        assert!(result.is_ok());
1786        assert_eq!(result.unwrap(), Value::NoSuchObject);
1787    }
1788
1789    // ========================================================================
1790    // Numeric Extraction Method Tests
1791    // ========================================================================
1792
1793    #[test]
1794    fn test_as_f64() {
1795        assert_eq!(Value::Integer(42).as_f64(), Some(42.0));
1796        assert_eq!(Value::Integer(-42).as_f64(), Some(-42.0));
1797        assert_eq!(Value::Counter32(1000).as_f64(), Some(1000.0));
1798        assert_eq!(Value::Gauge32(2000).as_f64(), Some(2000.0));
1799        assert_eq!(Value::TimeTicks(3000).as_f64(), Some(3000.0));
1800        assert_eq!(
1801            Value::Counter64(10_000_000_000).as_f64(),
1802            Some(10_000_000_000.0)
1803        );
1804        assert_eq!(Value::Null.as_f64(), None);
1805        assert_eq!(
1806            Value::OctetString(Bytes::from_static(b"test")).as_f64(),
1807            None
1808        );
1809    }
1810
1811    #[test]
1812    fn test_as_f64_wrapped() {
1813        // Small values behave same as as_f64()
1814        assert_eq!(Value::Counter64(1000).as_f64_wrapped(), Some(1000.0));
1815        assert_eq!(Value::Counter32(1000).as_f64_wrapped(), Some(1000.0));
1816        assert_eq!(Value::Integer(42).as_f64_wrapped(), Some(42.0));
1817
1818        // Large Counter64 wraps at 2^53
1819        let mantissa_limit = 1u64 << 53;
1820        assert_eq!(Value::Counter64(mantissa_limit).as_f64_wrapped(), Some(0.0));
1821        assert_eq!(
1822            Value::Counter64(mantissa_limit + 1).as_f64_wrapped(),
1823            Some(1.0)
1824        );
1825    }
1826
1827    #[test]
1828    fn test_as_decimal() {
1829        assert_eq!(Value::Integer(2350).as_decimal(2), Some(23.50));
1830        assert_eq!(Value::Integer(9999).as_decimal(2), Some(99.99));
1831        assert_eq!(Value::Integer(12500).as_decimal(3), Some(12.5));
1832        assert_eq!(Value::Integer(-500).as_decimal(2), Some(-5.0));
1833        assert_eq!(Value::Counter32(1000).as_decimal(1), Some(100.0));
1834        assert_eq!(Value::Null.as_decimal(2), None);
1835    }
1836
1837    #[test]
1838    fn test_as_duration() {
1839        use std::time::Duration;
1840
1841        // 100 ticks = 1 second
1842        assert_eq!(
1843            Value::TimeTicks(100).as_duration(),
1844            Some(Duration::from_secs(1))
1845        );
1846        // 360000 ticks = 3600 seconds = 1 hour
1847        assert_eq!(
1848            Value::TimeTicks(360_000).as_duration(),
1849            Some(Duration::from_secs(3600))
1850        );
1851        // 1 tick = 10 milliseconds
1852        assert_eq!(
1853            Value::TimeTicks(1).as_duration(),
1854            Some(Duration::from_millis(10))
1855        );
1856
1857        // Non-TimeTicks return None
1858        assert_eq!(Value::Integer(100).as_duration(), None);
1859        assert_eq!(Value::Counter32(100).as_duration(), None);
1860    }
1861
1862    // ========================================================================
1863    // Opaque Sub-type Extraction Tests
1864    // ========================================================================
1865
1866    #[test]
1867    fn test_as_opaque_float() {
1868        // Opaque-encoded float for pi ≈ 3.14159
1869        // 0x40490fdb is IEEE 754 single-precision for ~3.14159
1870        let data = Bytes::from_static(&[0x9f, 0x78, 0x04, 0x40, 0x49, 0x0f, 0xdb]);
1871        let value = Value::Opaque(data);
1872        let pi = value.as_opaque_float().unwrap();
1873        assert!((pi - std::f32::consts::PI).abs() < 0.0001);
1874
1875        // Non-Opaque returns None
1876        assert_eq!(Value::Integer(42).as_opaque_float(), None);
1877
1878        // Wrong subtype returns None
1879        let wrong_type = Bytes::from_static(&[0x9f, 0x79, 0x04, 0x40, 0x49, 0x0f, 0xdb]);
1880        assert_eq!(Value::Opaque(wrong_type).as_opaque_float(), None);
1881
1882        // Too short returns None
1883        let short = Bytes::from_static(&[0x9f, 0x78, 0x04, 0x40, 0x49]);
1884        assert_eq!(Value::Opaque(short).as_opaque_float(), None);
1885    }
1886
1887    #[test]
1888    fn test_as_opaque_double() {
1889        // Opaque-encoded double for pi
1890        // 0x400921fb54442d18 is IEEE 754 double-precision for pi
1891        let data = Bytes::from_static(&[
1892            0x9f, 0x79, 0x08, 0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2d, 0x18,
1893        ]);
1894        let value = Value::Opaque(data);
1895        let pi = value.as_opaque_double().unwrap();
1896        assert!((pi - std::f64::consts::PI).abs() < 1e-10);
1897
1898        // Non-Opaque returns None
1899        assert_eq!(Value::Integer(42).as_opaque_double(), None);
1900    }
1901
1902    #[test]
1903    fn test_as_opaque_counter64() {
1904        // 8-byte Counter64
1905        let data = Bytes::from_static(&[
1906            0x9f, 0x76, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
1907        ]);
1908        let value = Value::Opaque(data);
1909        assert_eq!(value.as_opaque_counter64(), Some(0x0123_4567_89AB_CDEF));
1910
1911        // Shorter encoding (e.g., small value)
1912        let small = Bytes::from_static(&[0x9f, 0x76, 0x01, 0x42]);
1913        assert_eq!(Value::Opaque(small).as_opaque_counter64(), Some(0x42));
1914
1915        // Zero
1916        let zero = Bytes::from_static(&[0x9f, 0x76, 0x01, 0x00]);
1917        assert_eq!(Value::Opaque(zero).as_opaque_counter64(), Some(0));
1918    }
1919
1920    #[test]
1921    fn test_as_opaque_i64() {
1922        // Positive value
1923        let positive = Bytes::from_static(&[0x9f, 0x7a, 0x02, 0x01, 0x00]);
1924        assert_eq!(Value::Opaque(positive).as_opaque_i64(), Some(256));
1925
1926        // Negative value (-1)
1927        let minus_one = Bytes::from_static(&[0x9f, 0x7a, 0x01, 0xFF]);
1928        assert_eq!(Value::Opaque(minus_one).as_opaque_i64(), Some(-1));
1929
1930        // Full 8-byte negative
1931        let full_neg = Bytes::from_static(&[
1932            0x9f, 0x7a, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
1933        ]);
1934        assert_eq!(Value::Opaque(full_neg).as_opaque_i64(), Some(-1));
1935
1936        // i64::MIN
1937        let min = Bytes::from_static(&[
1938            0x9f, 0x7a, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1939        ]);
1940        assert_eq!(Value::Opaque(min).as_opaque_i64(), Some(i64::MIN));
1941    }
1942
1943    #[test]
1944    fn test_as_opaque_u64() {
1945        // 8-byte U64
1946        let data = Bytes::from_static(&[
1947            0x9f, 0x7b, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
1948        ]);
1949        let value = Value::Opaque(data);
1950        assert_eq!(value.as_opaque_u64(), Some(0x0123_4567_89AB_CDEF));
1951
1952        // u64::MAX
1953        let max = Bytes::from_static(&[
1954            0x9f, 0x7b, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
1955        ]);
1956        assert_eq!(Value::Opaque(max).as_opaque_u64(), Some(u64::MAX));
1957    }
1958
1959    #[test]
1960    fn test_format_with_hint_integer() {
1961        // Decimal places
1962        assert_eq!(
1963            Value::Integer(2350).format_with_hint("d-2"),
1964            Some("23.50".into())
1965        );
1966        assert_eq!(
1967            Value::Integer(-500).format_with_hint("d-2"),
1968            Some("-5.00".into())
1969        );
1970
1971        // Basic formats
1972        assert_eq!(Value::Integer(255).format_with_hint("x"), Some("ff".into()));
1973        assert_eq!(Value::Integer(8).format_with_hint("o"), Some("10".into()));
1974        assert_eq!(Value::Integer(5).format_with_hint("b"), Some("101".into()));
1975        assert_eq!(Value::Integer(42).format_with_hint("d"), Some("42".into()));
1976
1977        // Invalid hint
1978        assert_eq!(Value::Integer(42).format_with_hint("invalid"), None);
1979
1980        // Counter32 etc. still return None (only Integer supported for INTEGER hints)
1981        assert_eq!(Value::Counter32(42).format_with_hint("d-2"), None);
1982    }
1983
1984    #[test]
1985    fn test_as_truth_value() {
1986        // Valid TruthValue
1987        assert_eq!(Value::Integer(1).as_truth_value(), Some(true));
1988        assert_eq!(Value::Integer(2).as_truth_value(), Some(false));
1989
1990        // Invalid integers
1991        assert_eq!(Value::Integer(0).as_truth_value(), None);
1992        assert_eq!(Value::Integer(3).as_truth_value(), None);
1993        assert_eq!(Value::Integer(-1).as_truth_value(), None);
1994
1995        // Non-Integer types
1996        assert_eq!(Value::Null.as_truth_value(), None);
1997        assert_eq!(Value::Counter32(1).as_truth_value(), None);
1998        assert_eq!(Value::Gauge32(1).as_truth_value(), None);
1999    }
2000
2001    // ========================================================================
2002    // RowStatus Tests
2003    // ========================================================================
2004
2005    #[test]
2006    fn test_row_status_from_i32() {
2007        assert_eq!(RowStatus::from_i32(1), Some(RowStatus::Active));
2008        assert_eq!(RowStatus::from_i32(2), Some(RowStatus::NotInService));
2009        assert_eq!(RowStatus::from_i32(3), Some(RowStatus::NotReady));
2010        assert_eq!(RowStatus::from_i32(4), Some(RowStatus::CreateAndGo));
2011        assert_eq!(RowStatus::from_i32(5), Some(RowStatus::CreateAndWait));
2012        assert_eq!(RowStatus::from_i32(6), Some(RowStatus::Destroy));
2013
2014        // Invalid values
2015        assert_eq!(RowStatus::from_i32(0), None);
2016        assert_eq!(RowStatus::from_i32(7), None);
2017        assert_eq!(RowStatus::from_i32(-1), None);
2018    }
2019
2020    #[test]
2021    fn test_row_status_try_from() {
2022        assert_eq!(RowStatus::try_from(1), Ok(RowStatus::Active));
2023        assert_eq!(RowStatus::try_from(6), Ok(RowStatus::Destroy));
2024        assert_eq!(RowStatus::try_from(0), Err(0));
2025        assert_eq!(RowStatus::try_from(7), Err(7));
2026        assert_eq!(RowStatus::try_from(-1), Err(-1));
2027    }
2028
2029    #[test]
2030    fn test_row_status_into_value() {
2031        let v: Value = RowStatus::Active.into();
2032        assert_eq!(v, Value::Integer(1));
2033
2034        let v: Value = RowStatus::Destroy.into();
2035        assert_eq!(v, Value::Integer(6));
2036    }
2037
2038    #[test]
2039    fn test_row_status_display() {
2040        assert_eq!(format!("{}", RowStatus::Active), "active");
2041        assert_eq!(format!("{}", RowStatus::NotInService), "notInService");
2042        assert_eq!(format!("{}", RowStatus::NotReady), "notReady");
2043        assert_eq!(format!("{}", RowStatus::CreateAndGo), "createAndGo");
2044        assert_eq!(format!("{}", RowStatus::CreateAndWait), "createAndWait");
2045        assert_eq!(format!("{}", RowStatus::Destroy), "destroy");
2046    }
2047
2048    #[test]
2049    fn test_as_row_status() {
2050        // Valid RowStatus values
2051        assert_eq!(Value::Integer(1).as_row_status(), Some(RowStatus::Active));
2052        assert_eq!(Value::Integer(6).as_row_status(), Some(RowStatus::Destroy));
2053
2054        // Invalid integers
2055        assert_eq!(Value::Integer(0).as_row_status(), None);
2056        assert_eq!(Value::Integer(7).as_row_status(), None);
2057
2058        // Non-Integer types
2059        assert_eq!(Value::Null.as_row_status(), None);
2060        assert_eq!(Value::Counter32(1).as_row_status(), None);
2061    }
2062
2063    // ========================================================================
2064    // StorageType Tests
2065    // ========================================================================
2066
2067    #[test]
2068    fn test_storage_type_from_i32() {
2069        assert_eq!(StorageType::from_i32(1), Some(StorageType::Other));
2070        assert_eq!(StorageType::from_i32(2), Some(StorageType::Volatile));
2071        assert_eq!(StorageType::from_i32(3), Some(StorageType::NonVolatile));
2072        assert_eq!(StorageType::from_i32(4), Some(StorageType::Permanent));
2073        assert_eq!(StorageType::from_i32(5), Some(StorageType::ReadOnly));
2074
2075        // Invalid values
2076        assert_eq!(StorageType::from_i32(0), None);
2077        assert_eq!(StorageType::from_i32(6), None);
2078        assert_eq!(StorageType::from_i32(-1), None);
2079    }
2080
2081    #[test]
2082    fn test_storage_type_try_from() {
2083        assert_eq!(StorageType::try_from(1), Ok(StorageType::Other));
2084        assert_eq!(StorageType::try_from(5), Ok(StorageType::ReadOnly));
2085        assert_eq!(StorageType::try_from(0), Err(0));
2086        assert_eq!(StorageType::try_from(6), Err(6));
2087        assert_eq!(StorageType::try_from(-1), Err(-1));
2088    }
2089
2090    #[test]
2091    fn test_storage_type_into_value() {
2092        let v: Value = StorageType::Volatile.into();
2093        assert_eq!(v, Value::Integer(2));
2094
2095        let v: Value = StorageType::NonVolatile.into();
2096        assert_eq!(v, Value::Integer(3));
2097    }
2098
2099    #[test]
2100    fn test_storage_type_display() {
2101        assert_eq!(format!("{}", StorageType::Other), "other");
2102        assert_eq!(format!("{}", StorageType::Volatile), "volatile");
2103        assert_eq!(format!("{}", StorageType::NonVolatile), "nonVolatile");
2104        assert_eq!(format!("{}", StorageType::Permanent), "permanent");
2105        assert_eq!(format!("{}", StorageType::ReadOnly), "readOnly");
2106    }
2107
2108    #[test]
2109    fn test_as_storage_type() {
2110        // Valid StorageType values
2111        assert_eq!(
2112            Value::Integer(2).as_storage_type(),
2113            Some(StorageType::Volatile)
2114        );
2115        assert_eq!(
2116            Value::Integer(3).as_storage_type(),
2117            Some(StorageType::NonVolatile)
2118        );
2119
2120        // Invalid integers
2121        assert_eq!(Value::Integer(0).as_storage_type(), None);
2122        assert_eq!(Value::Integer(6).as_storage_type(), None);
2123
2124        // Non-Integer types
2125        assert_eq!(Value::Null.as_storage_type(), None);
2126        assert_eq!(Value::Counter32(1).as_storage_type(), None);
2127    }
2128}