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