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