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