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        // A single-arc OID has no invertible BER encoding: X.690 8.19.4 packs the
348        // first TWO components into one subidentifier (arc1*40 + arc2), so encoding
349        // `[n]` produces subidentifier n*40, which decodes back to `[n, 0]`. This
350        // check only guards the validated paths (`validate()`/`to_ber_checked()`);
351        // the raw encode path (`to_ber()`/`VarBind` encoding) does not validate and
352        // will still emit the non-round-tripping form. Decoding never yields a
353        // single-arc OID (it always pushes two arcs, or zero for empty).
354        if self.arcs.len() == 1 {
355            return Err(Error::InvalidOid(
356                "OID must have at least two arcs to be BER-encodable".into(),
357            )
358            .boxed());
359        }
360
361        let arc1 = self.arcs[0];
362
363        // arc1 must be 0, 1, or 2
364        if arc1 > 2 {
365            return Err(Error::InvalidOid(
366                format!("first arc must be 0, 1, or 2, got {arc1}").into(),
367            )
368            .boxed());
369        }
370
371        // Validate arc2 constraints (at least two arcs exist past this point)
372        let arc2 = self.arcs[1];
373
374        // arc2 must be <= 39 when arc1 < 2
375        if arc1 < 2 && arc2 >= 40 {
376            return Err(Error::InvalidOid(
377                format!("second arc must be <= 39 when first arc is {arc1}, got {arc2}").into(),
378            )
379            .boxed());
380        }
381
382        // Check that first subidentifier (arc1*40 + arc2) won't overflow u32.
383        // Max valid arc2 = u32::MAX - arc1*40
384        let base = arc1 * 40;
385        if arc2 > u32::MAX - base {
386            return Err(
387                Error::InvalidOid("subidentifier overflow in first two arcs".into()).boxed(),
388            );
389        }
390
391        Ok(())
392    }
393
394    /// Validate that the OID doesn't exceed the maximum arc count.
395    ///
396    /// SNMP implementations commonly limit OIDs to 128 subidentifiers. This check
397    /// provides protection against `DoS` attacks from maliciously long OIDs.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use async_snmp::oid::{Oid, MAX_OID_LEN};
403    ///
404    /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
405    /// assert!(oid.validate_length().is_ok());
406    ///
407    /// // Create an OID with too many arcs
408    /// let too_long: Vec<u32> = (0..150).collect();
409    /// let long_oid = Oid::new(too_long);
410    /// assert!(long_oid.validate_length().is_err());
411    /// ```
412    pub fn validate_length(&self) -> Result<()> {
413        if self.arcs.len() > MAX_OID_LEN {
414            return Err(Error::InvalidOid(
415                format!(
416                    "OID has {} arcs, exceeds maximum {}",
417                    self.arcs.len(),
418                    MAX_OID_LEN
419                )
420                .into(),
421            )
422            .boxed());
423        }
424        Ok(())
425    }
426
427    /// Validate both arc constraints and length.
428    ///
429    /// Combines [`validate()`](Self::validate) and [`validate_length()`](Self::validate_length).
430    pub fn validate_all(&self) -> Result<()> {
431        self.validate()?;
432        self.validate_length()
433    }
434
435    /// Encode to BER format, returning bytes in a stack-allocated buffer.
436    ///
437    /// Uses `SmallVec` to avoid heap allocation for OIDs with up to ~20 arcs.
438    /// This is the optimized version used internally by encoding routines.
439    ///
440    /// OID encoding (X.690 Section 8.19):
441    /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
442    /// - Remaining arcs encoded as base-128 variable length
443    pub(crate) fn to_ber_smallvec(&self) -> SmallVec<[u8; 64]> {
444        let mut bytes = SmallVec::new();
445
446        if self.arcs.is_empty() {
447            return bytes;
448        }
449
450        // First two arcs combined into first subidentifier.
451        // Uses base-128 encoding because arc2 can be > 127 when arc1=2.
452        encode_subidentifier_smallvec(&mut bytes, first_subidentifier(&self.arcs));
453
454        // Remaining arcs
455        for &arc in self.arcs.iter().skip(2) {
456            encode_subidentifier_smallvec(&mut bytes, arc);
457        }
458
459        bytes
460    }
461
462    /// Encode to BER format.
463    ///
464    /// OID encoding (X.690 Section 8.19):
465    /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
466    /// - Remaining arcs encoded as base-128 variable length
467    ///
468    /// # Empty OID Encoding
469    ///
470    /// Empty OIDs are encoded as zero bytes (empty content). Note that net-snmp
471    /// encodes empty OIDs as `[0x00]` (single zero byte). This difference is
472    /// unlikely to matter in practice since empty OIDs are rarely used in SNMP.
473    ///
474    /// # Validation
475    ///
476    /// This method does not validate arc constraints. Use [`to_ber_checked()`](Self::to_ber_checked)
477    /// for validation, or call [`validate()`](Self::validate) first. If the first two arcs combine
478    /// (`arc1 * 40 + arc2`) to a value exceeding `u32::MAX`, this method saturates that
479    /// subidentifier to `u32::MAX` rather than panicking or wrapping; such an OID is invalid and
480    /// is rejected by `validate()`/`to_ber_checked()`.
481    #[must_use]
482    pub fn to_ber(&self) -> Vec<u8> {
483        self.to_ber_smallvec().to_vec()
484    }
485
486    /// Encode to BER format with validation.
487    ///
488    /// This is the strict, wire-safe encode entry point. It rejects OIDs that do
489    /// not have a well-formed, round-trippable BER encoding:
490    ///
491    /// - empty OIDs (no arcs): BER content would be zero bytes, which is not a
492    ///   valid OBJECT IDENTIFIER value (X.690 Section 8.19.4 requires at least one
493    ///   subidentifier);
494    /// - single-arc OIDs: rejected by [`validate()`](Self::validate) because the
495    ///   first subidentifier packs two arcs (`arc1 * 40 + arc2`) and `[n]` decodes
496    ///   back to `[n, 0]`;
497    /// - OIDs exceeding [`MAX_OID_LEN`] subidentifiers (RFC 2578 Section 3.5),
498    ///   rejected by [`validate_length()`](Self::validate_length);
499    /// - OIDs with invalid arc constraints per X.690 Section 8.19.4.
500    ///
501    /// Returns an error if any of the above hold.
502    pub fn to_ber_checked(&self) -> Result<Vec<u8>> {
503        if self.arcs.is_empty() {
504            return Err(Error::InvalidOid(
505                "cannot BER-encode an empty OID (no subidentifiers)".into(),
506            )
507            .boxed());
508        }
509        self.validate_all()?;
510        Ok(self.to_ber())
511    }
512
513    /// Returns the BER content size (excluding tag and length bytes).
514    pub(crate) fn ber_content_size(&self) -> usize {
515        use crate::ber::base128_len;
516
517        if self.arcs.is_empty() {
518            return 0;
519        }
520
521        let mut len = 0;
522
523        // First subidentifier (arc1*40 + arc2)
524        len += base128_len(first_subidentifier(&self.arcs));
525
526        // Remaining arcs
527        for &arc in self.arcs.iter().skip(2) {
528            len += base128_len(arc);
529        }
530
531        len
532    }
533
534    /// Returns the total BER-encoded size (tag + length + content).
535    pub(crate) fn ber_encoded_size(&self) -> usize {
536        use crate::ber::length_encoded_len;
537
538        let content_len = self.ber_content_size();
539        1 + length_encoded_len(content_len) + content_len
540    }
541
542    /// Decode from BER format.
543    ///
544    /// Enforces [`MAX_OID_LEN`] limit per RFC 2578 Section 3.5.
545    pub fn from_ber(data: &[u8]) -> Result<Self> {
546        if data.is_empty() {
547            return Ok(Self::empty());
548        }
549
550        let mut arcs = SmallVec::new();
551
552        // Decode first subidentifier (which encodes arc1*40 + arc2)
553        // This may be multi-byte for large arc2 values (when arc1=2)
554        let (first_subid, consumed) = decode_subidentifier(data)?;
555
556        // Decode first two arcs from the first subidentifier
557        if first_subid < 40 {
558            arcs.push(0);
559            arcs.push(first_subid);
560        } else if first_subid < 80 {
561            arcs.push(1);
562            arcs.push(first_subid - 40);
563        } else {
564            arcs.push(2);
565            arcs.push(first_subid - 80);
566        }
567
568        // Decode remaining arcs
569        let mut i = consumed;
570        while i < data.len() {
571            let (arc, bytes_consumed) = decode_subidentifier(&data[i..])?;
572            arcs.push(arc);
573            i += bytes_consumed;
574
575            // RFC 2578 Section 3.5: "at most 128 sub-identifiers in a value"
576            if arcs.len() > MAX_OID_LEN {
577                tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::OidTooLong { count: arcs.len(), max: MAX_OID_LEN } }, "OID exceeds maximum arc count");
578                return Err(Error::MalformedResponse {
579                    target: UNKNOWN_TARGET,
580                }
581                .boxed());
582            }
583        }
584
585        Ok(Self { arcs })
586    }
587}
588
589/// Compute the first OID subidentifier value from an arc slice.
590///
591/// Per X.690 Section 8.19: the first two arcs are encoded as `arc1 * 40 + arc2`.
592/// If there is only one arc, it is encoded as `arc1 * 40`.
593///
594/// Uses saturating arithmetic: for every OID that passes [`Oid::validate()`], the
595/// combined value fits in `u32` and no saturation occurs, so this is byte-identical
596/// to unchecked arithmetic on all valid input. An OID with a first subidentifier
597/// that would exceed `u32::MAX` is not representable in this crate's decode model
598/// (see [`decode_subidentifier`]) and is rejected by `validate()`/`to_ber_checked()`;
599/// saturating here only prevents the unchecked [`Oid::to_ber()`] path from panicking
600/// (debug) or wrapping (release) on such an out-of-range OID.
601#[inline]
602fn first_subidentifier(arcs: &SmallVec<[u32; 16]>) -> u32 {
603    if arcs.len() >= 2 {
604        arcs[0].saturating_mul(40).saturating_add(arcs[1])
605    } else {
606        arcs[0].saturating_mul(40)
607    }
608}
609
610/// Encode a subidentifier in base-128 variable length into a `SmallVec`.
611#[inline]
612fn encode_subidentifier_smallvec(bytes: &mut SmallVec<[u8; 64]>, value: u32) {
613    if value == 0 {
614        bytes.push(0);
615        return;
616    }
617
618    // Count how many 7-bit groups we need
619    let mut temp = value;
620    let mut count = 0;
621    while temp > 0 {
622        count += 1;
623        temp >>= 7;
624    }
625
626    // Encode from MSB to LSB
627    for i in (0..count).rev() {
628        let mut byte = ((value >> (i * 7)) & 0x7F) as u8;
629        if i > 0 {
630            byte |= 0x80; // Continuation bit
631        }
632        bytes.push(byte);
633    }
634}
635
636/// Decode a subidentifier, returning (value, `bytes_consumed`).
637fn decode_subidentifier(data: &[u8]) -> Result<(u32, usize)> {
638    let mut value: u32 = 0;
639    let mut i = 0;
640
641    loop {
642        if i >= data.len() {
643            tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::TruncatedData }, "unexpected end of data in OID subidentifier");
644            return Err(Error::MalformedResponse {
645                target: UNKNOWN_TARGET,
646            }
647            .boxed());
648        }
649
650        let byte = data[i];
651        i += 1;
652
653        // Check for overflow before shifting
654        if value > (u32::MAX >> 7) {
655            tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::IntegerOverflow }, "OID subidentifier overflow");
656            return Err(Error::MalformedResponse {
657                target: UNKNOWN_TARGET,
658            }
659            .boxed());
660        }
661
662        value = (value << 7) | u32::from(byte & 0x7F);
663
664        if byte & 0x80 == 0 {
665            // Last byte
666            break;
667        }
668    }
669
670    Ok((value, i))
671}
672
673impl fmt::Debug for Oid {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675        write!(f, "Oid({self})")
676    }
677}
678
679impl fmt::Display for Oid {
680    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681        let mut first = true;
682        for arc in &self.arcs {
683            if !first {
684                write!(f, ".")?;
685            }
686            write!(f, "{arc}")?;
687            first = false;
688        }
689        Ok(())
690    }
691}
692
693impl std::str::FromStr for Oid {
694    type Err = Box<crate::error::Error>;
695
696    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
697        Self::parse(s)
698    }
699}
700
701impl From<&[u32]> for Oid {
702    fn from(arcs: &[u32]) -> Self {
703        Self::from_slice(arcs)
704    }
705}
706
707impl<const N: usize> From<[u32; N]> for Oid {
708    fn from(arcs: [u32; N]) -> Self {
709        Self::new(arcs)
710    }
711}
712
713impl From<Vec<u32>> for Oid {
714    fn from(arcs: Vec<u32>) -> Self {
715        Self {
716            arcs: SmallVec::from_vec(arcs),
717        }
718    }
719}
720
721impl AsRef<[u32]> for Oid {
722    fn as_ref(&self) -> &[u32] {
723        self.arcs()
724    }
725}
726
727impl<'a> IntoIterator for &'a Oid {
728    type Item = &'a u32;
729    type IntoIter = std::slice::Iter<'a, u32>;
730
731    fn into_iter(self) -> Self::IntoIter {
732        self.arcs().iter()
733    }
734}
735
736impl IntoIterator for Oid {
737    type Item = u32;
738    type IntoIter = smallvec::IntoIter<[u32; 16]>;
739
740    fn into_iter(self) -> Self::IntoIter {
741        self.arcs.into_iter()
742    }
743}
744
745impl PartialOrd for Oid {
746    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
747        Some(self.cmp(other))
748    }
749}
750
751impl Ord for Oid {
752    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
753        self.arcs.cmp(&other.arcs)
754    }
755}
756
757/// Macro to create an OID at compile time.
758///
759/// This is the preferred way to create OID constants since it's concise
760/// and avoids parsing overhead.
761///
762/// # Examples
763///
764/// ```
765/// use async_snmp::oid;
766///
767/// // Create an OID for sysDescr.0
768/// let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
769/// assert_eq!(sys_descr.to_string(), "1.3.6.1.2.1.1.1.0");
770///
771/// // Trailing commas are allowed
772/// let sys_name = oid!(1, 3, 6, 1, 2, 1, 1, 5, 0,);
773///
774/// // Can use in const contexts (via from_slice)
775/// let interfaces = oid!(1, 3, 6, 1, 2, 1, 2);
776/// assert!(sys_descr.starts_with(&oid!(1, 3, 6, 1, 2, 1, 1)));
777/// ```
778#[macro_export]
779macro_rules! oid {
780    ($($arc:expr),* $(,)?) => {
781        $crate::oid::Oid::from_slice(&[$($arc),*])
782    };
783}
784
785// ========================================================================
786// mib-rs OID conversions (feature = "mib")
787// ========================================================================
788
789#[cfg(feature = "mib")]
790impl From<&mib_rs::Oid> for Oid {
791    fn from(oid: &mib_rs::Oid) -> Self {
792        Oid::from_slice(oid.as_ref())
793    }
794}
795
796#[cfg(feature = "mib")]
797impl From<mib_rs::Oid> for Oid {
798    fn from(oid: mib_rs::Oid) -> Self {
799        Oid::from_slice(oid.as_ref())
800    }
801}
802
803#[cfg(feature = "mib")]
804impl Oid {
805    /// Convert to a mib-rs OID.
806    ///
807    /// This is a method rather than a `From` impl because the orphan rule
808    /// prevents implementing `From<&Oid> for mib_rs::Oid` (foreign trait
809    /// for foreign type).
810    pub fn to_mib_oid(&self) -> mib_rs::Oid {
811        mib_rs::Oid::from(self.arcs())
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    #[test]
820    fn test_parse() {
821        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
822        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1, 1, 1, 0]);
823    }
824
825    #[test]
826    fn test_parse_leading_dot() {
827        let oid = Oid::parse(".1.3.6").unwrap();
828        assert_eq!(oid.arcs(), &[1, 3, 6]);
829
830        let oid = Oid::parse(".1.3.6.1.2.1").unwrap();
831        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1]);
832
833        // Leading dot on single arc
834        let oid = Oid::parse(".0").unwrap();
835        assert_eq!(oid.arcs(), &[0]);
836
837        // Just a dot yields empty OID
838        let oid = Oid::parse(".").unwrap();
839        assert!(oid.is_empty());
840    }
841
842    #[test]
843    fn test_parse_rejects_empty_components() {
844        assert!(Oid::parse("1.3.6.").is_err()); // Trailing dot
845        assert!(Oid::parse("1..3.6").is_err()); // Double dot
846        assert!(Oid::parse("..1.3").is_err()); // Double leading dot
847        assert!(Oid::parse("...").is_err()); // All dots
848    }
849
850    #[test]
851    fn test_display() {
852        let oid = Oid::from_slice(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
853        assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.1.0");
854    }
855
856    #[test]
857    fn test_starts_with() {
858        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
859        let prefix = Oid::parse("1.3.6.1").unwrap();
860        assert!(oid.starts_with(&prefix));
861        assert!(!prefix.starts_with(&oid));
862    }
863
864    #[test]
865    fn test_ber_roundtrip() {
866        let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
867        let ber = oid.to_ber();
868        let decoded = Oid::from_ber(&ber).unwrap();
869        assert_eq!(oid, decoded);
870    }
871
872    #[test]
873    fn test_ber_encoding() {
874        // 1.3.6.1 encodes as: (1*40+3)=43, 6, 1 = [0x2B, 0x06, 0x01]
875        let oid = Oid::parse("1.3.6.1").unwrap();
876        assert_eq!(oid.to_ber(), vec![0x2B, 0x06, 0x01]);
877    }
878
879    #[test]
880    fn test_macro() {
881        let oid = oid!(1, 3, 6, 1);
882        assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
883    }
884
885    #[test]
886    fn from_vec_u32() {
887        let v = vec![1, 3, 6, 1, 2, 1];
888        let oid = Oid::from(v);
889        assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1]);
890    }
891
892    #[test]
893    fn oid_as_ref() {
894        let oid = Oid::new([1, 3, 6, 1]);
895        let slice: &[u32] = oid.as_ref();
896        assert_eq!(slice, &[1, 3, 6, 1]);
897    }
898
899    // AUDIT-001: Test arc validation
900    // X.690 Section 8.19.4: arc1 must be 0, 1, or 2; arc2 must be <= 39 when arc1 < 2
901    #[test]
902    fn test_validate_arc1_must_be_0_1_or_2() {
903        // arc1 = 3 is invalid
904        let oid = Oid::from_slice(&[3, 0]);
905        let result = oid.validate();
906        assert!(result.is_err(), "arc1=3 should be invalid");
907    }
908
909    #[test]
910    fn test_validate_arc2_limit_when_arc1_is_0() {
911        // arc1 = 0, arc2 = 40 is invalid (max is 39)
912        let oid = Oid::from_slice(&[0, 40]);
913        let result = oid.validate();
914        assert!(result.is_err(), "arc2=40 with arc1=0 should be invalid");
915
916        // arc1 = 0, arc2 = 39 is valid
917        let oid = Oid::from_slice(&[0, 39]);
918        assert!(
919            oid.validate().is_ok(),
920            "arc2=39 with arc1=0 should be valid"
921        );
922    }
923
924    #[test]
925    fn test_validate_arc2_limit_when_arc1_is_1() {
926        // arc1 = 1, arc2 = 40 is invalid
927        let oid = Oid::from_slice(&[1, 40]);
928        let result = oid.validate();
929        assert!(result.is_err(), "arc2=40 with arc1=1 should be invalid");
930
931        // arc1 = 1, arc2 = 39 is valid
932        let oid = Oid::from_slice(&[1, 39]);
933        assert!(
934            oid.validate().is_ok(),
935            "arc2=39 with arc1=1 should be valid"
936        );
937    }
938
939    #[test]
940    fn test_validate_arc2_no_limit_when_arc1_is_2() {
941        // arc1 = 2, arc2 can be anything (e.g., 999)
942        let oid = Oid::from_slice(&[2, 999]);
943        assert!(
944            oid.validate().is_ok(),
945            "arc2=999 with arc1=2 should be valid"
946        );
947    }
948
949    #[test]
950    fn test_validate_rejects_single_arc() {
951        // A single-arc OID has no invertible BER encoding (encode [1] -> subid 40,
952        // decode -> [1, 0]); validate() and to_ber_checked() must reject it.
953        let oid = Oid::from_slice(&[1]);
954        assert!(oid.validate().is_err(), "single-arc OID must be rejected");
955        assert!(
956            oid.to_ber_checked().is_err(),
957            "to_ber_checked must reject single-arc OID"
958        );
959        // parse still accepts it (only validate() is stricter)
960        assert_eq!(Oid::parse("1").unwrap().arcs(), &[1]);
961    }
962
963    #[test]
964    fn test_to_ber_checked_rejects_non_wire_safe_oids() {
965        // Empty OID: no subidentifiers, not a valid OBJECT IDENTIFIER value
966        // (X.690 Section 8.19.4). The strict encode entry point must reject it
967        // even though empty OIDs are usable as a prefix concept elsewhere.
968        assert!(
969            Oid::empty().to_ber_checked().is_err(),
970            "to_ber_checked must reject empty OID"
971        );
972
973        // Single-arc OID: no invertible BER encoding ([n] -> subid n*40 -> [n, 0]).
974        assert!(
975            Oid::from_slice(&[1]).to_ber_checked().is_err(),
976            "to_ber_checked must reject single-arc OID"
977        );
978
979        // Over-MAX_OID_LEN OID: RFC 2578 Section 3.5 caps values at MAX_OID_LEN
980        // subidentifiers. validate() alone does not catch this; validate_length() does.
981        let mut arcs = vec![1u32, 3];
982        arcs.extend(2..(MAX_OID_LEN as u32));
983        // arcs now has MAX_OID_LEN elements and is accepted.
984        let at_limit = Oid::new(arcs.clone());
985        assert_eq!(at_limit.arcs().len(), MAX_OID_LEN);
986        assert!(
987            at_limit.to_ber_checked().is_ok(),
988            "to_ber_checked should accept OID at MAX_OID_LEN"
989        );
990        arcs.push(0);
991        let over_limit = Oid::new(arcs);
992        assert_eq!(over_limit.arcs().len(), MAX_OID_LEN + 1);
993        assert!(
994            over_limit.to_ber_checked().is_err(),
995            "to_ber_checked must reject OID exceeding MAX_OID_LEN"
996        );
997    }
998
999    #[test]
1000    fn test_to_ber_validates_arcs() {
1001        // Invalid OID should return error from to_ber_checked
1002        let oid = Oid::from_slice(&[3, 0]); // arc1=3 is invalid
1003        let result = oid.to_ber_checked();
1004        assert!(
1005            result.is_err(),
1006            "to_ber_checked should fail for invalid arc1"
1007        );
1008    }
1009
1010    // AUDIT-002: Test first subidentifier encoding for large arc2 values
1011    // X.690 Section 8.19 example: OID {2 999 3} has first subidentifier = 1079
1012    #[test]
1013    fn test_ber_encoding_large_arc2() {
1014        // OID 2.999.3: first subid = 2*40 + 999 = 1079 = 0x437
1015        // 1079 in base-128: 0x88 0x37 (continuation bit set on first byte)
1016        let oid = Oid::from_slice(&[2, 999, 3]);
1017        let ber = oid.to_ber();
1018        // First subidentifier 1079 = 0b10000110111 = 7 bits: 0b0110111 (0x37), 7 bits: 0b0001000 (0x08)
1019        // In base-128: (1079 >> 7) = 8, (1079 & 0x7F) = 55
1020        // So: 0x88 (8 | 0x80), 0x37 (55)
1021        assert_eq!(
1022            ber[0], 0x88,
1023            "first byte should be 0x88 (8 with continuation)"
1024        );
1025        assert_eq!(
1026            ber[1], 0x37,
1027            "second byte should be 0x37 (55, no continuation)"
1028        );
1029        assert_eq!(ber[2], 0x03, "third byte should be 0x03 (arc 3)");
1030        assert_eq!(ber.len(), 3, "OID 2.999.3 should encode to 3 bytes");
1031    }
1032
1033    #[test]
1034    fn test_ber_roundtrip_large_arc2() {
1035        // Ensure roundtrip works for OID with large arc2
1036        let oid = Oid::from_slice(&[2, 999, 3]);
1037        let ber = oid.to_ber();
1038        let decoded = Oid::from_ber(&ber).unwrap();
1039        assert_eq!(oid, decoded, "roundtrip should preserve OID 2.999.3");
1040    }
1041
1042    #[test]
1043    fn test_ber_encoding_arc2_equals_80() {
1044        // Edge case: arc1=2, arc2=0 gives first subid = 80, which is exactly 1 byte
1045        let oid = Oid::from_slice(&[2, 0]);
1046        let ber = oid.to_ber();
1047        assert_eq!(ber, vec![80], "OID 2.0 should encode to [80]");
1048    }
1049
1050    #[test]
1051    fn test_ber_encoding_arc2_equals_127() {
1052        // arc1=2, arc2=47 gives first subid = 127, still fits in 1 byte
1053        let oid = Oid::from_slice(&[2, 47]);
1054        let ber = oid.to_ber();
1055        assert_eq!(ber, vec![127], "OID 2.47 should encode to [127]");
1056    }
1057
1058    #[test]
1059    fn test_ber_encoding_arc2_equals_128_needs_2_bytes() {
1060        // arc1=2, arc2=48 gives first subid = 128, needs 2 bytes in base-128
1061        let oid = Oid::from_slice(&[2, 48]);
1062        let ber = oid.to_ber();
1063        // 128 = 0x80 = base-128: 0x81 0x00
1064        assert_eq!(
1065            ber,
1066            vec![0x81, 0x00],
1067            "OID 2.48 should encode to [0x81, 0x00]"
1068        );
1069    }
1070
1071    #[test]
1072    fn test_from_ber_zero_length() {
1073        // A zero-length OID content (BER encoding 06 00) is accepted and returns
1074        // an empty OID. This matches net-snmp's behavior in snmplib/asn1.c which
1075        // treats the same encoding as 0.0 rather than rejecting it. Our empty OID
1076        // differs from net-snmp's 0.0 - net-snmp encodes empty OIDs as [0x00]
1077        // (a single zero byte yielding 0.0), while we produce truly zero arcs.
1078        // Devices send this when returning endOfMibView with a malformed zero-length
1079        // OID instead of echoing back the requested OID (RFC 3416 violation).
1080        let result = Oid::from_ber(&[]);
1081        assert!(result.is_ok(), "zero-length OID content should be accepted");
1082        assert!(result.unwrap().is_empty());
1083    }
1084
1085    #[test]
1086    fn test_oid_non_minimal_subidentifier() {
1087        // Non-minimal subidentifier encoding with leading 0x80 bytes should be accepted
1088        // 0x80 0x01 should decode as 1 (non-minimal: minimal would be just 0x01)
1089        // OID: 1.3 followed by arc 1 encoded as 0x80 0x01
1090        let result = Oid::from_ber(&[0x2B, 0x80, 0x01]);
1091        assert!(
1092            result.is_ok(),
1093            "should accept non-minimal subidentifier 0x80 0x01"
1094        );
1095        let oid = result.unwrap();
1096        assert_eq!(oid.arcs(), &[1, 3, 1]);
1097
1098        // 0x80 0x80 0x01 should decode as 1 (two leading 0x80 bytes)
1099        let result = Oid::from_ber(&[0x2B, 0x80, 0x80, 0x01]);
1100        assert!(
1101            result.is_ok(),
1102            "should accept non-minimal subidentifier 0x80 0x80 0x01"
1103        );
1104        let oid = result.unwrap();
1105        assert_eq!(oid.arcs(), &[1, 3, 1]);
1106
1107        // 0x80 0x00 should decode as 0 (non-minimal zero)
1108        let result = Oid::from_ber(&[0x2B, 0x80, 0x00]);
1109        assert!(
1110            result.is_ok(),
1111            "should accept non-minimal subidentifier 0x80 0x00"
1112        );
1113        let oid = result.unwrap();
1114        assert_eq!(oid.arcs(), &[1, 3, 0]);
1115    }
1116
1117    // Tests for MAX_OID_LEN validation
1118    #[test]
1119    fn test_validate_length_within_limit() {
1120        // OID with MAX_OID_LEN arcs should be valid
1121        let arcs: Vec<u32> = (0..MAX_OID_LEN as u32).collect();
1122        let oid = Oid::new(arcs);
1123        assert!(
1124            oid.validate_length().is_ok(),
1125            "OID with exactly MAX_OID_LEN arcs should be valid"
1126        );
1127    }
1128
1129    #[test]
1130    fn test_validate_length_exceeds_limit() {
1131        // OID with more than MAX_OID_LEN arcs should fail
1132        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
1133        let oid = Oid::new(arcs);
1134        let result = oid.validate_length();
1135        assert!(
1136            result.is_err(),
1137            "OID exceeding MAX_OID_LEN should fail validation"
1138        );
1139    }
1140
1141    #[test]
1142    fn test_validate_all_combines_checks() {
1143        // Valid OID
1144        let oid = Oid::from_slice(&[1, 3, 6, 1]);
1145        assert!(oid.validate_all().is_ok());
1146
1147        // Invalid arc1 (fails validate)
1148        let oid = Oid::from_slice(&[3, 0]);
1149        assert!(oid.validate_all().is_err());
1150
1151        // Too many arcs (fails validate_length)
1152        let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
1153        let oid = Oid::new(arcs);
1154        assert!(oid.validate_all().is_err());
1155    }
1156
1157    #[test]
1158    fn test_oid_fromstr() {
1159        // Test basic parsing via FromStr trait
1160        let oid: Oid = "1.3.6.1.2.1.1.1.0".parse().unwrap();
1161        assert_eq!(oid, oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
1162
1163        // Test empty OID
1164        let empty: Oid = "".parse().unwrap();
1165        assert!(empty.is_empty());
1166
1167        // Test single arc
1168        let single: Oid = "1".parse().unwrap();
1169        assert_eq!(single.arcs(), &[1]);
1170
1171        // Test roundtrip Display -> FromStr
1172        let original = oid!(1, 3, 6, 1, 4, 1, 9, 9, 42);
1173        let displayed = original.to_string();
1174        let parsed: Oid = displayed.parse().unwrap();
1175        assert_eq!(original, parsed);
1176    }
1177
1178    #[test]
1179    fn test_oid_fromstr_invalid() {
1180        // Invalid arc value
1181        assert!("1.3.abc.1".parse::<Oid>().is_err());
1182
1183        // Negative number (parsed as invalid)
1184        assert!("1.3.-6.1".parse::<Oid>().is_err());
1185    }
1186
1187    // Test for first subidentifier overflow (arc1*40 + arc2 must fit in u32)
1188    // When arc1=2, arc2 cannot exceed u32::MAX - 80
1189    #[test]
1190    fn test_validate_arc2_overflow_when_arc1_is_2() {
1191        // Maximum valid arc2 when arc1=2: u32::MAX - 80 = 4294967215
1192        let max_valid_arc2 = u32::MAX - 80;
1193        let oid = Oid::from_slice(&[2, max_valid_arc2]);
1194        assert!(
1195            oid.validate().is_ok(),
1196            "arc2={max_valid_arc2} with arc1=2 should be valid (max that fits)"
1197        );
1198
1199        // One more than max should fail validation
1200        let overflow_arc2 = u32::MAX - 79; // 2*40 + this = u32::MAX + 1
1201        let oid = Oid::from_slice(&[2, overflow_arc2]);
1202        assert!(
1203            oid.validate().is_err(),
1204            "arc2={overflow_arc2} with arc1=2 should be invalid (would overflow first subidentifier)"
1205        );
1206
1207        // Also test arc2 = u32::MAX should definitely fail
1208        let oid = Oid::from_slice(&[2, u32::MAX]);
1209        assert!(
1210            oid.validate().is_err(),
1211            "arc2=u32::MAX with arc1=2 should be invalid"
1212        );
1213    }
1214
1215    #[test]
1216    fn test_to_ber_checked_rejects_overflow() {
1217        // Encoding an OID that would overflow should fail via to_ber_checked
1218        let oid = Oid::from_slice(&[2, u32::MAX]);
1219        let result = oid.to_ber_checked();
1220        assert!(
1221            result.is_err(),
1222            "to_ber_checked should reject OID that would overflow"
1223        );
1224    }
1225
1226    #[test]
1227    fn test_from_ber_enforces_max_oid_len() {
1228        // Create BER data for an OID with more than MAX_OID_LEN arcs
1229        // OID encoding: first subid encodes arc1*40+arc2, then each subsequent arc
1230        // First subid gives us 2 arcs (e.g., 1 and 3), so we need MAX_OID_LEN - 2
1231        // additional arcs to hit exactly MAX_OID_LEN.
1232
1233        // Build OID at exactly MAX_OID_LEN: 1.3 followed by (MAX_OID_LEN - 2) arcs of value 1
1234        let mut ber_at_limit = vec![0x2B]; // First subid = 1*40 + 3 = 43 (encodes arc1=1, arc2=3)
1235        ber_at_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 2));
1236
1237        let result = Oid::from_ber(&ber_at_limit);
1238        assert!(
1239            result.is_ok(),
1240            "OID with exactly MAX_OID_LEN arcs should decode successfully"
1241        );
1242        assert_eq!(result.unwrap().len(), MAX_OID_LEN);
1243
1244        // Now one more arc should exceed the limit
1245        let mut ber_over_limit = vec![0x2B]; // arc1=1, arc2=3
1246        ber_over_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 1));
1247
1248        let result = Oid::from_ber(&ber_over_limit);
1249        assert!(
1250            result.is_err(),
1251            "OID exceeding MAX_OID_LEN should fail to decode"
1252        );
1253    }
1254
1255    // ========================================================================
1256    // Suffix Extraction Tests
1257    // ========================================================================
1258
1259    #[test]
1260    fn test_strip_prefix() {
1261        let if_descr_5 = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 5);
1262        let if_descr = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
1263
1264        // Extract the index
1265        let index = if_descr_5.strip_prefix(&if_descr).unwrap();
1266        assert_eq!(index.arcs(), &[5]);
1267
1268        // Non-matching prefix returns None
1269        let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1);
1270        assert!(if_descr_5.strip_prefix(&sys_descr).is_none());
1271
1272        // Equal OIDs return empty
1273        let same = oid!(1, 3, 6);
1274        assert!(same.strip_prefix(&same).unwrap().is_empty());
1275
1276        // Empty prefix returns self
1277        let any = oid!(1, 2, 3);
1278        assert_eq!(any.strip_prefix(&Oid::empty()).unwrap(), any);
1279
1280        // Multi-arc index
1281        let composite = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1282        let column = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2);
1283        let idx = composite.strip_prefix(&column).unwrap();
1284        assert_eq!(idx.arcs(), &[1, 192, 168, 1, 100]);
1285    }
1286
1287    #[test]
1288    fn test_suffix() {
1289        let oid = oid!(1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 1, 192, 168, 1, 100);
1290
1291        // Get the 5-arc index
1292        assert_eq!(oid.suffix(5), Some(&[1, 192, 168, 1, 100][..]));
1293
1294        // Get just the last arc
1295        assert_eq!(oid.suffix(1), Some(&[100][..]));
1296
1297        // suffix(0) returns empty slice
1298        assert_eq!(oid.suffix(0), Some(&[][..]));
1299
1300        // Exact length returns full OID
1301        assert_eq!(oid.suffix(15), Some(oid.arcs()));
1302
1303        // Too large returns None
1304        assert!(oid.suffix(16).is_none());
1305        assert!(oid.suffix(100).is_none());
1306
1307        // Empty OID
1308        let empty = Oid::empty();
1309        assert_eq!(empty.suffix(0), Some(&[][..]));
1310        assert!(empty.suffix(1).is_none());
1311    }
1312
1313    #[test]
1314    fn oid_into_iter_ref() {
1315        let oid = Oid::new([1, 3, 6]);
1316        let arcs: Vec<&u32> = (&oid).into_iter().collect();
1317        assert_eq!(arcs, vec![&1, &3, &6]);
1318    }
1319
1320    #[test]
1321    fn oid_into_iter_owned() {
1322        let oid = Oid::new([1, 3, 6]);
1323        let arcs: Vec<u32> = oid.into_iter().collect();
1324        assert_eq!(arcs, vec![1, 3, 6]);
1325    }
1326
1327    #[test]
1328    fn oid_for_loop() {
1329        let oid = Oid::new([1, 3, 6]);
1330        let mut sum = 0u32;
1331        for arc in &oid {
1332            sum += arc;
1333        }
1334        assert_eq!(sum, 10);
1335    }
1336
1337    #[test]
1338    fn to_ber_saturates_first_subidentifier_instead_of_overflowing() {
1339        // arc1=2, arc2=u32::MAX: arc1*40 + arc2 overflows u32. The unchecked
1340        // encode path must saturate rather than panic (debug) or wrap (release).
1341        let oid = Oid::from_slice(&[2, u32::MAX]);
1342
1343        // Checked path still rejects this OID.
1344        assert!(oid.validate().is_err());
1345        assert!(oid.to_ber_checked().is_err());
1346
1347        // Unchecked path must not panic and must be deterministic.
1348        let bytes1 = oid.to_ber();
1349        let bytes2 = oid.to_ber();
1350        assert_eq!(bytes1, bytes2);
1351        assert!(!bytes1.is_empty());
1352
1353        // Saturated first subidentifier is u32::MAX, encoded as the sole arc.
1354        let expected = {
1355            let mut v = SmallVec::<[u8; 64]>::new();
1356            encode_subidentifier_smallvec(&mut v, u32::MAX);
1357            v.to_vec()
1358        };
1359        assert_eq!(bytes1, expected);
1360
1361        // ber_content_size/ber_encoded_size stay self-consistent.
1362        assert_eq!(oid.to_ber().len(), oid.ber_content_size());
1363    }
1364
1365    #[test]
1366    fn to_ber_unchanged_for_valid_oid() {
1367        // Control: a valid OID's encoding is unaffected by the saturating change.
1368        let oid = Oid::parse("1.3.6.1.2.1").unwrap();
1369        assert!(oid.validate().is_ok());
1370        assert_eq!(oid.to_ber(), vec![0x2b, 0x06, 0x01, 0x02, 0x01]);
1371    }
1372}