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