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}