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 /// Validate OID arcs per X.690 Section 8.19.4.
228 ///
229 /// - arc1 must be 0, 1, or 2
230 /// - arc2 must be <= 39 when arc1 is 0 or 1
231 /// - arc2 must not cause overflow when computing first subidentifier (arc1*40 + arc2)
232 ///
233 /// # Examples
234 ///
235 /// ```
236 /// use async_snmp::oid::Oid;
237 ///
238 /// // Standard SNMP OIDs are valid
239 /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
240 /// assert!(oid.validate().is_ok());
241 ///
242 /// // arc1 must be 0, 1, or 2
243 /// let invalid = Oid::from_slice(&[3, 0]);
244 /// assert!(invalid.validate().is_err());
245 ///
246 /// // arc2 must be <= 39 when arc1 is 0 or 1
247 /// let invalid = Oid::from_slice(&[0, 40]);
248 /// assert!(invalid.validate().is_err());
249 ///
250 /// // arc2 can be large when arc1 is 2, but must not overflow
251 /// let valid = Oid::from_slice(&[2, 999]);
252 /// assert!(valid.validate().is_ok());
253 /// ```
254 pub fn validate(&self) -> Result<()> {
255 if self.arcs.is_empty() {
256 return Ok(());
257 }
258
259 let arc1 = self.arcs[0];
260
261 // arc1 must be 0, 1, or 2
262 if arc1 > 2 {
263 return Err(Error::InvalidOid(
264 format!("first arc must be 0, 1, or 2, got {}", arc1).into(),
265 )
266 .boxed());
267 }
268
269 // Validate arc2 constraints
270 if self.arcs.len() >= 2 {
271 let arc2 = self.arcs[1];
272
273 // arc2 must be <= 39 when arc1 < 2
274 if arc1 < 2 && arc2 >= 40 {
275 return Err(Error::InvalidOid(
276 format!(
277 "second arc must be <= 39 when first arc is {}, got {}",
278 arc1, arc2
279 )
280 .into(),
281 )
282 .boxed());
283 }
284
285 // Check that first subidentifier (arc1*40 + arc2) won't overflow u32.
286 // Max valid arc2 = u32::MAX - arc1*40
287 let base = arc1 * 40;
288 if arc2 > u32::MAX - base {
289 return Err(
290 Error::InvalidOid("subidentifier overflow in first two arcs".into()).boxed(),
291 );
292 }
293 }
294
295 Ok(())
296 }
297
298 /// Validate that the OID doesn't exceed the maximum arc count.
299 ///
300 /// SNMP implementations commonly limit OIDs to 128 subidentifiers. This check
301 /// provides protection against DoS attacks from maliciously long OIDs.
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use async_snmp::oid::{Oid, MAX_OID_LEN};
307 ///
308 /// let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
309 /// assert!(oid.validate_length().is_ok());
310 ///
311 /// // Create an OID with too many arcs
312 /// let too_long: Vec<u32> = (0..150).collect();
313 /// let long_oid = Oid::new(too_long);
314 /// assert!(long_oid.validate_length().is_err());
315 /// ```
316 pub fn validate_length(&self) -> Result<()> {
317 if self.arcs.len() > MAX_OID_LEN {
318 return Err(Error::InvalidOid(
319 format!(
320 "OID has {} arcs, exceeds maximum {}",
321 self.arcs.len(),
322 MAX_OID_LEN
323 )
324 .into(),
325 )
326 .boxed());
327 }
328 Ok(())
329 }
330
331 /// Validate both arc constraints and length.
332 ///
333 /// Combines [`validate()`](Self::validate) and [`validate_length()`](Self::validate_length).
334 pub fn validate_all(&self) -> Result<()> {
335 self.validate()?;
336 self.validate_length()
337 }
338
339 /// Encode to BER format, returning bytes in a stack-allocated buffer.
340 ///
341 /// Uses SmallVec to avoid heap allocation for OIDs with up to ~20 arcs.
342 /// This is the optimized version used internally by encoding routines.
343 ///
344 /// OID encoding (X.690 Section 8.19):
345 /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
346 /// - Remaining arcs encoded as base-128 variable length
347 pub fn to_ber_smallvec(&self) -> SmallVec<[u8; 64]> {
348 let mut bytes = SmallVec::new();
349
350 if self.arcs.is_empty() {
351 return bytes;
352 }
353
354 // First two arcs combined into first subidentifier
355 // Uses base-128 encoding because arc2 can be > 127 when arc1=2
356 if self.arcs.len() >= 2 {
357 let first_subid = self.arcs[0] * 40 + self.arcs[1];
358 encode_subidentifier_smallvec(&mut bytes, first_subid);
359 } else if self.arcs.len() == 1 {
360 let first_subid = self.arcs[0] * 40;
361 encode_subidentifier_smallvec(&mut bytes, first_subid);
362 }
363
364 // Remaining arcs (only if there are more than 2)
365 if self.arcs.len() > 2 {
366 for &arc in &self.arcs[2..] {
367 encode_subidentifier_smallvec(&mut bytes, arc);
368 }
369 }
370
371 bytes
372 }
373
374 /// Encode to BER format.
375 ///
376 /// OID encoding (X.690 Section 8.19):
377 /// - First two arcs encoded as (arc1 * 40) + arc2 using base-128
378 /// - Remaining arcs encoded as base-128 variable length
379 ///
380 /// # Empty OID Encoding
381 ///
382 /// Empty OIDs are encoded as zero bytes (empty content). Note that net-snmp
383 /// encodes empty OIDs as `[0x00]` (single zero byte). This difference is
384 /// unlikely to matter in practice since empty OIDs are rarely used in SNMP.
385 ///
386 /// # Validation
387 ///
388 /// This method does not validate arc constraints. Use [`to_ber_checked()`](Self::to_ber_checked)
389 /// for validation, or call [`validate()`](Self::validate) first.
390 pub fn to_ber(&self) -> Vec<u8> {
391 self.to_ber_smallvec().to_vec()
392 }
393
394 /// Encode to BER format with validation.
395 ///
396 /// Returns an error if the OID has invalid arcs per X.690 Section 8.19.4.
397 pub fn to_ber_checked(&self) -> Result<Vec<u8>> {
398 self.validate()?;
399 Ok(self.to_ber())
400 }
401
402 /// Returns the BER content length (excluding tag and length bytes).
403 pub(crate) fn ber_content_len(&self) -> usize {
404 use crate::ber::base128_len;
405
406 if self.arcs.is_empty() {
407 return 0;
408 }
409
410 let mut len = 0;
411
412 // First subidentifier (arc1*40 + arc2)
413 if self.arcs.len() >= 2 {
414 let first_subid = self.arcs[0] * 40 + self.arcs[1];
415 len += base128_len(first_subid);
416 } else {
417 // Single arc: arc1 * 40
418 let first_subid = self.arcs[0] * 40;
419 len += base128_len(first_subid);
420 }
421
422 // Remaining arcs
423 for &arc in self.arcs.iter().skip(2) {
424 len += base128_len(arc);
425 }
426
427 len
428 }
429
430 /// Returns the total BER-encoded length (tag + length + content).
431 pub(crate) fn ber_encoded_len(&self) -> usize {
432 use crate::ber::length_encoded_len;
433
434 let content_len = self.ber_content_len();
435 1 + length_encoded_len(content_len) + content_len
436 }
437
438 /// Decode from BER format.
439 ///
440 /// Enforces [`MAX_OID_LEN`] limit per RFC 2578 Section 3.5.
441 pub fn from_ber(data: &[u8]) -> Result<Self> {
442 if data.is_empty() {
443 return Ok(Self::empty());
444 }
445
446 let mut arcs = SmallVec::new();
447
448 // Decode first subidentifier (which encodes arc1*40 + arc2)
449 // This may be multi-byte for large arc2 values (when arc1=2)
450 let (first_subid, consumed) = decode_subidentifier(data)?;
451
452 // Decode first two arcs from the first subidentifier
453 if first_subid < 40 {
454 arcs.push(0);
455 arcs.push(first_subid);
456 } else if first_subid < 80 {
457 arcs.push(1);
458 arcs.push(first_subid - 40);
459 } else {
460 arcs.push(2);
461 arcs.push(first_subid - 80);
462 }
463
464 // Decode remaining arcs
465 let mut i = consumed;
466 while i < data.len() {
467 let (arc, bytes_consumed) = decode_subidentifier(&data[i..])?;
468 arcs.push(arc);
469 i += bytes_consumed;
470
471 // RFC 2578 Section 3.5: "at most 128 sub-identifiers in a value"
472 if arcs.len() > MAX_OID_LEN {
473 tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::OidTooLong { count: arcs.len(), max: MAX_OID_LEN } }, "OID exceeds maximum arc count");
474 return Err(Error::MalformedResponse {
475 target: UNKNOWN_TARGET,
476 }
477 .boxed());
478 }
479 }
480
481 Ok(Self { arcs })
482 }
483}
484
485/// Encode a subidentifier in base-128 variable length into a SmallVec.
486#[inline]
487fn encode_subidentifier_smallvec(bytes: &mut SmallVec<[u8; 64]>, value: u32) {
488 if value == 0 {
489 bytes.push(0);
490 return;
491 }
492
493 // Count how many 7-bit groups we need
494 let mut temp = value;
495 let mut count = 0;
496 while temp > 0 {
497 count += 1;
498 temp >>= 7;
499 }
500
501 // Encode from MSB to LSB
502 for i in (0..count).rev() {
503 let mut byte = ((value >> (i * 7)) & 0x7F) as u8;
504 if i > 0 {
505 byte |= 0x80; // Continuation bit
506 }
507 bytes.push(byte);
508 }
509}
510
511/// Decode a subidentifier, returning (value, bytes_consumed).
512fn decode_subidentifier(data: &[u8]) -> Result<(u32, usize)> {
513 let mut value: u32 = 0;
514 let mut i = 0;
515
516 loop {
517 if i >= data.len() {
518 tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::TruncatedData }, "unexpected end of data in OID subidentifier");
519 return Err(Error::MalformedResponse {
520 target: UNKNOWN_TARGET,
521 }
522 .boxed());
523 }
524
525 let byte = data[i];
526 i += 1;
527
528 // Check for overflow before shifting
529 if value > (u32::MAX >> 7) {
530 tracing::debug!(target: "async_snmp::oid", { snmp.offset = %i, kind = %DecodeErrorKind::IntegerOverflow }, "OID subidentifier overflow");
531 return Err(Error::MalformedResponse {
532 target: UNKNOWN_TARGET,
533 }
534 .boxed());
535 }
536
537 value = (value << 7) | ((byte & 0x7F) as u32);
538
539 if byte & 0x80 == 0 {
540 // Last byte
541 break;
542 }
543 }
544
545 Ok((value, i))
546}
547
548impl fmt::Debug for Oid {
549 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550 write!(f, "Oid({})", self)
551 }
552}
553
554impl fmt::Display for Oid {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 let mut first = true;
557 for arc in &self.arcs {
558 if !first {
559 write!(f, ".")?;
560 }
561 write!(f, "{}", arc)?;
562 first = false;
563 }
564 Ok(())
565 }
566}
567
568impl std::str::FromStr for Oid {
569 type Err = Box<crate::error::Error>;
570
571 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
572 Self::parse(s)
573 }
574}
575
576impl From<&[u32]> for Oid {
577 fn from(arcs: &[u32]) -> Self {
578 Self::from_slice(arcs)
579 }
580}
581
582impl<const N: usize> From<[u32; N]> for Oid {
583 fn from(arcs: [u32; N]) -> Self {
584 Self::new(arcs)
585 }
586}
587
588impl PartialOrd for Oid {
589 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
590 Some(self.cmp(other))
591 }
592}
593
594impl Ord for Oid {
595 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
596 self.arcs.cmp(&other.arcs)
597 }
598}
599
600/// Macro to create an OID at compile time.
601///
602/// This is the preferred way to create OID constants since it's concise
603/// and avoids parsing overhead.
604///
605/// # Examples
606///
607/// ```
608/// use async_snmp::oid;
609///
610/// // Create an OID for sysDescr.0
611/// let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
612/// assert_eq!(sys_descr.to_string(), "1.3.6.1.2.1.1.1.0");
613///
614/// // Trailing commas are allowed
615/// let sys_name = oid!(1, 3, 6, 1, 2, 1, 1, 5, 0,);
616///
617/// // Can use in const contexts (via from_slice)
618/// let interfaces = oid!(1, 3, 6, 1, 2, 1, 2);
619/// assert!(sys_descr.starts_with(&oid!(1, 3, 6, 1, 2, 1, 1)));
620/// ```
621#[macro_export]
622macro_rules! oid {
623 ($($arc:expr),* $(,)?) => {
624 $crate::oid::Oid::from_slice(&[$($arc),*])
625 };
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn test_parse() {
634 let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
635 assert_eq!(oid.arcs(), &[1, 3, 6, 1, 2, 1, 1, 1, 0]);
636 }
637
638 #[test]
639 fn test_display() {
640 let oid = Oid::from_slice(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
641 assert_eq!(oid.to_string(), "1.3.6.1.2.1.1.1.0");
642 }
643
644 #[test]
645 fn test_starts_with() {
646 let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
647 let prefix = Oid::parse("1.3.6.1").unwrap();
648 assert!(oid.starts_with(&prefix));
649 assert!(!prefix.starts_with(&oid));
650 }
651
652 #[test]
653 fn test_ber_roundtrip() {
654 let oid = Oid::parse("1.3.6.1.2.1.1.1.0").unwrap();
655 let ber = oid.to_ber();
656 let decoded = Oid::from_ber(&ber).unwrap();
657 assert_eq!(oid, decoded);
658 }
659
660 #[test]
661 fn test_ber_encoding() {
662 // 1.3.6.1 encodes as: (1*40+3)=43, 6, 1 = [0x2B, 0x06, 0x01]
663 let oid = Oid::parse("1.3.6.1").unwrap();
664 assert_eq!(oid.to_ber(), vec![0x2B, 0x06, 0x01]);
665 }
666
667 #[test]
668 fn test_macro() {
669 let oid = oid!(1, 3, 6, 1);
670 assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
671 }
672
673 // AUDIT-001: Test arc validation
674 // X.690 Section 8.19.4: arc1 must be 0, 1, or 2; arc2 must be <= 39 when arc1 < 2
675 #[test]
676 fn test_validate_arc1_must_be_0_1_or_2() {
677 // arc1 = 3 is invalid
678 let oid = Oid::from_slice(&[3, 0]);
679 let result = oid.validate();
680 assert!(result.is_err(), "arc1=3 should be invalid");
681 }
682
683 #[test]
684 fn test_validate_arc2_limit_when_arc1_is_0() {
685 // arc1 = 0, arc2 = 40 is invalid (max is 39)
686 let oid = Oid::from_slice(&[0, 40]);
687 let result = oid.validate();
688 assert!(result.is_err(), "arc2=40 with arc1=0 should be invalid");
689
690 // arc1 = 0, arc2 = 39 is valid
691 let oid = Oid::from_slice(&[0, 39]);
692 assert!(
693 oid.validate().is_ok(),
694 "arc2=39 with arc1=0 should be valid"
695 );
696 }
697
698 #[test]
699 fn test_validate_arc2_limit_when_arc1_is_1() {
700 // arc1 = 1, arc2 = 40 is invalid
701 let oid = Oid::from_slice(&[1, 40]);
702 let result = oid.validate();
703 assert!(result.is_err(), "arc2=40 with arc1=1 should be invalid");
704
705 // arc1 = 1, arc2 = 39 is valid
706 let oid = Oid::from_slice(&[1, 39]);
707 assert!(
708 oid.validate().is_ok(),
709 "arc2=39 with arc1=1 should be valid"
710 );
711 }
712
713 #[test]
714 fn test_validate_arc2_no_limit_when_arc1_is_2() {
715 // arc1 = 2, arc2 can be anything (e.g., 999)
716 let oid = Oid::from_slice(&[2, 999]);
717 assert!(
718 oid.validate().is_ok(),
719 "arc2=999 with arc1=2 should be valid"
720 );
721 }
722
723 #[test]
724 fn test_to_ber_validates_arcs() {
725 // Invalid OID should return error from to_ber_checked
726 let oid = Oid::from_slice(&[3, 0]); // arc1=3 is invalid
727 let result = oid.to_ber_checked();
728 assert!(
729 result.is_err(),
730 "to_ber_checked should fail for invalid arc1"
731 );
732 }
733
734 // AUDIT-002: Test first subidentifier encoding for large arc2 values
735 // X.690 Section 8.19 example: OID {2 999 3} has first subidentifier = 1079
736 #[test]
737 fn test_ber_encoding_large_arc2() {
738 // OID 2.999.3: first subid = 2*40 + 999 = 1079 = 0x437
739 // 1079 in base-128: 0x88 0x37 (continuation bit set on first byte)
740 let oid = Oid::from_slice(&[2, 999, 3]);
741 let ber = oid.to_ber();
742 // First subidentifier 1079 = 0b10000110111 = 7 bits: 0b0110111 (0x37), 7 bits: 0b0001000 (0x08)
743 // In base-128: (1079 >> 7) = 8, (1079 & 0x7F) = 55
744 // So: 0x88 (8 | 0x80), 0x37 (55)
745 assert_eq!(
746 ber[0], 0x88,
747 "first byte should be 0x88 (8 with continuation)"
748 );
749 assert_eq!(
750 ber[1], 0x37,
751 "second byte should be 0x37 (55, no continuation)"
752 );
753 assert_eq!(ber[2], 0x03, "third byte should be 0x03 (arc 3)");
754 assert_eq!(ber.len(), 3, "OID 2.999.3 should encode to 3 bytes");
755 }
756
757 #[test]
758 fn test_ber_roundtrip_large_arc2() {
759 // Ensure roundtrip works for OID with large arc2
760 let oid = Oid::from_slice(&[2, 999, 3]);
761 let ber = oid.to_ber();
762 let decoded = Oid::from_ber(&ber).unwrap();
763 assert_eq!(oid, decoded, "roundtrip should preserve OID 2.999.3");
764 }
765
766 #[test]
767 fn test_ber_encoding_arc2_equals_80() {
768 // Edge case: arc1=2, arc2=0 gives first subid = 80, which is exactly 1 byte
769 let oid = Oid::from_slice(&[2, 0]);
770 let ber = oid.to_ber();
771 assert_eq!(ber, vec![80], "OID 2.0 should encode to [80]");
772 }
773
774 #[test]
775 fn test_ber_encoding_arc2_equals_127() {
776 // arc1=2, arc2=47 gives first subid = 127, still fits in 1 byte
777 let oid = Oid::from_slice(&[2, 47]);
778 let ber = oid.to_ber();
779 assert_eq!(ber, vec![127], "OID 2.47 should encode to [127]");
780 }
781
782 #[test]
783 fn test_ber_encoding_arc2_equals_128_needs_2_bytes() {
784 // arc1=2, arc2=48 gives first subid = 128, needs 2 bytes in base-128
785 let oid = Oid::from_slice(&[2, 48]);
786 let ber = oid.to_ber();
787 // 128 = 0x80 = base-128: 0x81 0x00
788 assert_eq!(
789 ber,
790 vec![0x81, 0x00],
791 "OID 2.48 should encode to [0x81, 0x00]"
792 );
793 }
794
795 #[test]
796 fn test_oid_non_minimal_subidentifier() {
797 // Non-minimal subidentifier encoding with leading 0x80 bytes should be accepted
798 // 0x80 0x01 should decode as 1 (non-minimal: minimal would be just 0x01)
799 // OID: 1.3 followed by arc 1 encoded as 0x80 0x01
800 let result = Oid::from_ber(&[0x2B, 0x80, 0x01]);
801 assert!(
802 result.is_ok(),
803 "should accept non-minimal subidentifier 0x80 0x01"
804 );
805 let oid = result.unwrap();
806 assert_eq!(oid.arcs(), &[1, 3, 1]);
807
808 // 0x80 0x80 0x01 should decode as 1 (two leading 0x80 bytes)
809 let result = Oid::from_ber(&[0x2B, 0x80, 0x80, 0x01]);
810 assert!(
811 result.is_ok(),
812 "should accept non-minimal subidentifier 0x80 0x80 0x01"
813 );
814 let oid = result.unwrap();
815 assert_eq!(oid.arcs(), &[1, 3, 1]);
816
817 // 0x80 0x00 should decode as 0 (non-minimal zero)
818 let result = Oid::from_ber(&[0x2B, 0x80, 0x00]);
819 assert!(
820 result.is_ok(),
821 "should accept non-minimal subidentifier 0x80 0x00"
822 );
823 let oid = result.unwrap();
824 assert_eq!(oid.arcs(), &[1, 3, 0]);
825 }
826
827 // Tests for MAX_OID_LEN validation
828 #[test]
829 fn test_validate_length_within_limit() {
830 // OID with MAX_OID_LEN arcs should be valid
831 let arcs: Vec<u32> = (0..MAX_OID_LEN as u32).collect();
832 let oid = Oid::new(arcs);
833 assert!(
834 oid.validate_length().is_ok(),
835 "OID with exactly MAX_OID_LEN arcs should be valid"
836 );
837 }
838
839 #[test]
840 fn test_validate_length_exceeds_limit() {
841 // OID with more than MAX_OID_LEN arcs should fail
842 let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
843 let oid = Oid::new(arcs);
844 let result = oid.validate_length();
845 assert!(
846 result.is_err(),
847 "OID exceeding MAX_OID_LEN should fail validation"
848 );
849 }
850
851 #[test]
852 fn test_validate_all_combines_checks() {
853 // Valid OID
854 let oid = Oid::from_slice(&[1, 3, 6, 1]);
855 assert!(oid.validate_all().is_ok());
856
857 // Invalid arc1 (fails validate)
858 let oid = Oid::from_slice(&[3, 0]);
859 assert!(oid.validate_all().is_err());
860
861 // Too many arcs (fails validate_length)
862 let arcs: Vec<u32> = (0..(MAX_OID_LEN + 1) as u32).collect();
863 let oid = Oid::new(arcs);
864 assert!(oid.validate_all().is_err());
865 }
866
867 #[test]
868 fn test_oid_fromstr() {
869 // Test basic parsing via FromStr trait
870 let oid: Oid = "1.3.6.1.2.1.1.1.0".parse().unwrap();
871 assert_eq!(oid, oid!(1, 3, 6, 1, 2, 1, 1, 1, 0));
872
873 // Test empty OID
874 let empty: Oid = "".parse().unwrap();
875 assert!(empty.is_empty());
876
877 // Test single arc
878 let single: Oid = "1".parse().unwrap();
879 assert_eq!(single.arcs(), &[1]);
880
881 // Test roundtrip Display -> FromStr
882 let original = oid!(1, 3, 6, 1, 4, 1, 9, 9, 42);
883 let displayed = original.to_string();
884 let parsed: Oid = displayed.parse().unwrap();
885 assert_eq!(original, parsed);
886 }
887
888 #[test]
889 fn test_oid_fromstr_invalid() {
890 // Invalid arc value
891 assert!("1.3.abc.1".parse::<Oid>().is_err());
892
893 // Negative number (parsed as invalid)
894 assert!("1.3.-6.1".parse::<Oid>().is_err());
895 }
896
897 // Test for first subidentifier overflow (arc1*40 + arc2 must fit in u32)
898 // When arc1=2, arc2 cannot exceed u32::MAX - 80
899 #[test]
900 fn test_validate_arc2_overflow_when_arc1_is_2() {
901 // Maximum valid arc2 when arc1=2: u32::MAX - 80 = 4294967215
902 let max_valid_arc2 = u32::MAX - 80;
903 let oid = Oid::from_slice(&[2, max_valid_arc2]);
904 assert!(
905 oid.validate().is_ok(),
906 "arc2={} with arc1=2 should be valid (max that fits)",
907 max_valid_arc2
908 );
909
910 // One more than max should fail validation
911 let overflow_arc2 = u32::MAX - 79; // 2*40 + this = u32::MAX + 1
912 let oid = Oid::from_slice(&[2, overflow_arc2]);
913 assert!(
914 oid.validate().is_err(),
915 "arc2={} with arc1=2 should be invalid (would overflow first subidentifier)",
916 overflow_arc2
917 );
918
919 // Also test arc2 = u32::MAX should definitely fail
920 let oid = Oid::from_slice(&[2, u32::MAX]);
921 assert!(
922 oid.validate().is_err(),
923 "arc2=u32::MAX with arc1=2 should be invalid"
924 );
925 }
926
927 #[test]
928 fn test_to_ber_checked_rejects_overflow() {
929 // Encoding an OID that would overflow should fail via to_ber_checked
930 let oid = Oid::from_slice(&[2, u32::MAX]);
931 let result = oid.to_ber_checked();
932 assert!(
933 result.is_err(),
934 "to_ber_checked should reject OID that would overflow"
935 );
936 }
937
938 #[test]
939 fn test_from_ber_enforces_max_oid_len() {
940 // Create BER data for an OID with more than MAX_OID_LEN arcs
941 // OID encoding: first subid encodes arc1*40+arc2, then each subsequent arc
942 // First subid gives us 2 arcs (e.g., 1 and 3), so we need MAX_OID_LEN - 2
943 // additional arcs to hit exactly MAX_OID_LEN.
944
945 // Build OID at exactly MAX_OID_LEN: 1.3 followed by (MAX_OID_LEN - 2) arcs of value 1
946 let mut ber_at_limit = vec![0x2B]; // First subid = 1*40 + 3 = 43 (encodes arc1=1, arc2=3)
947 ber_at_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 2));
948
949 let result = Oid::from_ber(&ber_at_limit);
950 assert!(
951 result.is_ok(),
952 "OID with exactly MAX_OID_LEN arcs should decode successfully"
953 );
954 assert_eq!(result.unwrap().len(), MAX_OID_LEN);
955
956 // Now one more arc should exceed the limit
957 let mut ber_over_limit = vec![0x2B]; // arc1=1, arc2=3
958 ber_over_limit.extend(std::iter::repeat_n(0x01, MAX_OID_LEN - 1));
959
960 let result = Oid::from_ber(&ber_over_limit);
961 assert!(
962 result.is_err(),
963 "OID exceeding MAX_OID_LEN should fail to decode"
964 );
965 }
966}