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