Skip to main content

async_snmp/
oid.rs

1//! Object Identifier (OID) type.
2//!
3//! OIDs are stored as `SmallVec<[u32; 16]>` to avoid heap allocation for common OIDs.
4
5use crate::error::internal::DecodeErrorKind;
6use crate::error::{Error, Result, UNKNOWN_TARGET};
7use smallvec::SmallVec;
8use std::fmt;
9
10/// Maximum number of arcs (subidentifiers) allowed in an OID.
11///
12/// Per RFC 2578 Section 3.5: "there are at most 128 sub-identifiers in a value".
13///
14/// This limit is enforced during BER decoding via [`Oid::from_ber()`], and can
15/// be checked via [`Oid::validate_length()`] for OIDs constructed from other sources.
16pub const MAX_OID_LEN: usize = 128;
17
18/// Object Identifier.
19///
20/// Stored as a sequence of arc values (u32). Uses `SmallVec` to avoid
21/// heap allocation for OIDs with 16 or fewer arcs.
22#[derive(Clone, PartialEq, Eq, Hash)]
23pub struct Oid {
24    arcs: SmallVec<[u32; 16]>,
25}
26
27impl Oid {
28    /// Create an empty OID.
29    #[must_use]
30    pub fn empty() -> Self {
31        Self {
32            arcs: SmallVec::new(),
33        }
34    }
35
36    /// Create an OID from arc values.
37    ///
38    /// Accepts any iterator of `u32` values.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use async_snmp::oid::Oid;
44    ///
45    /// // From a Vec
46    /// let oid = Oid::new(vec![1, 3, 6, 1, 2, 1]);
47    /// assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1]);
48    ///
49    /// // From an array
50    /// let oid = Oid::new([1, 3, 6, 1]);
51    /// assert_eq!(oid.len(), 4);
52    ///
53    /// // From a range
54    /// let oid = Oid::new(0..5);
55    /// assert_eq!(oid.arcs(), &[0, 1, 2, 3, 4]);
56    /// ```
57    pub fn new(arcs: impl IntoIterator<Item = u32>) -> Self {
58        Self {
59            arcs: arcs.into_iter().collect(),
60        }
61    }
62
63    /// Create an OID from a slice of arcs.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use async_snmp::oid::Oid;
69    ///
70    /// let arcs = [1, 3, 6, 1, 2, 1, 1, 1, 0];
71    /// let oid = Oid::from_slice(&arcs);
72    /// assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.1.0");
73    ///
74    /// // Empty slice creates an empty OID
75    /// let empty = Oid::from_slice(&[]);
76    /// assert!(empty.is_empty());
77    /// ```
78    #[must_use]
79    pub fn from_slice(arcs: &[u32]) -> Self {
80        Self {
81            arcs: SmallVec::from_slice(arcs),
82        }
83    }
84
85    /// Parse an OID from dotted string notation (e.g., "1.3.6.1.2.1.1.1.0").
86    ///
87    /// # Validation
88    ///
89    /// This method parses the string format but does **not** validate arc constraints
90    /// per X.690 Section 8.19.4. Invalid OIDs like `"3.0"` (arc1 must be 0, 1, or 2)
91    /// or `"0.40"` (arc2 must be ≤39 when arc1 < 2) will parse successfully.
92    ///
93    /// To validate arc constraints, call [`validate()`](Self::validate) after parsing,
94    /// or use [`to_ber_checked()`](Self::to_ber_checked) which validates before encoding.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use async_snmp::oid::Oid;
100    ///
101    /// // Valid OID
102    /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
103    /// assert!(oid.validate().is_ok());
104    ///
105    /// // Invalid arc1 parses but fails validation
106    /// let invalid = Oid::parse("3.0").unwrap();
107    /// assert!(invalid.validate().is_err());
108    /// ```
109    pub fn parse(s: &str) -> Result<Self> {
110        // Accept leading-dot notation (e.g. ".1.3.6.1.2.1") used by net-snmp
111        // and common in SNMP documentation to indicate absolute OIDs.
112        let s = s.strip_prefix('.').unwrap_or(s);
113
114        if s.is_empty() {
115            return Ok(Self::empty());
116        }
117
118        let mut arcs = SmallVec::new();
119
120        for part in s.split('.') {
121            if part.is_empty() {
122                return Err(Error::InvalidOid(format!("'{s}': empty arc").into()).boxed());
123            }
124
125            let arc: u32 = part
126                .parse()
127                .map_err(|_| Error::InvalidOid(format!("'{s}': invalid arc").into()).boxed())?;
128
129            arcs.push(arc);
130        }
131
132        Ok(Self { arcs })
133    }
134
135    /// Get the arc values.
136    #[must_use]
137    pub fn arcs(&self) -> &[u32] {
138        &self.arcs
139    }
140
141    /// Get the number of arcs.
142    #[must_use]
143    pub fn len(&self) -> usize {
144        self.arcs.len()
145    }
146
147    /// Check if the OID is empty.
148    #[must_use]
149    pub fn is_empty(&self) -> bool {
150        self.arcs.is_empty()
151    }
152
153    /// Check if this OID starts with another OID.
154    ///
155    /// Returns `true` if `self` begins with the same arcs as `other`.
156    /// An OID always starts with itself, and any OID starts with an empty OID.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use async_snmp::oid::Oid;
162    ///
163    /// let sys_descr = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
164    /// let system = Oid::parse("1.3.6.1.2.1.1").unwrap();
165    /// let interfaces = Oid::parse("1.3.6.1.2.1.2").unwrap();
166    ///
167    /// // sysDescr is under the system subtree
168    /// assert!(sys_descr.starts_with(&system));
169    ///
170    /// // sysDescr is not under the interfaces subtree
171    /// assert!(!sys_descr.starts_with(&interfaces));
172    ///
173    /// // Every OID starts with itself
174    /// assert!(sys_descr.starts_with(&sys_descr));
175    ///
176    /// // Every OID starts with the empty OID
177    /// assert!(sys_descr.starts_with(&Oid::empty()));
178    /// ```
179    #[must_use]
180    pub fn starts_with(&self, other: &Oid) -> bool {
181        self.arcs.len() >= other.arcs.len() && self.arcs[..other.arcs.len()] == other.arcs[..]
182    }
183
184    /// Get the parent OID (all arcs except the last).
185    ///
186    /// Returns `None` if the OID is empty.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use async_snmp::oid::Oid;
192    ///
193    /// let sys_descr = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
194    /// let parent = sys_descr.parent().unwrap();
195    /// assert_eq!(parent.to_string(), "1.3.6.1.2.1.1.1");
196    ///
197    /// // Can chain parent() calls
198    /// let grandparent = parent.parent().unwrap();
199    /// assert_eq!(grandparent.to_string(), "1.3.6.1.2.1.1");
200    ///
201    /// // Empty OID has no parent
202    /// assert!(Oid::empty().parent().is_none());
203    /// ```
204    #[must_use]
205    pub fn parent(&self) -> Option<Oid> {
206        if self.arcs.is_empty() {
207            None
208        } else {
209            Some(Oid {
210                arcs: SmallVec::from_slice(&self.arcs[..self.arcs.len() - 1]),
211            })
212        }
213    }
214
215    /// Create a child OID by appending an arc.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use async_snmp::oid::Oid;
221    ///
222    /// let system = Oid::parse("1.3.6.1.2.1.1").unwrap();
223    ///
224    /// // sysDescr is system.1
225    /// let sys_descr = system.child(1);
226    /// assert_eq!(sys_descr.to_string(), "1.3.6.1.2.1.1.1");
227    ///
228    /// // sysDescr.0 is the scalar instance
229    /// let sys_descr_instance = sys_descr.child(0);
230    /// assert_eq!(sys_descr_instance.to_string(), "1.3.6.1.2.1.1.1.0");
231    /// ```
232    #[must_use]
233    pub fn child(&self, arc: u32) -> Oid {
234        let mut arcs = self.arcs.clone();
235        arcs.push(arc);
236        Oid { arcs }
237    }
238
239    /// Strip a prefix OID, returning the remaining arcs as a new Oid.
240    ///
241    /// Returns `None` if `self` doesn't start with the given prefix.
242    /// Follows `str::strip_prefix` semantics - stripping an equal OID returns an empty OID.
243    ///
244    /// This is useful for extracting table indexes from walked OIDs.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use async_snmp::{oid, Oid};
250    ///
251    /// let if_descr_5 = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 5);
252    /// let if_descr = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
253    ///
254    /// // Extract the index
255    /// let index = if_descr_5.strip_prefix(&if_descr).unwrap();
256    /// assert_eq!(index.arcs(), &[5]);
257    ///
258    /// // Non-matching prefix returns None
259    /// let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1);
260    /// assert!(if_descr_5.strip_prefix(&sys_descr).is_none());
261    ///
262    /// // Equal OIDs return empty
263    /// let same = oid!(1, 3, 6);
264    /// assert!(same.strip_prefix(&same).unwrap().is_empty());
265    ///
266    /// // Empty prefix returns self
267    /// let any = oid!(1, 2, 3);
268    /// assert_eq!(any.strip_prefix(&Oid::empty()).unwrap(), any);
269    /// ```
270    #[must_use]
271    pub fn strip_prefix(&self, prefix: &Oid) -> Option<Oid> {
272        if self.starts_with(prefix) {
273            Some(Oid::from_slice(&self.arcs[prefix.len()..]))
274        } else {
275            None
276        }
277    }
278
279    /// Get the last N arcs as a slice (for multi-level table indexes).
280    ///
281    /// Returns `None` if `n` exceeds the OID length.
282    ///
283    /// This is useful for grouping SNMP table walk results by composite indexes.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use async_snmp::oid;
289    ///
290    /// // ipNetToMediaPhysAddress has index (ifIndex, IpAddress) = 5 arcs
291    /// let oid = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
292    ///
293    /// // Get the 5-arc index (ifIndex=1, IP=192.168.1.100)
294    /// let index = oid.suffix(5).unwrap();
295    /// assert_eq!(index, &[1, 192, 168, 1, 100]);
296    ///
297    /// // Get just the last arc
298    /// assert_eq!(oid.suffix(1), Some(&[100][..]));
299    ///
300    /// // suffix(0) returns empty slice
301    /// assert_eq!(oid.suffix(0), Some(&[][..]));
302    ///
303    /// // Too large returns None
304    /// assert!(oid.suffix(100).is_none());
305    /// ```
306    #[must_use]
307    pub fn suffix(&self, n: usize) -> Option<&[u32]> {
308        if n <= self.arcs.len() {
309            Some(&self.arcs[self.arcs.len() - n..])
310        } else {
311            None
312        }
313    }
314
315    /// Validate OID arcs per X.690 Section 8.19.4.
316    ///
317    /// - arc1 must be 0, 1, or 2
318    /// - arc2 must be <= 39 when arc1 is 0 or 1
319    /// - arc2 must not cause overflow when computing first subidentifier (arc1*40 + arc2)
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use async_snmp::oid::Oid;
325    ///
326    /// // Standard SNMP OIDs are valid
327    /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
328    /// assert!(oid.validate().is_ok());
329    ///
330    /// // arc1 must be 0, 1, or 2
331    /// let invalid = Oid::from_slice(&[3, 0]);
332    /// assert!(invalid.validate().is_err());
333    ///
334    /// // arc2 must be <= 39 when arc1 is 0 or 1
335    /// let invalid = Oid::from_slice(&[0, 40]);
336    /// assert!(invalid.validate().is_err());
337    ///
338    /// // arc2 can be large when arc1 is 2, but must not overflow
339    /// let valid = Oid::from_slice(&[2, 999]);
340    /// assert!(valid.validate().is_ok());
341    /// ```
342    pub fn validate(&self) -> Result<()> {
343        if self.arcs.is_empty() {
344            return Ok(());
345        }
346
347        let arc1 = self.arcs[0];
348
349        // arc1 must be 0, 1, or 2
350        if arc1 > 2 {
351            return Err(Error::InvalidOid(
352                format!("first arc must be 0, 1, or 2, got {arc1}").into(),
353            )
354            .boxed());
355        }
356
357        // Validate arc2 constraints
358        if self.arcs.len() >= 2 {
359            let arc2 = self.arcs[1];
360
361            // arc2 must be <= 39 when arc1 < 2
362            if arc1 < 2 && arc2 >= 40 {
363                return Err(Error::InvalidOid(
364                    format!("second arc must be <= 39 when first arc is {arc1}, got {arc2}").into(),
365                )
366                .boxed());
367            }
368
369            // Check that first subidentifier (arc1*40 + arc2) won't overflow u32.
370            // Max valid arc2 = u32::MAX - arc1*40
371            let base = arc1 * 40;
372            if arc2 > u32::MAX - base {
373                return Err(
374                    Error::InvalidOid("subidentifier overflow in first two arcs".into()).boxed(),
375                );
376            }
377        }
378
379        Ok(())
380    }
381
382    /// Validate that the OID doesn't exceed the maximum arc count.
383    ///
384    /// SNMP implementations commonly limit OIDs to 128 subidentifiers. This check
385    /// provides protection against `DoS` attacks from maliciously long OIDs.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// use async_snmp::oid::{Oid, MAX_OID_LEN};
391    ///
392    /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
393    /// assert!(oid.validate_length().is_ok());
394    ///
395    /// // Create an OID with too many arcs
396    /// let too_long: Vec<u32> = (0..150).collect();
397    /// let long_oid = Oid::new(too_long);
398    /// assert!(long_oid.validate_length().is_err());
399    /// ```
400    pub fn validate_length(&self) -> Result<()> {
401        if self.arcs.len() > MAX_OID_LEN {
402            return Err(Error::InvalidOid(
403                format!(
404                    "OID has {} arcs, exceeds maximum {}",
405                    self.arcs.len(),
406                    MAX_OID_LEN
407                )
408                .into(),
409            )
410            .boxed());
411        }
412        Ok(())
413    }
414
415    /// Validate both arc constraints and length.
416    ///
417    /// Combines [`validate()`](Self::validate) and [`validate_length()`](Self::validate_length).
418    pub fn validate_all(&self) -> Result<()> {
419        self.validate()?;
420        self.validate_length()
421    }
422
423    /// Encode to BER format, returning bytes in a stack-allocated buffer.
424    ///
425    /// Uses `SmallVec` to avoid heap allocation for OIDs with up to ~20 arcs.
426    /// This is the optimized version used internally by encoding routines.
427    ///
428    /// OID encoding (X.690 Section 8.19):
429    /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
430    /// - Remaining arcs encoded as base-128 variable length
431    pub(crate) fn to_ber_smallvec(&self) -> SmallVec<[u8; 64]> {
432        let mut bytes = SmallVec::new();
433
434        if self.arcs.is_empty() {
435            return bytes;
436        }
437
438        // First two arcs combined into first subidentifier.
439        // Uses base-128 encoding because arc2 can be > 127 when arc1=2.
440        encode_subidentifier_smallvec(&mut bytes, first_subidentifier(&self.arcs));
441
442        // Remaining arcs
443        for &arc in self.arcs.iter().skip(2) {
444            encode_subidentifier_smallvec(&mut bytes, arc);
445        }
446
447        bytes
448    }
449
450    /// Encode to BER format.
451    ///
452    /// OID encoding (X.690 Section 8.19):
453    /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
454    /// - Remaining arcs encoded as base-128 variable length
455    ///
456    /// # Empty OID Encoding
457    ///
458    /// Empty OIDs are encoded as zero bytes (empty content). Note that net-snmp
459    /// encodes empty OIDs as `[0x00]` (single zero byte). This difference is
460    /// unlikely to matter in practice since empty OIDs are rarely used in SNMP.
461    ///
462    /// # Validation
463    ///
464    /// This method does not validate arc constraints. Use [`to_ber_checked()`](Self::to_ber_checked)
465    /// for validation, or call [`validate()`](Self::validate) first.
466    #[must_use]
467    pub fn to_ber(&self) -> Vec<u8> {
468        self.to_ber_smallvec().to_vec()
469    }
470
471    /// Encode to BER format with validation.
472    ///
473    /// Returns an error if the OID has invalid arcs per X.690 Section 8.19.4.
474    pub fn to_ber_checked(&self) -> Result<Vec<u8>> {
475        self.validate()?;
476        Ok(self.to_ber())
477    }
478
479    /// Returns the BER content size (excluding tag and length bytes).
480    pub(crate) fn ber_content_size(&self) -> usize {
481        use crate::ber::base128_len;
482
483        if self.arcs.is_empty() {
484            return 0;
485        }
486
487        let mut len = 0;
488
489        // First subidentifier (arc1*40 + arc2)
490        len += base128_len(first_subidentifier(&self.arcs));
491
492        // Remaining arcs
493        for &arc in self.arcs.iter().skip(2) {
494            len += base128_len(arc);
495        }
496
497        len
498    }
499
500    /// Returns the total BER-encoded size (tag + length + content).
501    pub(crate) fn ber_encoded_size(&self) -> usize {
502        use crate::ber::length_encoded_len;
503
504        let content_len = self.ber_content_size();
505        1 + length_encoded_len(content_len) + content_len
506    }
507
508    /// Decode from BER format.
509    ///
510    /// Enforces [`MAX_OID_LEN`] limit per RFC 2578 Section 3.5.
511    pub fn from_ber(data: &[u8]) -> Result<Self> {
512        if data.is_empty() {
513            return Ok(Self::empty());
514        }
515
516        let mut arcs = SmallVec::new();
517
518        // Decode first subidentifier (which encodes arc1*40 + arc2)
519        // This may be multi-byte for large arc2 values (when arc1=2)
520        let (first_subid, consumed) = decode_subidentifier(data)?;
521
522        // Decode first two arcs from the first subidentifier
523        if first_subid < 40 {
524            arcs.push(0);
525            arcs.push(first_subid);
526        } else if first_subid < 80 {
527            arcs.push(1);
528            arcs.push(first_subid - 40);
529        } else {
530            arcs.push(2);
531            arcs.push(first_subid - 80);
532        }
533
534        // Decode remaining arcs
535        let mut i = consumed;
536        while i < data.len() {
537            let (arc, bytes_consumed) = decode_subidentifier(&data[i..])?;
538            arcs.push(arc);
539            i += bytes_consumed;
540
541            // RFC 2578 Section 3.5: "at most 128 sub-identifiers in a value"
542            if arcs.len() > MAX_OID_LEN {
543                tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::OidTooLong { count: arcs.len(), max: MAX_OID_LEN } }, "OID exceeds maximum arc count");
544                return Err(Error::MalformedResponse {
545                    target: UNKNOWN_TARGET,
546                }
547                .boxed());
548            }
549        }
550
551        Ok(Self { arcs })
552    }
553}
554
555/// Compute the first OID subidentifier value from an arc slice.
556///
557/// Per X.690 Section 8.19: the first two arcs are encoded as `arc1 * 40 + arc2`.
558/// If there is only one arc, it is encoded as `arc1 * 40`.
559#[inline]
560fn first_subidentifier(arcs: &SmallVec<[u32; 16]>) -> u32 {
561    if arcs.len() >= 2 {
562        arcs[0] * 40 + arcs[1]
563    } else {
564        arcs[0] * 40
565    }
566}
567
568/// Encode a subidentifier in base-128 variable length into a `SmallVec`.
569#[inline]
570fn encode_subidentifier_smallvec(bytes: &mut SmallVec<[u8; 64]>, value: u32) {
571    if value == 0 {
572        bytes.push(0);
573        return;
574    }
575
576    // Count how many 7-bit groups we need
577    let mut temp = value;
578    let mut count = 0;
579    while temp > 0 {
580        count += 1;
581        temp >>= 7;
582    }
583
584    // Encode from MSB to LSB
585    for i in (0..count).rev() {
586        let mut byte = ((value >> (i * 7)) & 0x7F) as u8;
587        if i > 0 {
588            byte |= 0x80; // Continuation bit
589        }
590        bytes.push(byte);
591    }
592}
593
594/// Decode a subidentifier, returning (value, `bytes_consumed`).
595fn decode_subidentifier(data: &[u8]) -> Result<(u32, usize)> {
596    let mut value: u32 = 0;
597    let mut i = 0;
598
599    loop {
600        if i >= data.len() {
601            tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::TruncatedData }, "unexpected end of data in OID subidentifier");
602            return Err(Error::MalformedResponse {
603                target: UNKNOWN_TARGET,
604            }
605            .boxed());
606        }
607
608        let byte = data[i];
609        i += 1;
610
611        // Check for overflow before shifting
612        if value > (u32::MAX >> 7) {
613            tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::IntegerOverflow }, "OID subidentifier overflow");
614            return Err(Error::MalformedResponse {
615                target: UNKNOWN_TARGET,
616            }
617            .boxed());
618        }
619
620        value = (value << 7) | u32::from(byte & 0x7F);
621
622        if byte & 0x80 == 0 {
623            // Last byte
624            break;
625        }
626    }
627
628    Ok((value, i))
629}
630
631impl fmt::Debug for Oid {
632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633        write!(f, "Oid({self})")
634    }
635}
636
637impl fmt::Display for Oid {
638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639        let mut first = true;
640        for arc in &self.arcs {
641            if !first {
642                write!(f, ".")?;
643            }
644            write!(f, "{arc}")?;
645            first = false;
646        }
647        Ok(())
648    }
649}
650
651impl std::str::FromStr for Oid {
652    type Err = Box<crate::error::Error>;
653
654    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
655        Self::parse(s)
656    }
657}
658
659impl From<&[u32]> for Oid {
660    fn from(arcs: &[u32]) -> Self {
661        Self::from_slice(arcs)
662    }
663}
664
665impl<const N: usize> From<[u32; N]> for Oid {
666    fn from(arcs: [u32; N]) -> Self {
667        Self::new(arcs)
668    }
669}
670
671impl From<Vec<u32>> for Oid {
672    fn from(arcs: Vec<u32>) -> Self {
673        Self {
674            arcs: SmallVec::from_vec(arcs),
675        }
676    }
677}
678
679impl AsRef<[u32]> for Oid {
680    fn as_ref(&self) -> &[u32] {
681        self.arcs()
682    }
683}
684
685impl<'a> IntoIterator for &'a Oid {
686    type Item = &'a u32;
687    type IntoIter = std::slice::Iter<'a, u32>;
688
689    fn into_iter(self) -> Self::IntoIter {
690        self.arcs().iter()
691    }
692}
693
694impl IntoIterator for Oid {
695    type Item = u32;
696    type IntoIter = smallvec::IntoIter<[u32; 16]>;
697
698    fn into_iter(self) -> Self::IntoIter {
699        self.arcs.into_iter()
700    }
701}
702
703impl PartialOrd for Oid {
704    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
705        Some(self.cmp(other))
706    }
707}
708
709impl Ord for Oid {
710    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
711        self.arcs.cmp(&other.arcs)
712    }
713}
714
715/// Macro to create an OID at compile time.
716///
717/// This is the preferred way to create OID constants since it's concise
718/// and avoids parsing overhead.
719///
720/// # Examples
721///
722/// ```
723/// use async_snmp::oid;
724///
725/// // Create an OID for sysDescr.0
726/// let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
727/// assert_eq!(sys_descr.to_string(), "1.3.6.1.2.1.1.1.0");
728///
729/// // Trailing commas are allowed
730/// let sys_name = oid!(1, 3, 6, 1, 2, 1, 1, 5, 0,);
731///
732/// // Can use in const contexts (via from_slice)
733/// let interfaces = oid!(1, 3, 6, 1, 2, 1, 2);
734/// assert!(sys_descr.starts_with(&oid!(1, 3, 6, 1, 2, 1, 1)));
735/// ```
736#[macro_export]
737macro_rules! oid {
738    ($($arc:expr),* $(,)?) => {
739        $crate::oid::Oid::from_slice(&[$($arc),*])
740    };
741}
742
743// ========================================================================
744// mib-rs OID conversions (feature = "mib")
745// ========================================================================
746
747#[cfg(feature = "mib")]
748impl From<&mib_rs::Oid> for Oid {
749    fn from(oid: &mib_rs::Oid) -> Self {
750        Oid::from_slice(oid.as_ref())
751    }
752}
753
754#[cfg(feature = "mib")]
755impl From<mib_rs::Oid> for Oid {
756    fn from(oid: mib_rs::Oid) -> Self {
757        Oid::from_slice(oid.as_ref())
758    }
759}
760
761#[cfg(feature = "mib")]
762impl Oid {
763    /// Convert to a mib-rs OID.
764    ///
765    /// This is a method rather than a `From` impl because the orphan rule
766    /// prevents implementing `From<&Oid> for mib_rs::Oid` (foreign trait
767    /// for foreign type).
768    pub fn to_mib_oid(&self) -> mib_rs::Oid {
769        mib_rs::Oid::from(self.arcs())
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_parse() {
779        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
780        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1, 1, 1, 0]);
781    }
782
783    #[test]
784    fn test_parse_leading_dot() {
785        let oid = Oid::parse(".1.3.6").unwrap();
786        assert_eq!(oid.arcs(), &[1, 3, 6]);
787
788        let oid = Oid::parse(".1.3.6.1.2.1").unwrap();
789        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1]);
790
791        // Leading dot on single arc
792        let oid = Oid::parse(".0").unwrap();
793        assert_eq!(oid.arcs(), &[0]);
794
795        // Just a dot yields empty OID
796        let oid = Oid::parse(".").unwrap();
797        assert!(oid.is_empty());
798    }
799
800    #[test]
801    fn test_parse_rejects_empty_components() {
802        assert!(Oid::parse("1.3.6.").is_err()); // Trailing dot
803        assert!(Oid::parse("1..3.6").is_err()); // Double dot
804        assert!(Oid::parse("..1.3").is_err()); // Double leading dot
805        assert!(Oid::parse("...").is_err()); // All dots
806    }
807
808    #[test]
809    fn test_display() {
810        let oid = Oid::from_slice(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
811        assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.1.0");
812    }
813
814    #[test]
815    fn test_starts_with() {
816        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
817        let prefix = Oid::parse("1.3.6.1").unwrap();
818        assert!(oid.starts_with(&prefix));
819        assert!(!prefix.starts_with(&oid));
820    }
821
822    #[test]
823    fn test_ber_roundtrip() {
824        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
825        let ber = oid.to_ber();
826        let decoded = Oid::from_ber(&ber).unwrap();
827        assert_eq!(oid, decoded);
828    }
829
830    #[test]
831    fn test_ber_encoding() {
832        // 1.3.6.1 encodes as: (1*40+3)=43, 6, 1 = [0x2B, 0x06, 0x01]
833        let oid = Oid::parse("1.3.6.1").unwrap();
834        assert_eq!(oid.to_ber(), vec![0x2B, 0x06, 0x01]);
835    }
836
837    #[test]
838    fn test_macro() {
839        let oid = oid!(1, 3, 6, 1);
840        assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
841    }
842
843    #[test]
844    fn from_vec_u32() {
845        let v = vec![1, 3, 6, 1, 2, 1];
846        let oid = Oid::from(v);
847        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1]);
848    }
849
850    #[test]
851    fn oid_as_ref() {
852        let oid = Oid::new([1, 3, 6, 1]);
853        let slice: &[u32] = oid.as_ref();
854        assert_eq!(slice, &[1, 3, 6, 1]);
855    }
856
857    // AUDIT-001: Test arc validation
858    // X.690 Section 8.19.4: arc1 must be 0, 1, or 2; arc2 must be <= 39 when arc1 < 2
859    #[test]
860    fn test_validate_arc1_must_be_0_1_or_2() {
861        // arc1 = 3 is invalid
862        let oid = Oid::from_slice(&[3, 0]);
863        let result = oid.validate();
864        assert!(result.is_err(), "arc1=3 should be invalid");
865    }
866
867    #[test]
868    fn test_validate_arc2_limit_when_arc1_is_0() {
869        // arc1 = 0, arc2 = 40 is invalid (max is 39)
870        let oid = Oid::from_slice(&[0, 40]);
871        let result = oid.validate();
872        assert!(result.is_err(), "arc2=40 with arc1=0 should be invalid");
873
874        // arc1 = 0, arc2 = 39 is valid
875        let oid = Oid::from_slice(&[0, 39]);
876        assert!(
877            oid.validate().is_ok(),
878            "arc2=39 with arc1=0 should be valid"
879        );
880    }
881
882    #[test]
883    fn test_validate_arc2_limit_when_arc1_is_1() {
884        // arc1 = 1, arc2 = 40 is invalid
885        let oid = Oid::from_slice(&[1, 40]);
886        let result = oid.validate();
887        assert!(result.is_err(), "arc2=40 with arc1=1 should be invalid");
888
889        // arc1 = 1, arc2 = 39 is valid
890        let oid = Oid::from_slice(&[1, 39]);
891        assert!(
892            oid.validate().is_ok(),
893            "arc2=39 with arc1=1 should be valid"
894        );
895    }
896
897    #[test]
898    fn test_validate_arc2_no_limit_when_arc1_is_2() {
899        // arc1 = 2, arc2 can be anything (e.g., 999)
900        let oid = Oid::from_slice(&[2, 999]);
901        assert!(
902            oid.validate().is_ok(),
903            "arc2=999 with arc1=2 should be valid"
904        );
905    }
906
907    #[test]
908    fn test_to_ber_validates_arcs() {
909        // Invalid OID should return error from to_ber_checked
910        let oid = Oid::from_slice(&[3, 0]); // arc1=3 is invalid
911        let result = oid.to_ber_checked();
912        assert!(
913            result.is_err(),
914            "to_ber_checked should fail for invalid arc1"
915        );
916    }
917
918    // AUDIT-002: Test first subidentifier encoding for large arc2 values
919    // X.690 Section 8.19 example: OID {2 999 3} has first subidentifier = 1079
920    #[test]
921    fn test_ber_encoding_large_arc2() {
922        // OID 2.999.3: first subid = 2*40 + 999 = 1079 = 0x437
923        // 1079 in base-128: 0x88 0x37 (continuation bit set on first byte)
924        let oid = Oid::from_slice(&[2, 999, 3]);
925        let ber = oid.to_ber();
926        // First subidentifier 1079 = 0b10000110111 = 7 bits: 0b0110111 (0x37), 7 bits: 0b0001000 (0x08)
927        // In base-128: (1079 >> 7) = 8, (1079 & 0x7F) = 55
928        // So: 0x88 (8 | 0x80), 0x37 (55)
929        assert_eq!(
930            ber[0], 0x88,
931            "first byte should be 0x88 (8 with continuation)"
932        );
933        assert_eq!(
934            ber[1], 0x37,
935            "second byte should be 0x37 (55, no continuation)"
936        );
937        assert_eq!(ber[2], 0x03, "third byte should be 0x03 (arc 3)");
938        assert_eq!(ber.len(), 3, "OID 2.999.3 should encode to 3 bytes");
939    }
940
941    #[test]
942    fn test_ber_roundtrip_large_arc2() {
943        // Ensure roundtrip works for OID with large arc2
944        let oid = Oid::from_slice(&[2, 999, 3]);
945        let ber = oid.to_ber();
946        let decoded = Oid::from_ber(&ber).unwrap();
947        assert_eq!(oid, decoded, "roundtrip should preserve OID 2.999.3");
948    }
949
950    #[test]
951    fn test_ber_encoding_arc2_equals_80() {
952        // Edge case: arc1=2, arc2=0 gives first subid = 80, which is exactly 1 byte
953        let oid = Oid::from_slice(&[2, 0]);
954        let ber = oid.to_ber();
955        assert_eq!(ber, vec![80], "OID 2.0 should encode to [80]");
956    }
957
958    #[test]
959    fn test_ber_encoding_arc2_equals_127() {
960        // arc1=2, arc2=47 gives first subid = 127, still fits in 1 byte
961        let oid = Oid::from_slice(&[2, 47]);
962        let ber = oid.to_ber();
963        assert_eq!(ber, vec![127], "OID 2.47 should encode to [127]");
964    }
965
966    #[test]
967    fn test_ber_encoding_arc2_equals_128_needs_2_bytes() {
968        // arc1=2, arc2=48 gives first subid = 128, needs 2 bytes in base-128
969        let oid = Oid::from_slice(&[2, 48]);
970        let ber = oid.to_ber();
971        // 128 = 0x80 = base-128: 0x81 0x00
972        assert_eq!(
973            ber,
974            vec![0x81, 0x00],
975            "OID 2.48 should encode to [0x81, 0x00]"
976        );
977    }
978
979    #[test]
980    fn test_from_ber_zero_length() {
981        // A zero-length OID content (BER encoding 06 00) is accepted and returns
982        // an empty OID. This matches net-snmp's behavior in snmplib/asn1.c which
983        // treats the same encoding as 0.0 rather than rejecting it. Our empty OID
984        // differs from net-snmp's 0.0 - net-snmp encodes empty OIDs as [0x00]
985        // (a single zero byte yielding 0.0), while we produce truly zero arcs.
986        // Devices send this when returning endOfMibView with a malformed zero-length
987        // OID instead of echoing back the requested OID (RFC 3416 violation).
988        let result = Oid::from_ber(&[]);
989        assert!(result.is_ok(), "zero-length OID content should be accepted");
990        assert!(result.unwrap().is_empty());
991    }
992
993    #[test]
994    fn test_oid_non_minimal_subidentifier() {
995        // Non-minimal subidentifier encoding with leading 0x80 bytes should be accepted
996        // 0x80 0x01 should decode as 1 (non-minimal: minimal would be just 0x01)
997        // OID: 1.3 followed by arc 1 encoded as 0x80 0x01
998        let result = Oid::from_ber(&[0x2B, 0x80, 0x01]);
999        assert!(
1000            result.is_ok(),
1001            "should accept non-minimal subidentifier 0x80 0x01"
1002        );
1003        let oid = result.unwrap();
1004        assert_eq!(oid.arcs(), &[1, 3, 1]);
1005
1006        // 0x80 0x80 0x01 should decode as 1 (two leading 0x80 bytes)
1007        let result = Oid::from_ber(&[0x2B, 0x80, 0x80, 0x01]);
1008        assert!(
1009            result.is_ok(),
1010            "should accept non-minimal subidentifier 0x80 0x80 0x01"
1011        );
1012        let oid = result.unwrap();
1013        assert_eq!(oid.arcs(), &[1, 3, 1]);
1014
1015        // 0x80 0x00 should decode as 0 (non-minimal zero)
1016        let result = Oid::from_ber(&[0x2B, 0x80, 0x00]);
1017        assert!(
1018            result.is_ok(),
1019            "should accept non-minimal subidentifier 0x80 0x00"
1020        );
1021        let oid = result.unwrap();
1022        assert_eq!(oid.arcs(), &[1, 3, 0]);
1023    }
1024
1025    // Tests for MAX_OID_LEN validation
1026    #[test]
1027    fn test_validate_length_within_limit() {
1028        // OID with MAX_OID_LEN arcs should be valid
1029        let arcs: Vec<u32> = (0..MAX_OID_LEN as u32).collect();
1030        let oid = Oid::new(arcs);
1031        assert!(
1032            oid.validate_length().is_ok(),
1033            "OID with exactly MAX_OID_LEN arcs should be valid"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_validate_length_exceeds_limit() {
1039        // OID with more than MAX_OID_LEN arcs should fail
1040        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
1041        let oid = Oid::new(arcs);
1042        let result = oid.validate_length();
1043        assert!(
1044            result.is_err(),
1045            "OID exceeding MAX_OID_LEN should fail validation"
1046        );
1047    }
1048
1049    #[test]
1050    fn test_validate_all_combines_checks() {
1051        // Valid OID
1052        let oid = Oid::from_slice(&[1, 3, 6, 1]);
1053        assert!(oid.validate_all().is_ok());
1054
1055        // Invalid arc1 (fails validate)
1056        let oid = Oid::from_slice(&[3, 0]);
1057        assert!(oid.validate_all().is_err());
1058
1059        // Too many arcs (fails validate_length)
1060        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
1061        let oid = Oid::new(arcs);
1062        assert!(oid.validate_all().is_err());
1063    }
1064
1065    #[test]
1066    fn test_oid_fromstr() {
1067        // Test basic parsing via FromStr trait
1068        let oid: Oid = "1.3.6.1.2.1.1.1.0".parse().unwrap();
1069        assert_eq!(oid, oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
1070
1071        // Test empty OID
1072        let empty: Oid = "".parse().unwrap();
1073        assert!(empty.is_empty());
1074
1075        // Test single arc
1076        let single: Oid = "1".parse().unwrap();
1077        assert_eq!(single.arcs(), &[1]);
1078
1079        // Test roundtrip Display -> FromStr
1080        let original = oid!(1, 3, 6, 1, 4, 1, 9, 9, 42);
1081        let displayed = original.to_string();
1082        let parsed: Oid = displayed.parse().unwrap();
1083        assert_eq!(original, parsed);
1084    }
1085
1086    #[test]
1087    fn test_oid_fromstr_invalid() {
1088        // Invalid arc value
1089        assert!("1.3.abc.1".parse::<Oid>().is_err());
1090
1091        // Negative number (parsed as invalid)
1092        assert!("1.3.-6.1".parse::<Oid>().is_err());
1093    }
1094
1095    // Test for first subidentifier overflow (arc1*40 + arc2 must fit in u32)
1096    // When arc1=2, arc2 cannot exceed u32::MAX - 80
1097    #[test]
1098    fn test_validate_arc2_overflow_when_arc1_is_2() {
1099        // Maximum valid arc2 when arc1=2: u32::MAX - 80 = 4294967215
1100        let max_valid_arc2 = u32::MAX - 80;
1101        let oid = Oid::from_slice(&[2, max_valid_arc2]);
1102        assert!(
1103            oid.validate().is_ok(),
1104            "arc2={max_valid_arc2} with arc1=2 should be valid (max that fits)"
1105        );
1106
1107        // One more than max should fail validation
1108        let overflow_arc2 = u32::MAX - 79; // 2*40 + this = u32::MAX + 1
1109        let oid = Oid::from_slice(&[2, overflow_arc2]);
1110        assert!(
1111            oid.validate().is_err(),
1112            "arc2={overflow_arc2} with arc1=2 should be invalid (would overflow first subidentifier)"
1113        );
1114
1115        // Also test arc2 = u32::MAX should definitely fail
1116        let oid = Oid::from_slice(&[2, u32::MAX]);
1117        assert!(
1118            oid.validate().is_err(),
1119            "arc2=u32::MAX with arc1=2 should be invalid"
1120        );
1121    }
1122
1123    #[test]
1124    fn test_to_ber_checked_rejects_overflow() {
1125        // Encoding an OID that would overflow should fail via to_ber_checked
1126        let oid = Oid::from_slice(&[2, u32::MAX]);
1127        let result = oid.to_ber_checked();
1128        assert!(
1129            result.is_err(),
1130            "to_ber_checked should reject OID that would overflow"
1131        );
1132    }
1133
1134    #[test]
1135    fn test_from_ber_enforces_max_oid_len() {
1136        // Create BER data for an OID with more than MAX_OID_LEN arcs
1137        // OID encoding: first subid encodes arc1*40+arc2, then each subsequent arc
1138        // First subid gives us 2 arcs (e.g., 1 and 3), so we need MAX_OID_LEN - 2
1139        // additional arcs to hit exactly MAX_OID_LEN.
1140
1141        // Build OID at exactly MAX_OID_LEN: 1.3 followed by (MAX_OID_LEN - 2) arcs of value 1
1142        let mut ber_at_limit = vec![0x2B]; // First subid = 1*40 + 3 = 43 (encodes arc1=1, arc2=3)
1143        ber_at_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 2));
1144
1145        let result = Oid::from_ber(&ber_at_limit);
1146        assert!(
1147            result.is_ok(),
1148            "OID with exactly MAX_OID_LEN arcs should decode successfully"
1149        );
1150        assert_eq!(result.unwrap().len(), MAX_OID_LEN);
1151
1152        // Now one more arc should exceed the limit
1153        let mut ber_over_limit = vec![0x2B]; // arc1=1, arc2=3
1154        ber_over_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 1));
1155
1156        let result = Oid::from_ber(&ber_over_limit);
1157        assert!(
1158            result.is_err(),
1159            "OID exceeding MAX_OID_LEN should fail to decode"
1160        );
1161    }
1162
1163    // ========================================================================
1164    // Suffix Extraction Tests
1165    // ========================================================================
1166
1167    #[test]
1168    fn test_strip_prefix() {
1169        let if_descr_5 = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 5);
1170        let if_descr = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
1171
1172        // Extract the index
1173        let index = if_descr_5.strip_prefix(&if_descr).unwrap();
1174        assert_eq!(index.arcs(), &[5]);
1175
1176        // Non-matching prefix returns None
1177        let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1);
1178        assert!(if_descr_5.strip_prefix(&sys_descr).is_none());
1179
1180        // Equal OIDs return empty
1181        let same = oid!(1, 3, 6);
1182        assert!(same.strip_prefix(&same).unwrap().is_empty());
1183
1184        // Empty prefix returns self
1185        let any = oid!(1, 2, 3);
1186        assert_eq!(any.strip_prefix(&Oid::empty()).unwrap(), any);
1187
1188        // Multi-arc index
1189        let composite = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1190        let column = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2);
1191        let idx = composite.strip_prefix(&column).unwrap();
1192        assert_eq!(idx.arcs(), &[1, 192, 168, 1, 100]);
1193    }
1194
1195    #[test]
1196    fn test_suffix() {
1197        let oid = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1198
1199        // Get the 5-arc index
1200        assert_eq!(oid.suffix(5), Some(&[1, 192, 168, 1, 100][..]));
1201
1202        // Get just the last arc
1203        assert_eq!(oid.suffix(1), Some(&[100][..]));
1204
1205        // suffix(0) returns empty slice
1206        assert_eq!(oid.suffix(0), Some(&[][..]));
1207
1208        // Exact length returns full OID
1209        assert_eq!(oid.suffix(15), Some(oid.arcs()));
1210
1211        // Too large returns None
1212        assert!(oid.suffix(16).is_none());
1213        assert!(oid.suffix(100).is_none());
1214
1215        // Empty OID
1216        let empty = Oid::empty();
1217        assert_eq!(empty.suffix(0), Some(&[][..]));
1218        assert!(empty.suffix(1).is_none());
1219    }
1220
1221    #[test]
1222    fn oid_into_iter_ref() {
1223        let oid = Oid::new([1, 3, 6]);
1224        let arcs: Vec<&u32> = (&oid).into_iter().collect();
1225        assert_eq!(arcs, vec![&1, &3, &6]);
1226    }
1227
1228    #[test]
1229    fn oid_into_iter_owned() {
1230        let oid = Oid::new([1, 3, 6]);
1231        let arcs: Vec<u32> = oid.into_iter().collect();
1232        assert_eq!(arcs, vec![1, 3, 6]);
1233    }
1234
1235    #[test]
1236    fn oid_for_loop() {
1237        let oid = Oid::new([1, 3, 6]);
1238        let mut sum = 0u32;
1239        for arc in &oid {
1240            sum += arc;
1241        }
1242        assert_eq!(sum, 10);
1243    }
1244}