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#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_parse() {
708        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
709        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1, 1, 1, 0]);
710    }
711
712    #[test]
713    fn test_display() {
714        let oid = Oid::from_slice(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
715        assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.1.0");
716    }
717
718    #[test]
719    fn test_starts_with() {
720        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
721        let prefix = Oid::parse("1.3.6.1").unwrap();
722        assert!(oid.starts_with(&prefix));
723        assert!(!prefix.starts_with(&oid));
724    }
725
726    #[test]
727    fn test_ber_roundtrip() {
728        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
729        let ber = oid.to_ber();
730        let decoded = Oid::from_ber(&ber).unwrap();
731        assert_eq!(oid, decoded);
732    }
733
734    #[test]
735    fn test_ber_encoding() {
736        // 1.3.6.1 encodes as: (1*40+3)=43, 6, 1 = [0x2B, 0x06, 0x01]
737        let oid = Oid::parse("1.3.6.1").unwrap();
738        assert_eq!(oid.to_ber(), vec![0x2B, 0x06, 0x01]);
739    }
740
741    #[test]
742    fn test_macro() {
743        let oid = oid!(1, 3, 6, 1);
744        assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
745    }
746
747    // AUDIT-001: Test arc validation
748    // X.690 Section 8.19.4: arc1 must be 0, 1, or 2; arc2 must be <= 39 when arc1 < 2
749    #[test]
750    fn test_validate_arc1_must_be_0_1_or_2() {
751        // arc1 = 3 is invalid
752        let oid = Oid::from_slice(&[3, 0]);
753        let result = oid.validate();
754        assert!(result.is_err(), "arc1=3 should be invalid");
755    }
756
757    #[test]
758    fn test_validate_arc2_limit_when_arc1_is_0() {
759        // arc1 = 0, arc2 = 40 is invalid (max is 39)
760        let oid = Oid::from_slice(&[0, 40]);
761        let result = oid.validate();
762        assert!(result.is_err(), "arc2=40 with arc1=0 should be invalid");
763
764        // arc1 = 0, arc2 = 39 is valid
765        let oid = Oid::from_slice(&[0, 39]);
766        assert!(
767            oid.validate().is_ok(),
768            "arc2=39 with arc1=0 should be valid"
769        );
770    }
771
772    #[test]
773    fn test_validate_arc2_limit_when_arc1_is_1() {
774        // arc1 = 1, arc2 = 40 is invalid
775        let oid = Oid::from_slice(&[1, 40]);
776        let result = oid.validate();
777        assert!(result.is_err(), "arc2=40 with arc1=1 should be invalid");
778
779        // arc1 = 1, arc2 = 39 is valid
780        let oid = Oid::from_slice(&[1, 39]);
781        assert!(
782            oid.validate().is_ok(),
783            "arc2=39 with arc1=1 should be valid"
784        );
785    }
786
787    #[test]
788    fn test_validate_arc2_no_limit_when_arc1_is_2() {
789        // arc1 = 2, arc2 can be anything (e.g., 999)
790        let oid = Oid::from_slice(&[2, 999]);
791        assert!(
792            oid.validate().is_ok(),
793            "arc2=999 with arc1=2 should be valid"
794        );
795    }
796
797    #[test]
798    fn test_to_ber_validates_arcs() {
799        // Invalid OID should return error from to_ber_checked
800        let oid = Oid::from_slice(&[3, 0]); // arc1=3 is invalid
801        let result = oid.to_ber_checked();
802        assert!(
803            result.is_err(),
804            "to_ber_checked should fail for invalid arc1"
805        );
806    }
807
808    // AUDIT-002: Test first subidentifier encoding for large arc2 values
809    // X.690 Section 8.19 example: OID {2 999 3} has first subidentifier = 1079
810    #[test]
811    fn test_ber_encoding_large_arc2() {
812        // OID 2.999.3: first subid = 2*40 + 999 = 1079 = 0x437
813        // 1079 in base-128: 0x88 0x37 (continuation bit set on first byte)
814        let oid = Oid::from_slice(&[2, 999, 3]);
815        let ber = oid.to_ber();
816        // First subidentifier 1079 = 0b10000110111 = 7 bits: 0b0110111 (0x37), 7 bits: 0b0001000 (0x08)
817        // In base-128: (1079 >> 7) = 8, (1079 & 0x7F) = 55
818        // So: 0x88 (8 | 0x80), 0x37 (55)
819        assert_eq!(
820            ber[0], 0x88,
821            "first byte should be 0x88 (8 with continuation)"
822        );
823        assert_eq!(
824            ber[1], 0x37,
825            "second byte should be 0x37 (55, no continuation)"
826        );
827        assert_eq!(ber[2], 0x03, "third byte should be 0x03 (arc 3)");
828        assert_eq!(ber.len(), 3, "OID 2.999.3 should encode to 3 bytes");
829    }
830
831    #[test]
832    fn test_ber_roundtrip_large_arc2() {
833        // Ensure roundtrip works for OID with large arc2
834        let oid = Oid::from_slice(&[2, 999, 3]);
835        let ber = oid.to_ber();
836        let decoded = Oid::from_ber(&ber).unwrap();
837        assert_eq!(oid, decoded, "roundtrip should preserve OID 2.999.3");
838    }
839
840    #[test]
841    fn test_ber_encoding_arc2_equals_80() {
842        // Edge case: arc1=2, arc2=0 gives first subid = 80, which is exactly 1 byte
843        let oid = Oid::from_slice(&[2, 0]);
844        let ber = oid.to_ber();
845        assert_eq!(ber, vec![80], "OID 2.0 should encode to [80]");
846    }
847
848    #[test]
849    fn test_ber_encoding_arc2_equals_127() {
850        // arc1=2, arc2=47 gives first subid = 127, still fits in 1 byte
851        let oid = Oid::from_slice(&[2, 47]);
852        let ber = oid.to_ber();
853        assert_eq!(ber, vec![127], "OID 2.47 should encode to [127]");
854    }
855
856    #[test]
857    fn test_ber_encoding_arc2_equals_128_needs_2_bytes() {
858        // arc1=2, arc2=48 gives first subid = 128, needs 2 bytes in base-128
859        let oid = Oid::from_slice(&[2, 48]);
860        let ber = oid.to_ber();
861        // 128 = 0x80 = base-128: 0x81 0x00
862        assert_eq!(
863            ber,
864            vec![0x81, 0x00],
865            "OID 2.48 should encode to [0x81, 0x00]"
866        );
867    }
868
869    #[test]
870    fn test_oid_non_minimal_subidentifier() {
871        // Non-minimal subidentifier encoding with leading 0x80 bytes should be accepted
872        // 0x80 0x01 should decode as 1 (non-minimal: minimal would be just 0x01)
873        // OID: 1.3 followed by arc 1 encoded as 0x80 0x01
874        let result = Oid::from_ber(&[0x2B, 0x80, 0x01]);
875        assert!(
876            result.is_ok(),
877            "should accept non-minimal subidentifier 0x80 0x01"
878        );
879        let oid = result.unwrap();
880        assert_eq!(oid.arcs(), &[1, 3, 1]);
881
882        // 0x80 0x80 0x01 should decode as 1 (two leading 0x80 bytes)
883        let result = Oid::from_ber(&[0x2B, 0x80, 0x80, 0x01]);
884        assert!(
885            result.is_ok(),
886            "should accept non-minimal subidentifier 0x80 0x80 0x01"
887        );
888        let oid = result.unwrap();
889        assert_eq!(oid.arcs(), &[1, 3, 1]);
890
891        // 0x80 0x00 should decode as 0 (non-minimal zero)
892        let result = Oid::from_ber(&[0x2B, 0x80, 0x00]);
893        assert!(
894            result.is_ok(),
895            "should accept non-minimal subidentifier 0x80 0x00"
896        );
897        let oid = result.unwrap();
898        assert_eq!(oid.arcs(), &[1, 3, 0]);
899    }
900
901    // Tests for MAX_OID_LEN validation
902    #[test]
903    fn test_validate_length_within_limit() {
904        // OID with MAX_OID_LEN arcs should be valid
905        let arcs: Vec<u32> = (0..MAX_OID_LEN as u32).collect();
906        let oid = Oid::new(arcs);
907        assert!(
908            oid.validate_length().is_ok(),
909            "OID with exactly MAX_OID_LEN arcs should be valid"
910        );
911    }
912
913    #[test]
914    fn test_validate_length_exceeds_limit() {
915        // OID with more than MAX_OID_LEN arcs should fail
916        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
917        let oid = Oid::new(arcs);
918        let result = oid.validate_length();
919        assert!(
920            result.is_err(),
921            "OID exceeding MAX_OID_LEN should fail validation"
922        );
923    }
924
925    #[test]
926    fn test_validate_all_combines_checks() {
927        // Valid OID
928        let oid = Oid::from_slice(&[1, 3, 6, 1]);
929        assert!(oid.validate_all().is_ok());
930
931        // Invalid arc1 (fails validate)
932        let oid = Oid::from_slice(&[3, 0]);
933        assert!(oid.validate_all().is_err());
934
935        // Too many arcs (fails validate_length)
936        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
937        let oid = Oid::new(arcs);
938        assert!(oid.validate_all().is_err());
939    }
940
941    #[test]
942    fn test_oid_fromstr() {
943        // Test basic parsing via FromStr trait
944        let oid: Oid = "1.3.6.1.2.1.1.1.0".parse().unwrap();
945        assert_eq!(oid, oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
946
947        // Test empty OID
948        let empty: Oid = "".parse().unwrap();
949        assert!(empty.is_empty());
950
951        // Test single arc
952        let single: Oid = "1".parse().unwrap();
953        assert_eq!(single.arcs(), &[1]);
954
955        // Test roundtrip Display -> FromStr
956        let original = oid!(1, 3, 6, 1, 4, 1, 9, 9, 42);
957        let displayed = original.to_string();
958        let parsed: Oid = displayed.parse().unwrap();
959        assert_eq!(original, parsed);
960    }
961
962    #[test]
963    fn test_oid_fromstr_invalid() {
964        // Invalid arc value
965        assert!("1.3.abc.1".parse::<Oid>().is_err());
966
967        // Negative number (parsed as invalid)
968        assert!("1.3.-6.1".parse::<Oid>().is_err());
969    }
970
971    // Test for first subidentifier overflow (arc1*40 + arc2 must fit in u32)
972    // When arc1=2, arc2 cannot exceed u32::MAX - 80
973    #[test]
974    fn test_validate_arc2_overflow_when_arc1_is_2() {
975        // Maximum valid arc2 when arc1=2: u32::MAX - 80 = 4294967215
976        let max_valid_arc2 = u32::MAX - 80;
977        let oid = Oid::from_slice(&[2, max_valid_arc2]);
978        assert!(
979            oid.validate().is_ok(),
980            "arc2={} with arc1=2 should be valid (max that fits)",
981            max_valid_arc2
982        );
983
984        // One more than max should fail validation
985        let overflow_arc2 = u32::MAX - 79; // 2*40 + this = u32::MAX + 1
986        let oid = Oid::from_slice(&[2, overflow_arc2]);
987        assert!(
988            oid.validate().is_err(),
989            "arc2={} with arc1=2 should be invalid (would overflow first subidentifier)",
990            overflow_arc2
991        );
992
993        // Also test arc2 = u32::MAX should definitely fail
994        let oid = Oid::from_slice(&[2, u32::MAX]);
995        assert!(
996            oid.validate().is_err(),
997            "arc2=u32::MAX with arc1=2 should be invalid"
998        );
999    }
1000
1001    #[test]
1002    fn test_to_ber_checked_rejects_overflow() {
1003        // Encoding an OID that would overflow should fail via to_ber_checked
1004        let oid = Oid::from_slice(&[2, u32::MAX]);
1005        let result = oid.to_ber_checked();
1006        assert!(
1007            result.is_err(),
1008            "to_ber_checked should reject OID that would overflow"
1009        );
1010    }
1011
1012    #[test]
1013    fn test_from_ber_enforces_max_oid_len() {
1014        // Create BER data for an OID with more than MAX_OID_LEN arcs
1015        // OID encoding: first subid encodes arc1*40+arc2, then each subsequent arc
1016        // First subid gives us 2 arcs (e.g., 1 and 3), so we need MAX_OID_LEN - 2
1017        // additional arcs to hit exactly MAX_OID_LEN.
1018
1019        // Build OID at exactly MAX_OID_LEN: 1.3 followed by (MAX_OID_LEN - 2) arcs of value 1
1020        let mut ber_at_limit = vec![0x2B]; // First subid = 1*40 + 3 = 43 (encodes arc1=1, arc2=3)
1021        ber_at_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 2));
1022
1023        let result = Oid::from_ber(&ber_at_limit);
1024        assert!(
1025            result.is_ok(),
1026            "OID with exactly MAX_OID_LEN arcs should decode successfully"
1027        );
1028        assert_eq!(result.unwrap().len(), MAX_OID_LEN);
1029
1030        // Now one more arc should exceed the limit
1031        let mut ber_over_limit = vec![0x2B]; // arc1=1, arc2=3
1032        ber_over_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 1));
1033
1034        let result = Oid::from_ber(&ber_over_limit);
1035        assert!(
1036            result.is_err(),
1037            "OID exceeding MAX_OID_LEN should fail to decode"
1038        );
1039    }
1040
1041    // ========================================================================
1042    // Suffix Extraction Tests
1043    // ========================================================================
1044
1045    #[test]
1046    fn test_strip_prefix() {
1047        let if_descr_5 = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 5);
1048        let if_descr = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
1049
1050        // Extract the index
1051        let index = if_descr_5.strip_prefix(&if_descr).unwrap();
1052        assert_eq!(index.arcs(), &[5]);
1053
1054        // Non-matching prefix returns None
1055        let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1);
1056        assert!(if_descr_5.strip_prefix(&sys_descr).is_none());
1057
1058        // Equal OIDs return empty
1059        let same = oid!(1, 3, 6);
1060        assert!(same.strip_prefix(&same).unwrap().is_empty());
1061
1062        // Empty prefix returns self
1063        let any = oid!(1, 2, 3);
1064        assert_eq!(any.strip_prefix(&Oid::empty()).unwrap(), any);
1065
1066        // Multi-arc index
1067        let composite = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1068        let column = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2);
1069        let idx = composite.strip_prefix(&column).unwrap();
1070        assert_eq!(idx.arcs(), &[1, 192, 168, 1, 100]);
1071    }
1072
1073    #[test]
1074    fn test_suffix() {
1075        let oid = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1076
1077        // Get the 5-arc index
1078        assert_eq!(oid.suffix(5), Some(&[1, 192, 168, 1, 100][..]));
1079
1080        // Get just the last arc
1081        assert_eq!(oid.suffix(1), Some(&[100][..]));
1082
1083        // suffix(0) returns empty slice
1084        assert_eq!(oid.suffix(0), Some(&[][..]));
1085
1086        // Exact length returns full OID
1087        assert_eq!(oid.suffix(15), Some(oid.arcs()));
1088
1089        // Too large returns None
1090        assert!(oid.suffix(16).is_none());
1091        assert!(oid.suffix(100).is_none());
1092
1093        // Empty OID
1094        let empty = Oid::empty();
1095        assert_eq!(empty.suffix(0), Some(&[][..]));
1096        assert!(empty.suffix(1).is_none());
1097    }
1098}