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}