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