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    pub fn from_i32(value: i32) -> Option<Self> {
78        Self::try_from(value).ok()
79    }
80}
81
82impl From<RowStatus> for Value {
83    fn from(status: RowStatus) -> Self {
84        Value::Integer(status as i32)
85    }
86}
87
88impl std::fmt::Display for RowStatus {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::Active => write!(f, "active"),
92            Self::NotInService => write!(f, "notInService"),
93            Self::NotReady => write!(f, "notReady"),
94            Self::CreateAndGo => write!(f, "createAndGo"),
95            Self::CreateAndWait => write!(f, "createAndWait"),
96            Self::Destroy => write!(f, "destroy"),
97        }
98    }
99}
100
101/// RFC 2579 StorageType textual convention.
102///
103/// Describes how an SNMP row's data is stored and persisted.
104///
105/// # Persistence Levels
106///
107/// | Type | Survives Reboot | Writable |
108/// |------|-----------------|----------|
109/// | other | undefined | varies |
110/// | volatile | no | yes |
111/// | nonVolatile | yes | yes |
112/// | permanent | yes | limited |
113/// | readOnly | yes | no |
114///
115/// # Example
116///
117/// ```
118/// use async_snmp::{Value, StorageType};
119///
120/// // Reading a StorageType column
121/// let value = Value::Integer(3);
122/// assert_eq!(value.as_storage_type(), Some(StorageType::NonVolatile));
123///
124/// // Creating a value to write
125/// let storage: Value = StorageType::Volatile.into();
126/// assert_eq!(storage, Value::Integer(2));
127/// ```
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129pub enum StorageType {
130    /// Implementation-specific storage.
131    Other = 1,
132    /// Lost on reboot; can be modified.
133    Volatile = 2,
134    /// Survives reboot; can be modified.
135    NonVolatile = 3,
136    /// Survives reboot; limited modifications allowed.
137    Permanent = 4,
138    /// Survives reboot; cannot be modified.
139    ReadOnly = 5,
140}
141
142impl TryFrom<i32> for StorageType {
143    type Error = i32;
144
145    fn try_from(value: i32) -> std::result::Result<Self, i32> {
146        match value {
147            1 => Ok(Self::Other),
148            2 => Ok(Self::Volatile),
149            3 => Ok(Self::NonVolatile),
150            4 => Ok(Self::Permanent),
151            5 => Ok(Self::ReadOnly),
152            _ => Err(value),
153        }
154    }
155}
156
157impl StorageType {
158    /// Create from raw SNMP integer value.
159    ///
160    /// Returns `None` for values outside the valid range (1-5).
161    pub fn from_i32(value: i32) -> Option<Self> {
162        Self::try_from(value).ok()
163    }
164}
165
166impl From<StorageType> for Value {
167    fn from(storage: StorageType) -> Self {
168        Value::Integer(storage as i32)
169    }
170}
171
172impl std::fmt::Display for StorageType {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            Self::Other => write!(f, "other"),
176            Self::Volatile => write!(f, "volatile"),
177            Self::NonVolatile => write!(f, "nonVolatile"),
178            Self::Permanent => write!(f, "permanent"),
179            Self::ReadOnly => write!(f, "readOnly"),
180        }
181    }
182}
183
184/// SNMP value.
185///
186/// Represents all SNMP data types including SMIv2 types and exception values.
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188#[non_exhaustive]
189pub enum Value {
190    /// INTEGER (ASN.1 primitive, signed 32-bit)
191    Integer(i32),
192
193    /// OCTET STRING (arbitrary bytes).
194    ///
195    /// Per RFC 2578 (SMIv2), OCTET STRING values have a maximum size of 65535 octets.
196    /// This limit is **not enforced** during decoding to maintain permissive parsing
197    /// behavior. Applications that require strict compliance should validate size
198    /// after decoding.
199    OctetString(Bytes),
200
201    /// NULL
202    Null,
203
204    /// OBJECT IDENTIFIER
205    ObjectIdentifier(Oid),
206
207    /// IpAddress (4 bytes, big-endian)
208    IpAddress([u8; 4]),
209
210    /// Counter32 (unsigned 32-bit, wrapping)
211    Counter32(u32),
212
213    /// Gauge32 / Unsigned32 (unsigned 32-bit, non-wrapping)
214    Gauge32(u32),
215
216    /// TimeTicks (hundredths of seconds since epoch)
217    TimeTicks(u32),
218
219    /// Opaque (legacy, arbitrary bytes)
220    Opaque(Bytes),
221
222    /// Counter64 (unsigned 64-bit, wrapping).
223    ///
224    /// **SNMPv2c/v3 only.** Counter64 was introduced in SNMPv2 (RFC 2578) and is
225    /// not supported in SNMPv1. When sending Counter64 values to an SNMPv1 agent,
226    /// the value will be silently ignored or cause an error depending on the agent
227    /// implementation.
228    ///
229    /// If your application needs to support SNMPv1, avoid using Counter64 or
230    /// fall back to Counter32 (with potential overflow for high-bandwidth counters).
231    Counter64(u64),
232
233    /// noSuchObject exception - the requested OID exists in the MIB but has no value.
234    ///
235    /// This exception indicates that the agent recognizes the OID (it's a valid
236    /// MIB object), but there is no instance available. This commonly occurs when
237    /// requesting a table column OID without an index.
238    ///
239    /// # Example
240    ///
241    /// ```
242    /// use async_snmp::Value;
243    ///
244    /// let response = Value::NoSuchObject;
245    /// assert!(response.is_exception());
246    ///
247    /// // When handling responses, check for exceptions:
248    /// match response {
249    ///     Value::NoSuchObject => println!("OID exists but has no value"),
250    ///     _ => {}
251    /// }
252    /// ```
253    NoSuchObject,
254
255    /// noSuchInstance exception - the specific instance does not exist.
256    ///
257    /// This exception indicates that while the MIB object exists, the specific
258    /// instance (index) requested does not. This commonly occurs when querying
259    /// a table row that doesn't exist.
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use async_snmp::Value;
265    ///
266    /// let response = Value::NoSuchInstance;
267    /// assert!(response.is_exception());
268    /// ```
269    NoSuchInstance,
270
271    /// endOfMibView exception - end of the MIB has been reached.
272    ///
273    /// This exception is returned during GETNEXT/GETBULK operations when
274    /// there are no more OIDs lexicographically greater than the requested OID.
275    /// This is the normal termination condition for SNMP walks.
276    ///
277    /// # Example
278    ///
279    /// ```
280    /// use async_snmp::Value;
281    ///
282    /// let response = Value::EndOfMibView;
283    /// assert!(response.is_exception());
284    ///
285    /// // Commonly used to detect end of walk
286    /// if matches!(response, Value::EndOfMibView) {
287    ///     println!("Walk complete - reached end of MIB");
288    /// }
289    /// ```
290    EndOfMibView,
291
292    /// Unknown/unrecognized value type (for forward compatibility)
293    Unknown { tag: u8, data: Bytes },
294}
295
296impl Value {
297    /// Try to get as i32.
298    ///
299    /// Returns `Some(i32)` for [`Value::Integer`], `None` otherwise.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use async_snmp::Value;
305    ///
306    /// let v = Value::Integer(42);
307    /// assert_eq!(v.as_i32(), Some(42));
308    ///
309    /// let v = Value::Integer(-100);
310    /// assert_eq!(v.as_i32(), Some(-100));
311    ///
312    /// // Counter32 is not an Integer
313    /// let v = Value::Counter32(42);
314    /// assert_eq!(v.as_i32(), None);
315    /// ```
316    pub fn as_i32(&self) -> Option<i32> {
317        match self {
318            Value::Integer(v) => Some(*v),
319            _ => None,
320        }
321    }
322
323    /// Try to get as u32.
324    ///
325    /// Returns `Some(u32)` for [`Value::Counter32`], [`Value::Gauge32`],
326    /// [`Value::TimeTicks`], or non-negative [`Value::Integer`]. Returns `None` otherwise.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use async_snmp::Value;
332    ///
333    /// // Works for Counter32, Gauge32, TimeTicks
334    /// assert_eq!(Value::Counter32(100).as_u32(), Some(100));
335    /// assert_eq!(Value::Gauge32(200).as_u32(), Some(200));
336    /// assert_eq!(Value::TimeTicks(300).as_u32(), Some(300));
337    ///
338    /// // Works for non-negative integers
339    /// assert_eq!(Value::Integer(50).as_u32(), Some(50));
340    ///
341    /// // Returns None for negative integers
342    /// assert_eq!(Value::Integer(-1).as_u32(), None);
343    ///
344    /// // Counter64 returns None (use as_u64 instead)
345    /// assert_eq!(Value::Counter64(100).as_u32(), None);
346    /// ```
347    pub fn as_u32(&self) -> Option<u32> {
348        match self {
349            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v),
350            Value::Integer(v) if *v >= 0 => Some(*v as u32),
351            _ => None,
352        }
353    }
354
355    /// Try to get as u64.
356    ///
357    /// Returns `Some(u64)` for [`Value::Counter64`], or any 32-bit unsigned type
358    /// ([`Value::Counter32`], [`Value::Gauge32`], [`Value::TimeTicks`]), or
359    /// non-negative [`Value::Integer`]. Returns `None` otherwise.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// use async_snmp::Value;
365    ///
366    /// // Counter64 is the primary use case
367    /// assert_eq!(Value::Counter64(10_000_000_000).as_u64(), Some(10_000_000_000));
368    ///
369    /// // Also works for 32-bit unsigned types
370    /// assert_eq!(Value::Counter32(100).as_u64(), Some(100));
371    /// assert_eq!(Value::Gauge32(200).as_u64(), Some(200));
372    ///
373    /// // Non-negative integers work
374    /// assert_eq!(Value::Integer(50).as_u64(), Some(50));
375    ///
376    /// // Negative integers return None
377    /// assert_eq!(Value::Integer(-1).as_u64(), None);
378    /// ```
379    pub fn as_u64(&self) -> Option<u64> {
380        match self {
381            Value::Counter64(v) => Some(*v),
382            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v as u64),
383            Value::Integer(v) if *v >= 0 => Some(*v as u64),
384            _ => None,
385        }
386    }
387
388    /// Try to get as bytes.
389    ///
390    /// Returns `Some(&[u8])` for [`Value::OctetString`] or [`Value::Opaque`].
391    /// Returns `None` otherwise.
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// use async_snmp::Value;
397    /// use bytes::Bytes;
398    ///
399    /// let v = Value::OctetString(Bytes::from_static(b"hello"));
400    /// assert_eq!(v.as_bytes(), Some(b"hello".as_slice()));
401    ///
402    /// // Works for Opaque too
403    /// let v = Value::Opaque(Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]));
404    /// assert_eq!(v.as_bytes(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
405    ///
406    /// // Other types return None
407    /// assert_eq!(Value::Integer(42).as_bytes(), None);
408    /// ```
409    pub fn as_bytes(&self) -> Option<&[u8]> {
410        match self {
411            Value::OctetString(v) | Value::Opaque(v) => Some(v),
412            _ => None,
413        }
414    }
415
416    /// Try to get as string (UTF-8).
417    ///
418    /// Returns `Some(&str)` if the value is an [`Value::OctetString`] or [`Value::Opaque`]
419    /// containing valid UTF-8. Returns `None` for other types or invalid UTF-8.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use async_snmp::Value;
425    /// use bytes::Bytes;
426    ///
427    /// let v = Value::OctetString(Bytes::from_static(b"Linux router1 5.4.0"));
428    /// assert_eq!(v.as_str(), Some("Linux router1 5.4.0"));
429    ///
430    /// // Invalid UTF-8 returns None
431    /// let v = Value::OctetString(Bytes::from_static(&[0xFF, 0xFE]));
432    /// assert_eq!(v.as_str(), None);
433    ///
434    /// // Binary data with valid UTF-8 bytes still works, but use as_bytes() for clarity
435    /// let binary = Value::OctetString(Bytes::from_static(&[0x80, 0x81, 0x82]));
436    /// assert_eq!(binary.as_str(), None); // Invalid UTF-8 sequence
437    /// assert!(binary.as_bytes().is_some());
438    /// ```
439    pub fn as_str(&self) -> Option<&str> {
440        self.as_bytes().and_then(|b| std::str::from_utf8(b).ok())
441    }
442
443    /// Try to get as OID.
444    ///
445    /// Returns `Some(&Oid)` for [`Value::ObjectIdentifier`], `None` otherwise.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// use async_snmp::{Value, oid};
451    ///
452    /// let v = Value::ObjectIdentifier(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0));
453    /// let oid = v.as_oid().unwrap();
454    /// assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.2.0");
455    ///
456    /// // Other types return None
457    /// assert_eq!(Value::Integer(42).as_oid(), None);
458    /// ```
459    pub fn as_oid(&self) -> Option<&Oid> {
460        match self {
461            Value::ObjectIdentifier(oid) => Some(oid),
462            _ => None,
463        }
464    }
465
466    /// Try to get as IP address.
467    ///
468    /// Returns `Some(Ipv4Addr)` for [`Value::IpAddress`], `None` otherwise.
469    ///
470    /// # Examples
471    ///
472    /// ```
473    /// use async_snmp::Value;
474    /// use std::net::Ipv4Addr;
475    ///
476    /// let v = Value::IpAddress([192, 168, 1, 1]);
477    /// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(192, 168, 1, 1)));
478    ///
479    /// // Other types return None
480    /// assert_eq!(Value::Integer(42).as_ip(), None);
481    /// ```
482    pub fn as_ip(&self) -> Option<std::net::Ipv4Addr> {
483        match self {
484            Value::IpAddress(bytes) => Some(std::net::Ipv4Addr::from(*bytes)),
485            _ => None,
486        }
487    }
488
489    /// Extract any numeric value as f64.
490    ///
491    /// Useful for metrics systems and graphing where all values become f64.
492    /// Counter64 values above 2^53 may lose precision.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use async_snmp::Value;
498    ///
499    /// assert_eq!(Value::Integer(42).as_f64(), Some(42.0));
500    /// assert_eq!(Value::Counter32(1000).as_f64(), Some(1000.0));
501    /// assert_eq!(Value::Counter64(10_000_000_000).as_f64(), Some(10_000_000_000.0));
502    /// assert_eq!(Value::Null.as_f64(), None);
503    /// ```
504    pub fn as_f64(&self) -> Option<f64> {
505        match self {
506            Value::Integer(v) => Some(*v as f64),
507            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => Some(*v as f64),
508            Value::Counter64(v) => Some(*v as f64),
509            _ => None,
510        }
511    }
512
513    /// Extract Counter64 as f64 with wrapping at 2^53.
514    ///
515    /// Prevents precision loss for large counters. IEEE 754 double-precision
516    /// floats have a 53-bit mantissa, so Counter64 values above 2^53 lose
517    /// precision when converted directly. This method wraps at the mantissa
518    /// limit, preserving precision for rate calculations.
519    ///
520    /// Use when computing rates where precision matters more than absolute
521    /// magnitude. For Counter32 and other types, behaves identically to `as_f64()`.
522    ///
523    /// # Examples
524    ///
525    /// ```
526    /// use async_snmp::Value;
527    ///
528    /// // Small values behave the same as as_f64()
529    /// assert_eq!(Value::Counter64(1000).as_f64_wrapped(), Some(1000.0));
530    ///
531    /// // Large Counter64 wraps at 2^53
532    /// let large = 1u64 << 54; // 2^54
533    /// let wrapped = Value::Counter64(large).as_f64_wrapped().unwrap();
534    /// assert!(wrapped < large as f64); // Wrapped to smaller value
535    /// ```
536    pub fn as_f64_wrapped(&self) -> Option<f64> {
537        const MANTISSA_LIMIT: u64 = 1 << 53;
538        match self {
539            Value::Counter64(v) => Some((*v % MANTISSA_LIMIT) as f64),
540            _ => self.as_f64(),
541        }
542    }
543
544    /// Extract integer with implied decimal places.
545    ///
546    /// Many SNMP sensors report fixed-point values as integers with an
547    /// implied decimal point. This method applies the scaling directly,
548    /// returning a usable f64 value.
549    ///
550    /// This complements `format_with_hint("d-2")` which returns a String
551    /// for display. Use `as_decimal()` when you need the numeric value
552    /// for computation or metrics.
553    ///
554    /// # Examples
555    ///
556    /// ```
557    /// use async_snmp::Value;
558    ///
559    /// // Temperature 2350 with places=2 → 23.50
560    /// assert_eq!(Value::Integer(2350).as_decimal(2), Some(23.50));
561    ///
562    /// // Percentage 9999 with places=2 → 99.99
563    /// assert_eq!(Value::Integer(9999).as_decimal(2), Some(99.99));
564    ///
565    /// // Voltage 12500 with places=3 → 12.500
566    /// assert_eq!(Value::Integer(12500).as_decimal(3), Some(12.5));
567    ///
568    /// // Non-numeric types return None
569    /// assert_eq!(Value::Null.as_decimal(2), None);
570    /// ```
571    pub fn as_decimal(&self, places: u8) -> Option<f64> {
572        let divisor = 10f64.powi(places as i32);
573        self.as_f64().map(|v| v / divisor)
574    }
575
576    /// TimeTicks as Duration (hundredths of seconds).
577    ///
578    /// TimeTicks represents time in hundredths of a second. This method
579    /// converts to `std::time::Duration` for idiomatic Rust time handling.
580    ///
581    /// Common use: sysUpTime, interface last-change timestamps.
582    ///
583    /// # Examples
584    ///
585    /// ```
586    /// use async_snmp::Value;
587    /// use std::time::Duration;
588    ///
589    /// // 360000 ticks = 3600 seconds = 1 hour
590    /// let uptime = Value::TimeTicks(360000);
591    /// assert_eq!(uptime.as_duration(), Some(Duration::from_secs(3600)));
592    ///
593    /// // Non-TimeTicks return None
594    /// assert_eq!(Value::Integer(100).as_duration(), None);
595    /// ```
596    pub fn as_duration(&self) -> Option<std::time::Duration> {
597        match self {
598            Value::TimeTicks(v) => Some(std::time::Duration::from_millis(*v as u64 * 10)),
599            _ => None,
600        }
601    }
602
603    /// Extract IEEE 754 float from Opaque value (net-snmp extension).
604    ///
605    /// Decodes the two-layer ASN.1 structure used by net-snmp to encode floats
606    /// inside Opaque values: extension tag (0x9f) + float type (0x78) + length (4)
607    /// + 4 bytes IEEE 754 big-endian float.
608    ///
609    /// This is a non-standard extension supported by net-snmp for agents that
610    /// need to report floating-point values. Standard SNMP uses implied decimal
611    /// points via DISPLAY-HINT instead.
612    ///
613    /// # Examples
614    ///
615    /// ```
616    /// use async_snmp::Value;
617    /// use bytes::Bytes;
618    ///
619    /// // Opaque-encoded float for pi
620    /// let data = Bytes::from_static(&[0x9f, 0x78, 0x04, 0x40, 0x49, 0x0f, 0xdb]);
621    /// let value = Value::Opaque(data);
622    /// let pi = value.as_opaque_float().unwrap();
623    /// assert!((pi - std::f32::consts::PI).abs() < 0.0001);
624    ///
625    /// // Non-Opaque or wrong format returns None
626    /// assert_eq!(Value::Integer(42).as_opaque_float(), None);
627    /// ```
628    pub fn as_opaque_float(&self) -> Option<f32> {
629        match self {
630            Value::Opaque(data)
631                if data.len() >= 7
632                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
633                && data[1] == 0x78       // ASN_OPAQUE_FLOAT
634                && data[2] == 0x04 =>
635            {
636                // length = 4
637                let bytes: [u8; 4] = data[3..7].try_into().ok()?;
638                Some(f32::from_be_bytes(bytes))
639            }
640            _ => None,
641        }
642    }
643
644    /// Extract IEEE 754 double from Opaque value (net-snmp extension).
645    ///
646    /// Decodes the two-layer ASN.1 structure used by net-snmp to encode doubles
647    /// inside Opaque values: extension tag (0x9f) + double type (0x79) + length (8)
648    /// + 8 bytes IEEE 754 big-endian double.
649    ///
650    /// # Examples
651    ///
652    /// ```
653    /// use async_snmp::Value;
654    /// use bytes::Bytes;
655    ///
656    /// // Opaque-encoded double for pi ≈ 3.141592653589793
657    /// let data = Bytes::from_static(&[
658    ///     0x9f, 0x79, 0x08,  // extension tag, double type, length
659    ///     0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2d, 0x18  // IEEE 754 double
660    /// ]);
661    /// let value = Value::Opaque(data);
662    /// let pi = value.as_opaque_double().unwrap();
663    /// assert!((pi - std::f64::consts::PI).abs() < 1e-10);
664    /// ```
665    pub fn as_opaque_double(&self) -> Option<f64> {
666        match self {
667            Value::Opaque(data)
668                if data.len() >= 11
669                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
670                && data[1] == 0x79       // ASN_OPAQUE_DOUBLE
671                && data[2] == 0x08 =>
672            {
673                // length = 8
674                let bytes: [u8; 8] = data[3..11].try_into().ok()?;
675                Some(f64::from_be_bytes(bytes))
676            }
677            _ => None,
678        }
679    }
680
681    /// Extract Counter64 from Opaque value (net-snmp extension for SNMPv1).
682    ///
683    /// SNMPv1 doesn't support Counter64 natively. net-snmp encodes 64-bit
684    /// counters inside Opaque for SNMPv1 compatibility using extension tag
685    /// (0x9f) + counter64 type (0x76) + length + big-endian bytes.
686    ///
687    /// # Examples
688    ///
689    /// ```
690    /// use async_snmp::Value;
691    /// use bytes::Bytes;
692    ///
693    /// // Opaque-encoded Counter64 with value 0x0123456789ABCDEF
694    /// let data = Bytes::from_static(&[
695    ///     0x9f, 0x76, 0x08,  // extension tag, counter64 type, length
696    ///     0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
697    /// ]);
698    /// let value = Value::Opaque(data);
699    /// assert_eq!(value.as_opaque_counter64(), Some(0x0123456789ABCDEF));
700    /// ```
701    pub fn as_opaque_counter64(&self) -> Option<u64> {
702        self.as_opaque_unsigned(0x76)
703    }
704
705    /// Extract signed 64-bit integer from Opaque value (net-snmp extension).
706    ///
707    /// Uses extension tag (0x9f) + i64 type (0x7a) + length + big-endian bytes.
708    ///
709    /// # Examples
710    ///
711    /// ```
712    /// use async_snmp::Value;
713    /// use bytes::Bytes;
714    ///
715    /// // Opaque-encoded I64 with value -1 (0xFFFFFFFFFFFFFFFF)
716    /// let data = Bytes::from_static(&[
717    ///     0x9f, 0x7a, 0x08,
718    ///     0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
719    /// ]);
720    /// let value = Value::Opaque(data);
721    /// assert_eq!(value.as_opaque_i64(), Some(-1i64));
722    /// ```
723    pub fn as_opaque_i64(&self) -> Option<i64> {
724        match self {
725            Value::Opaque(data)
726                if data.len() >= 4
727                && data[0] == 0x9f       // ASN_OPAQUE_TAG1 (extension)
728                && data[1] == 0x7a =>
729            {
730                // ASN_OPAQUE_I64
731                let len = data[2] as usize;
732                if data.len() < 3 + len || len == 0 || len > 8 {
733                    return None;
734                }
735                let bytes = &data[3..3 + len];
736                // Sign-extend from the actual length
737                let is_negative = bytes[0] & 0x80 != 0;
738                let mut value: i64 = if is_negative { -1 } else { 0 };
739                for &byte in bytes {
740                    value = (value << 8) | (byte as i64);
741                }
742                Some(value)
743            }
744            _ => None,
745        }
746    }
747
748    /// Extract unsigned 64-bit integer from Opaque value (net-snmp extension).
749    ///
750    /// Uses extension tag (0x9f) + u64 type (0x7b) + length + big-endian bytes.
751    ///
752    /// # Examples
753    ///
754    /// ```
755    /// use async_snmp::Value;
756    /// use bytes::Bytes;
757    ///
758    /// // Opaque-encoded U64 with value 0x0123456789ABCDEF
759    /// let data = Bytes::from_static(&[
760    ///     0x9f, 0x7b, 0x08,
761    ///     0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
762    /// ]);
763    /// let value = Value::Opaque(data);
764    /// assert_eq!(value.as_opaque_u64(), Some(0x0123456789ABCDEF));
765    /// ```
766    pub fn as_opaque_u64(&self) -> Option<u64> {
767        self.as_opaque_unsigned(0x7b)
768    }
769
770    /// Helper for extracting unsigned 64-bit values from Opaque (Counter64, U64).
771    fn as_opaque_unsigned(&self, expected_type: u8) -> Option<u64> {
772        match self {
773            Value::Opaque(data)
774                if data.len() >= 4
775                && data[0] == 0x9f           // ASN_OPAQUE_TAG1 (extension)
776                && data[1] == expected_type =>
777            {
778                let len = data[2] as usize;
779                if data.len() < 3 + len || len == 0 || len > 8 {
780                    return None;
781                }
782                let bytes = &data[3..3 + len];
783                let mut value: u64 = 0;
784                for &byte in bytes {
785                    value = (value << 8) | (byte as u64);
786                }
787                Some(value)
788            }
789            _ => None,
790        }
791    }
792
793    /// Extract RFC 2579 TruthValue as bool.
794    ///
795    /// TruthValue is an INTEGER with: true(1), false(2).
796    /// Returns `None` for non-Integer values or values outside {1, 2}.
797    ///
798    /// # Examples
799    ///
800    /// ```
801    /// use async_snmp::Value;
802    ///
803    /// assert_eq!(Value::Integer(1).as_truth_value(), Some(true));
804    /// assert_eq!(Value::Integer(2).as_truth_value(), Some(false));
805    ///
806    /// // Invalid values return None
807    /// assert_eq!(Value::Integer(0).as_truth_value(), None);
808    /// assert_eq!(Value::Integer(3).as_truth_value(), None);
809    /// assert_eq!(Value::Null.as_truth_value(), None);
810    /// ```
811    pub fn as_truth_value(&self) -> Option<bool> {
812        match self {
813            Value::Integer(1) => Some(true),
814            Value::Integer(2) => Some(false),
815            _ => None,
816        }
817    }
818
819    /// Extract RFC 2579 RowStatus.
820    ///
821    /// Returns `None` for non-Integer values or values outside {1-6}.
822    ///
823    /// # Examples
824    ///
825    /// ```
826    /// use async_snmp::{Value, RowStatus};
827    ///
828    /// assert_eq!(Value::Integer(1).as_row_status(), Some(RowStatus::Active));
829    /// assert_eq!(Value::Integer(6).as_row_status(), Some(RowStatus::Destroy));
830    ///
831    /// // Invalid values return None
832    /// assert_eq!(Value::Integer(0).as_row_status(), None);
833    /// assert_eq!(Value::Integer(7).as_row_status(), None);
834    /// assert_eq!(Value::Null.as_row_status(), None);
835    /// ```
836    pub fn as_row_status(&self) -> Option<RowStatus> {
837        match self {
838            Value::Integer(v) => RowStatus::from_i32(*v),
839            _ => None,
840        }
841    }
842
843    /// Extract RFC 2579 StorageType.
844    ///
845    /// Returns `None` for non-Integer values or values outside {1-5}.
846    ///
847    /// # Examples
848    ///
849    /// ```
850    /// use async_snmp::{Value, StorageType};
851    ///
852    /// assert_eq!(Value::Integer(2).as_storage_type(), Some(StorageType::Volatile));
853    /// assert_eq!(Value::Integer(3).as_storage_type(), Some(StorageType::NonVolatile));
854    ///
855    /// // Invalid values return None
856    /// assert_eq!(Value::Integer(0).as_storage_type(), None);
857    /// assert_eq!(Value::Integer(6).as_storage_type(), None);
858    /// assert_eq!(Value::Null.as_storage_type(), None);
859    /// ```
860    pub fn as_storage_type(&self) -> Option<StorageType> {
861        match self {
862            Value::Integer(v) => StorageType::from_i32(*v),
863            _ => None,
864        }
865    }
866
867    /// Check if this is an exception value.
868    pub fn is_exception(&self) -> bool {
869        matches!(
870            self,
871            Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView
872        )
873    }
874
875    /// Returns the total BER-encoded size (tag + length + content).
876    pub(crate) fn ber_encoded_size(&self) -> usize {
877        use crate::ber::{
878            integer_content_len, length_encoded_len, unsigned32_content_len, unsigned64_content_len,
879        };
880
881        match self {
882            Value::Integer(v) => {
883                let content_len = integer_content_len(*v);
884                1 + length_encoded_len(content_len) + content_len
885            }
886            Value::OctetString(data) => {
887                let content_len = data.len();
888                1 + length_encoded_len(content_len) + content_len
889            }
890            Value::Null => 2, // tag + length(0)
891            Value::ObjectIdentifier(oid) => oid.ber_encoded_size(),
892            Value::IpAddress(_) => 6, // tag + length(4) + 4 bytes
893            Value::Counter32(v) | Value::Gauge32(v) | Value::TimeTicks(v) => {
894                let content_len = unsigned32_content_len(*v);
895                1 + length_encoded_len(content_len) + content_len
896            }
897            Value::Opaque(data) => {
898                let content_len = data.len();
899                1 + length_encoded_len(content_len) + content_len
900            }
901            Value::Counter64(v) => {
902                let content_len = unsigned64_content_len(*v);
903                1 + length_encoded_len(content_len) + content_len
904            }
905            Value::NoSuchObject | Value::NoSuchInstance | Value::EndOfMibView => 2, // tag + length(0)
906            Value::Unknown { data, .. } => {
907                let content_len = data.len();
908                1 + length_encoded_len(content_len) + content_len
909            }
910        }
911    }
912
913    /// Format an OctetString, Opaque, or Integer value using RFC 2579 DISPLAY-HINT.
914    ///
915    /// For OctetString and Opaque, uses the OCTET STRING hint format (e.g., "1x:").
916    /// For Integer, uses the INTEGER hint format (e.g., "d-2" for decimal places).
917    ///
918    /// Returns `None` for other value types or invalid hint syntax.
919    /// On invalid OCTET STRING hint syntax, falls back to hex encoding.
920    ///
921    /// # Example
922    ///
923    /// ```
924    /// use async_snmp::Value;
925    /// use bytes::Bytes;
926    ///
927    /// // OctetString: MAC address
928    /// let mac = Value::OctetString(Bytes::from_static(&[0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]));
929    /// assert_eq!(mac.format_with_hint("1x:"), Some("00:1a:2b:3c:4d:5e".into()));
930    ///
931    /// // Integer: Temperature with 2 decimal places
932    /// let temp = Value::Integer(2350);
933    /// assert_eq!(temp.format_with_hint("d-2"), Some("23.50".into()));
934    ///
935    /// // Integer: Hex format
936    /// let int = Value::Integer(255);
937    /// assert_eq!(int.format_with_hint("x"), Some("ff".into()));
938    /// ```
939    pub fn format_with_hint(&self, hint: &str) -> Option<String> {
940        match self {
941            Value::OctetString(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
942            Value::Opaque(bytes) => Some(crate::format::display_hint::apply(hint, bytes)),
943            Value::Integer(v) => crate::format::display_hint::apply_integer(hint, *v),
944            _ => None,
945        }
946    }
947
948    /// Encode to BER.
949    pub fn encode(&self, buf: &mut EncodeBuf) {
950        match self {
951            Value::Integer(v) => buf.push_integer(*v),
952            Value::OctetString(data) => buf.push_octet_string(data),
953            Value::Null => buf.push_null(),
954            Value::ObjectIdentifier(oid) => buf.push_oid(oid),
955            Value::IpAddress(addr) => buf.push_ip_address(*addr),
956            Value::Counter32(v) => buf.push_unsigned32(tag::application::COUNTER32, *v),
957            Value::Gauge32(v) => buf.push_unsigned32(tag::application::GAUGE32, *v),
958            Value::TimeTicks(v) => buf.push_unsigned32(tag::application::TIMETICKS, *v),
959            Value::Opaque(data) => {
960                buf.push_bytes(data);
961                buf.push_length(data.len());
962                buf.push_tag(tag::application::OPAQUE);
963            }
964            Value::Counter64(v) => buf.push_integer64(*v),
965            Value::NoSuchObject => {
966                buf.push_length(0);
967                buf.push_tag(tag::context::NO_SUCH_OBJECT);
968            }
969            Value::NoSuchInstance => {
970                buf.push_length(0);
971                buf.push_tag(tag::context::NO_SUCH_INSTANCE);
972            }
973            Value::EndOfMibView => {
974                buf.push_length(0);
975                buf.push_tag(tag::context::END_OF_MIB_VIEW);
976            }
977            Value::Unknown { tag: t, data } => {
978                buf.push_bytes(data);
979                buf.push_length(data.len());
980                buf.push_tag(*t);
981            }
982        }
983    }
984
985    /// Decode from BER.
986    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
987        let tag = decoder.read_tag()?;
988        let len = decoder.read_length()?;
989
990        match tag {
991            tag::universal::INTEGER => {
992                let value = decoder.read_integer_value(len)?;
993                Ok(Value::Integer(value))
994            }
995            tag::universal::OCTET_STRING => {
996                let available = decoder.remaining();
997                let len = if len > available {
998                    // Some devices (notably MikroTik) send OctetString values
999                    // where the declared length exceeds the enclosing varbind
1000                    // SEQUENCE boundary by 1-2 bytes (firmware bug). Trust the
1001                    // SEQUENCE boundary over the value length field and clamp.
1002                    tracing::warn!(
1003                        target: "async_snmp::value",
1004                        { snmp.offset = %decoder.offset(), declared = len, available },
1005                        "OctetString length exceeds varbind SEQUENCE boundary, clamping to available bytes"
1006                    );
1007                    available
1008                } else {
1009                    len
1010                };
1011                let data = decoder.read_bytes(len)?;
1012                Ok(Value::OctetString(data))
1013            }
1014            tag::universal::NULL => {
1015                if len != 0 {
1016                    tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), kind = %DecodeErrorKind::InvalidNull }, "decode error");
1017                    return Err(Error::MalformedResponse {
1018                        target: UNKNOWN_TARGET,
1019                    }
1020                    .boxed());
1021                }
1022                Ok(Value::Null)
1023            }
1024            tag::universal::OBJECT_IDENTIFIER => {
1025                let oid = decoder.read_oid_value(len)?;
1026                Ok(Value::ObjectIdentifier(oid))
1027            }
1028            tag::application::IP_ADDRESS => {
1029                if len != 4 {
1030                    tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), length = len, kind = %DecodeErrorKind::InvalidIpAddressLength { length: len } }, "decode error");
1031                    return Err(Error::MalformedResponse {
1032                        target: UNKNOWN_TARGET,
1033                    }
1034                    .boxed());
1035                }
1036                let data = decoder.read_bytes(4)?;
1037                Ok(Value::IpAddress([data[0], data[1], data[2], data[3]]))
1038            }
1039            tag::application::COUNTER32 => {
1040                let value = decoder.read_unsigned32_value(len)?;
1041                Ok(Value::Counter32(value))
1042            }
1043            tag::application::GAUGE32 => {
1044                let value = decoder.read_unsigned32_value(len)?;
1045                Ok(Value::Gauge32(value))
1046            }
1047            tag::application::TIMETICKS => {
1048                let value = decoder.read_unsigned32_value(len)?;
1049                Ok(Value::TimeTicks(value))
1050            }
1051            tag::application::OPAQUE => {
1052                let available = decoder.remaining();
1053                let len = if len > available {
1054                    tracing::warn!(
1055                        target: "async_snmp::value",
1056                        { snmp.offset = %decoder.offset(), declared = len, available },
1057                        "Opaque length exceeds varbind SEQUENCE boundary, clamping to available bytes"
1058                    );
1059                    available
1060                } else {
1061                    len
1062                };
1063                let data = decoder.read_bytes(len)?;
1064                Ok(Value::Opaque(data))
1065            }
1066            tag::application::NSAP => {
1067                let data = decoder.read_bytes(len)?;
1068                Ok(Value::OctetString(data))
1069            }
1070            tag::application::COUNTER64 => {
1071                let value = decoder.read_integer64_value(len)?;
1072                Ok(Value::Counter64(value))
1073            }
1074            tag::application::UINTEGER32 => {
1075                let value = decoder.read_unsigned32_value(len)?;
1076                Ok(Value::Gauge32(value))
1077            }
1078            tag::context::NO_SUCH_OBJECT => {
1079                if len != 0 {
1080                    let _ = decoder.read_bytes(len)?;
1081                }
1082                Ok(Value::NoSuchObject)
1083            }
1084            tag::context::NO_SUCH_INSTANCE => {
1085                if len != 0 {
1086                    let _ = decoder.read_bytes(len)?;
1087                }
1088                Ok(Value::NoSuchInstance)
1089            }
1090            tag::context::END_OF_MIB_VIEW => {
1091                if len != 0 {
1092                    let _ = decoder.read_bytes(len)?;
1093                }
1094                Ok(Value::EndOfMibView)
1095            }
1096            // Reject constructed OCTET STRING (0x24).
1097            // Net-snmp documents but does not parse constructed form; we follow suit.
1098            tag::universal::OCTET_STRING_CONSTRUCTED => {
1099                tracing::debug!(target: "async_snmp::value", { offset = decoder.offset(), kind = %DecodeErrorKind::ConstructedOctetString }, "decode error");
1100                Err(Error::MalformedResponse {
1101                    target: UNKNOWN_TARGET,
1102                }
1103                .boxed())
1104            }
1105            _ => {
1106                // Unknown tag - preserve for forward compatibility
1107                let data = decoder.read_bytes(len)?;
1108                Ok(Value::Unknown { tag, data })
1109            }
1110        }
1111    }
1112}
1113
1114impl std::fmt::Display for Value {
1115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1116        match self {
1117            Value::Integer(v) => write!(f, "{}", v),
1118            Value::OctetString(data) => {
1119                // Try to display as string if it's valid UTF-8
1120                if let Ok(s) = std::str::from_utf8(data) {
1121                    write!(f, "{}", s)
1122                } else {
1123                    write!(f, "0x{}", hex::encode(data))
1124                }
1125            }
1126            Value::Null => write!(f, "NULL"),
1127            Value::ObjectIdentifier(oid) => write!(f, "{}", oid),
1128            Value::IpAddress(addr) => {
1129                write!(f, "{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3])
1130            }
1131            Value::Counter32(v) => write!(f, "{}", v),
1132            Value::Gauge32(v) => write!(f, "{}", v),
1133            Value::TimeTicks(v) => {
1134                write!(f, "{}", crate::format::format_timeticks(*v))
1135            }
1136            Value::Opaque(data) => write!(f, "Opaque(0x{})", hex::encode(data)),
1137            Value::Counter64(v) => write!(f, "{}", v),
1138            Value::NoSuchObject => write!(f, "noSuchObject"),
1139            Value::NoSuchInstance => write!(f, "noSuchInstance"),
1140            Value::EndOfMibView => write!(f, "endOfMibView"),
1141            Value::Unknown { tag, data } => {
1142                write!(
1143                    f,
1144                    "Unknown(tag=0x{:02X}, data=0x{})",
1145                    tag,
1146                    hex::encode(data)
1147                )
1148            }
1149        }
1150    }
1151}
1152
1153/// Convenience conversions for creating [`Value`] from common Rust types.
1154///
1155/// # Examples
1156///
1157/// ```
1158/// use async_snmp::Value;
1159/// use bytes::Bytes;
1160///
1161/// // From integers
1162/// let v: Value = 42i32.into();
1163/// assert_eq!(v.as_i32(), Some(42));
1164///
1165/// // From strings (creates OctetString)
1166/// let v: Value = "hello".into();
1167/// assert_eq!(v.as_str(), Some("hello"));
1168///
1169/// // From String
1170/// let v: Value = String::from("world").into();
1171/// assert_eq!(v.as_str(), Some("world"));
1172///
1173/// // From byte slices
1174/// let v: Value = (&[1u8, 2, 3][..]).into();
1175/// assert_eq!(v.as_bytes(), Some(&[1, 2, 3][..]));
1176///
1177/// // From Bytes
1178/// let v: Value = Bytes::from_static(b"data").into();
1179/// assert_eq!(v.as_bytes(), Some(b"data".as_slice()));
1180///
1181/// // From u64 (creates Counter64)
1182/// let v: Value = 10_000_000_000u64.into();
1183/// assert_eq!(v.as_u64(), Some(10_000_000_000));
1184///
1185/// // From Ipv4Addr
1186/// use std::net::Ipv4Addr;
1187/// let v: Value = Ipv4Addr::new(10, 0, 0, 1).into();
1188/// assert_eq!(v.as_ip(), Some(Ipv4Addr::new(10, 0, 0, 1)));
1189///
1190/// // From [u8; 4] (creates IpAddress)
1191/// let v: Value = [192u8, 168, 1, 1].into();
1192/// assert!(matches!(v, Value::IpAddress([192, 168, 1, 1])));
1193/// ```
1194impl From<i32> for Value {
1195    fn from(v: i32) -> Self {
1196        Value::Integer(v)
1197    }
1198}
1199
1200impl From<&str> for Value {
1201    fn from(s: &str) -> Self {
1202        Value::OctetString(Bytes::copy_from_slice(s.as_bytes()))
1203    }
1204}
1205
1206impl From<String> for Value {
1207    fn from(s: String) -> Self {
1208        Value::OctetString(Bytes::from(s))
1209    }
1210}
1211
1212impl From<&[u8]> for Value {
1213    fn from(data: &[u8]) -> Self {
1214        Value::OctetString(Bytes::copy_from_slice(data))
1215    }
1216}
1217
1218impl From<Oid> for Value {
1219    fn from(oid: Oid) -> Self {
1220        Value::ObjectIdentifier(oid)
1221    }
1222}
1223
1224impl From<std::net::Ipv4Addr> for Value {
1225    fn from(addr: std::net::Ipv4Addr) -> Self {
1226        Value::IpAddress(addr.octets())
1227    }
1228}
1229
1230impl From<Bytes> for Value {
1231    fn from(data: Bytes) -> Self {
1232        Value::OctetString(data)
1233    }
1234}
1235
1236impl From<u64> for Value {
1237    fn from(v: u64) -> Self {
1238        Value::Counter64(v)
1239    }
1240}
1241
1242/// Converts a 4-byte array into [`Value::IpAddress`].
1243///
1244/// The bytes are interpreted as a big-endian IPv4 address, matching the IpAddress
1245/// SNMP type (RFC 2578 Section 7.1.5). Use this when you already have an address
1246/// in `[u8; 4]` form (e.g., from `Ipv4Addr::octets()`).
1247impl From<[u8; 4]> for Value {
1248    fn from(addr: [u8; 4]) -> Self {
1249        Value::IpAddress(addr)
1250    }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255    use super::*;
1256
1257    // AUDIT-003: Test that constructed OCTET STRING (0x24) is explicitly rejected.
1258    // Net-snmp documents but does not parse constructed form; we reject it.
1259    #[test]
1260    fn test_reject_constructed_octet_string() {
1261        // Constructed OCTET STRING has tag 0x24 (0x04 | 0x20)
1262        // Create a fake BER-encoded constructed OCTET STRING: 0x24 0x03 0x04 0x01 0x41
1263        // (constructed OCTET STRING containing primitive OCTET STRING "A")
1264        let data = bytes::Bytes::from_static(&[0x24, 0x03, 0x04, 0x01, 0x41]);
1265        let mut decoder = Decoder::new(data);
1266        let result = Value::decode(&mut decoder);
1267
1268        assert!(
1269            result.is_err(),
1270            "constructed OCTET STRING (0x24) should be rejected"
1271        );
1272        // Verify error is MalformedResponse (detailed error kind is logged via tracing)
1273        let err = result.unwrap_err();
1274        assert!(
1275            matches!(&*err, crate::Error::MalformedResponse { .. }),
1276            "expected MalformedResponse error, got: {:?}",
1277            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.clone()), value);
1309    }
1310
1311    #[test]
1312    fn test_integer_negative() {
1313        let value = Value::Integer(-42);
1314        assert_eq!(roundtrip(value.clone()), value);
1315    }
1316
1317    #[test]
1318    fn test_integer_zero() {
1319        let value = Value::Integer(0);
1320        assert_eq!(roundtrip(value.clone()), value);
1321    }
1322
1323    #[test]
1324    fn test_integer_min() {
1325        let value = Value::Integer(i32::MIN);
1326        assert_eq!(roundtrip(value.clone()), value);
1327    }
1328
1329    #[test]
1330    fn test_integer_max() {
1331        let value = Value::Integer(i32::MAX);
1332        assert_eq!(roundtrip(value.clone()), 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.clone()), 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.clone()), value);
1345    }
1346
1347    #[test]
1348    fn test_octet_string_empty() {
1349        let value = Value::OctetString(Bytes::new());
1350        assert_eq!(roundtrip(value.clone()), value);
1351    }
1352
1353    #[test]
1354    fn test_null() {
1355        let value = Value::Null;
1356        assert_eq!(roundtrip(value.clone()), 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.clone()), value);
1363    }
1364
1365    #[test]
1366    fn test_ip_address() {
1367        let value = Value::IpAddress([192, 168, 1, 1]);
1368        assert_eq!(roundtrip(value.clone()), 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.clone()), 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.clone()), value);
1381    }
1382
1383    #[test]
1384    fn test_counter32() {
1385        let value = Value::Counter32(999999);
1386        assert_eq!(roundtrip(value.clone()), value);
1387    }
1388
1389    #[test]
1390    fn test_counter32_zero() {
1391        let value = Value::Counter32(0);
1392        assert_eq!(roundtrip(value.clone()), value);
1393    }
1394
1395    #[test]
1396    fn test_counter32_max() {
1397        let value = Value::Counter32(u32::MAX);
1398        assert_eq!(roundtrip(value.clone()), value);
1399    }
1400
1401    #[test]
1402    fn test_gauge32() {
1403        let value = Value::Gauge32(1000000000);
1404        assert_eq!(roundtrip(value.clone()), value);
1405    }
1406
1407    #[test]
1408    fn test_gauge32_max() {
1409        let value = Value::Gauge32(u32::MAX);
1410        assert_eq!(roundtrip(value.clone()), value);
1411    }
1412
1413    #[test]
1414    fn test_timeticks() {
1415        let value = Value::TimeTicks(123456);
1416        assert_eq!(roundtrip(value.clone()), value);
1417    }
1418
1419    #[test]
1420    fn test_timeticks_max() {
1421        let value = Value::TimeTicks(u32::MAX);
1422        assert_eq!(roundtrip(value.clone()), 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.clone()), value);
1429    }
1430
1431    #[test]
1432    fn test_opaque_empty() {
1433        let value = Value::Opaque(Bytes::new());
1434        assert_eq!(roundtrip(value.clone()), value);
1435    }
1436
1437    #[test]
1438    fn test_counter64() {
1439        let value = Value::Counter64(123456789012345);
1440        assert_eq!(roundtrip(value.clone()), value);
1441    }
1442
1443    #[test]
1444    fn test_counter64_zero() {
1445        let value = Value::Counter64(0);
1446        assert_eq!(roundtrip(value.clone()), value);
1447    }
1448
1449    #[test]
1450    fn test_counter64_max() {
1451        let value = Value::Counter64(u64::MAX);
1452        assert_eq!(roundtrip(value.clone()), value);
1453    }
1454
1455    #[test]
1456    fn test_no_such_object() {
1457        let value = Value::NoSuchObject;
1458        assert_eq!(roundtrip(value.clone()), value);
1459    }
1460
1461    #[test]
1462    fn test_no_such_instance() {
1463        let value = Value::NoSuchInstance;
1464        assert_eq!(roundtrip(value.clone()), value);
1465    }
1466
1467    #[test]
1468    fn test_end_of_mib_view() {
1469        let value = Value::EndOfMibView;
1470        assert_eq!(roundtrip(value.clone()), 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.clone()), 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(123456);
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(12345678)), "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 = 12345678901234u64.into();
1730        assert_eq!(v, Value::Counter64(12345678901234));
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(360000).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(0x0123456789ABCDEF));
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(0x0123456789ABCDEF));
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}