Skip to main content

async_snmp/ber/
decode.rs

1//! BER decoding.
2//!
3//! Zero-copy decoding using `Bytes` to avoid allocations.
4
5use std::net::SocketAddr;
6
7use super::length::decode_length;
8use super::tag;
9use crate::error::internal::DecodeErrorKind;
10use crate::error::{Error, Result, UNKNOWN_TARGET};
11use crate::oid::Oid;
12use bytes::Bytes;
13
14/// BER decoder that reads from a byte buffer.
15pub struct Decoder {
16    data: Bytes,
17    offset: usize,
18    target: Option<SocketAddr>,
19}
20
21impl Decoder {
22    /// Create a new decoder from bytes.
23    pub fn new(data: Bytes) -> Self {
24        Self {
25            data,
26            offset: 0,
27            target: None,
28        }
29    }
30
31    /// Create a decoder from bytes with a target address for error context.
32    pub fn with_target(data: Bytes, target: SocketAddr) -> Self {
33        Self {
34            data,
35            offset: 0,
36            target: Some(target),
37        }
38    }
39
40    /// Create a decoder from a byte slice (copies the data).
41    #[must_use]
42    pub fn from_slice(data: &[u8]) -> Self {
43        Self::new(Bytes::copy_from_slice(data))
44    }
45
46    /// Get the target address for error context.
47    fn target(&self) -> SocketAddr {
48        self.target.unwrap_or(UNKNOWN_TARGET)
49    }
50
51    /// Return a boxed `MalformedResponse` error for the current target.
52    fn malformed(&self) -> Box<crate::error::Error> {
53        Error::MalformedResponse {
54            target: self.target(),
55        }
56        .boxed()
57    }
58
59    /// Get the current offset.
60    pub fn offset(&self) -> usize {
61        self.offset
62    }
63
64    /// Get remaining bytes.
65    pub fn remaining(&self) -> usize {
66        self.data.len() - self.offset
67    }
68
69    /// Check if we've reached the end.
70    pub fn is_empty(&self) -> bool {
71        self.offset >= self.data.len()
72    }
73
74    /// Peek at the next byte without consuming it.
75    pub fn peek_byte(&self) -> Option<u8> {
76        if self.offset < self.data.len() {
77            Some(self.data[self.offset])
78        } else {
79            None
80        }
81    }
82
83    /// Peek at the next tag without consuming it.
84    ///
85    /// Returns `None` if the buffer is empty or if the next byte signals a
86    /// multi-byte tag (low five bits all set, i.e. `byte & 0x1F == 0x1F`).
87    /// Valid SNMP uses only single-byte tags (all defined tags are below 31).
88    pub fn peek_tag(&self) -> Option<u8> {
89        let byte = self.peek_byte()?;
90        if byte & 0x1F == 0x1F {
91            return None;
92        }
93        Some(byte)
94    }
95
96    /// Read a single byte.
97    pub fn read_byte(&mut self) -> Result<u8> {
98        if self.offset >= self.data.len() {
99            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::TruncatedData }, "truncated data: unexpected end of input");
100            return Err(self.malformed());
101        }
102        let byte = self.data[self.offset];
103        self.offset += 1;
104        Ok(byte)
105    }
106
107    /// Read a tag byte.
108    ///
109    /// Returns an error if the tag byte signals a multi-byte tag
110    /// (low five bits all set, i.e. `byte & 0x1F == 0x1F`).
111    /// Valid SNMP uses only single-byte tags (all defined tags are below 31).
112    pub fn read_tag(&mut self) -> Result<u8> {
113        let tag = self.read_byte()?;
114        if tag & 0x1F == 0x1F {
115            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset - 1, kind = %DecodeErrorKind::UnexpectedTag { expected: 0, actual: tag } }, "multi-byte tag not supported");
116            return Err(self.malformed());
117        }
118        Ok(tag)
119    }
120
121    /// Read a length and return (length, bytes consumed).
122    pub fn read_length(&mut self) -> Result<usize> {
123        let (len, consumed) = decode_length(&self.data[self.offset..], self.offset, self.target)?;
124        self.offset += consumed;
125        Ok(len)
126    }
127
128    /// Read raw bytes without copying.
129    pub fn read_bytes(&mut self, len: usize) -> Result<Bytes> {
130        // Use saturating_add to prevent overflow from bypassing bounds check
131        if self.offset.saturating_add(len) > self.data.len() {
132            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::InsufficientData { needed: len, available: self.remaining() } }, "insufficient data");
133            return Err(self.malformed());
134        }
135        let bytes = self.data.slice(self.offset..self.offset + len);
136        self.offset += len;
137        Ok(bytes)
138    }
139
140    /// Read and expect a specific tag, returning the content length.
141    pub fn expect_tag(&mut self, expected: u8) -> Result<usize> {
142        let tag = self.read_tag()?;
143        if tag != expected {
144            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset - 1, kind = %DecodeErrorKind::UnexpectedTag { expected, actual: tag } }, "unexpected tag");
145            return Err(self.malformed());
146        }
147        self.read_length()
148    }
149
150    /// Read a BER integer (signed).
151    pub fn read_integer(&mut self) -> Result<i32> {
152        let len = self.expect_tag(tag::universal::INTEGER)?;
153        self.read_integer_value(len)
154    }
155
156    /// Read integer value given the length.
157    pub fn read_integer_value(&mut self, len: usize) -> Result<i32> {
158        if len == 0 {
159            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::ZeroLengthInteger }, "zero-length integer");
160            return Err(self.malformed());
161        }
162        if len > 8 {
163            // Net-snmp accepts up to sizeof(long)=8 bytes for INTEGER; longer is truly malformed.
164            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::IntegerTooLong { length: len } }, "integer encoding too long");
165            return Err(self.malformed());
166        }
167
168        let bytes = self.read_bytes(len)?;
169
170        // Sign-extend into i64, then truncate to i32. Net-snmp does the same (CHECK_OVERFLOW_S)
171        // to stay compatible with devices that send oversized but otherwise valid encodings.
172        let is_negative = bytes[0] & 0x80 != 0;
173        let mut value: i64 = if is_negative { -1 } else { 0 };
174
175        for &byte in &bytes {
176            value = (value << 8) | i64::from(byte);
177        }
178
179        Ok(value as i32)
180    }
181
182    /// Read a 64-bit unsigned integer (Counter64).
183    pub fn read_integer64(&mut self, expected_tag: u8) -> Result<u64> {
184        let len = self.expect_tag(expected_tag)?;
185        self.read_integer64_value(len)
186    }
187
188    /// Read 64-bit unsigned integer value given the length.
189    pub fn read_integer64_value(&mut self, len: usize) -> Result<u64> {
190        if len == 0 {
191            // Net-snmp accepts zero-length Counter64 silently (loop runs 0 times, value is 0).
192            tracing::warn!(target: "async_snmp::ber", { snmp.offset = %self.offset }, "zero-length Counter64; interpreting as 0");
193            return Ok(0);
194        }
195        if len > 9 {
196            // 9 bytes max: 1 leading zero + 8 bytes for u64
197            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Integer64TooLong { length: len } }, "integer64 too long");
198            return Err(self.malformed());
199        }
200
201        let bytes = self.read_bytes(len)?;
202
203        if len == 9 && bytes[0] != 0x00 {
204            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Integer64MissingLeadingZero }, "9-octet integer64 missing leading zero");
205            return Err(self.malformed());
206        }
207
208        let mut value: u64 = 0;
209
210        for &byte in &bytes {
211            value = (value << 8) | u64::from(byte);
212        }
213
214        Ok(value)
215    }
216
217    /// Read an unsigned 32-bit integer with specific tag.
218    pub fn read_unsigned32(&mut self, expected_tag: u8) -> Result<u32> {
219        let len = self.expect_tag(expected_tag)?;
220        self.read_unsigned32_value(len)
221    }
222
223    /// Read unsigned 32-bit integer value given length.
224    pub fn read_unsigned32_value(&mut self, len: usize) -> Result<u32> {
225        if len == 0 {
226            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::ZeroLengthInteger }, "zero-length integer");
227            return Err(self.malformed());
228        }
229        if len > 9 {
230            // Net-snmp accepts up to sizeof(long)+1=9 bytes for unsigned32; longer is truly malformed.
231            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Unsigned32TooLong { length: len } }, "unsigned32 encoding too long");
232            return Err(self.malformed());
233        }
234
235        let bytes = self.read_bytes(len)?;
236
237        if len == 9 && bytes[0] != 0x00 {
238            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Unsigned32MissingLeadingZero }, "9-octet unsigned32 missing leading zero");
239            return Err(self.malformed());
240        }
241
242        // Accumulate into u64, then truncate to u32. Net-snmp does the same (CHECK_OVERFLOW_U)
243        // to stay compatible with devices that send oversized but otherwise valid encodings.
244        let mut value: u64 = 0;
245
246        for &byte in &bytes {
247            value = (value << 8) | u64::from(byte);
248        }
249
250        Ok(value as u32)
251    }
252
253    /// Read an OCTET STRING.
254    pub fn read_octet_string(&mut self) -> Result<Bytes> {
255        let len = self.expect_tag(tag::universal::OCTET_STRING)?;
256        self.read_bytes(len)
257    }
258
259    /// Read a NULL.
260    pub fn read_null(&mut self) -> Result<()> {
261        let len = self.expect_tag(tag::universal::NULL)?;
262        if len != 0 {
263            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::InvalidNull }, "NULL with non-zero length");
264            return Err(self.malformed());
265        }
266        Ok(())
267    }
268
269    /// Read an OBJECT IDENTIFIER.
270    pub fn read_oid(&mut self) -> Result<Oid> {
271        let len = self.expect_tag(tag::universal::OBJECT_IDENTIFIER)?;
272        let bytes = self.read_bytes(len)?;
273        Oid::from_ber(&bytes)
274    }
275
276    /// Read an OID given a pre-read length.
277    pub fn read_oid_value(&mut self, len: usize) -> Result<Oid> {
278        let bytes = self.read_bytes(len)?;
279        Oid::from_ber(&bytes)
280    }
281
282    /// Read a SEQUENCE, returning a decoder for its contents.
283    pub fn read_sequence(&mut self) -> Result<Decoder> {
284        self.read_constructed(tag::universal::SEQUENCE)
285    }
286
287    /// Read a constructed type with a specific tag, returning a decoder for its contents.
288    pub fn read_constructed(&mut self, expected_tag: u8) -> Result<Decoder> {
289        let len = self.expect_tag(expected_tag)?;
290        let content = self.read_bytes(len)?;
291        Ok(Decoder {
292            data: content,
293            offset: 0,
294            target: self.target,
295        })
296    }
297
298    /// Read an IP address.
299    pub fn read_ip_address(&mut self) -> Result<[u8; 4]> {
300        let len = self.expect_tag(tag::application::IP_ADDRESS)?;
301        if len != 4 {
302            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::InvalidIpAddressLength { length: len } }, "IP address must be 4 bytes");
303            return Err(self.malformed());
304        }
305        let bytes = self.read_bytes(4)?;
306        Ok([bytes[0], bytes[1], bytes[2], bytes[3]])
307    }
308
309    /// Skip a TLV (tag-length-value) without parsing.
310    pub fn skip_tlv(&mut self) -> Result<()> {
311        let _tag = self.read_tag()?;
312        let len = self.read_length()?;
313        // Use saturating_add and check BEFORE modifying offset to prevent overflow
314        let new_offset = self.offset.saturating_add(len);
315        if new_offset > self.data.len() {
316            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::TlvOverflow }, "TLV extends past end of data");
317            return Err(self.malformed());
318        }
319        self.offset = new_offset;
320        Ok(())
321    }
322
323    /// Create a sub-decoder for a portion of the remaining data.
324    pub fn sub_decoder(&mut self, len: usize) -> Result<Decoder> {
325        let content = self.read_bytes(len)?;
326        Ok(Decoder {
327            data: content,
328            offset: 0,
329            target: self.target,
330        })
331    }
332
333    /// Get the underlying bytes for the entire buffer.
334    pub fn as_bytes(&self) -> &Bytes {
335        &self.data
336    }
337
338    /// Get remaining data as a slice.
339    pub fn remaining_slice(&self) -> &[u8] {
340        &self.data[self.offset..]
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_decode_integer() {
350        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x00]);
351        assert_eq!(dec.read_integer().unwrap(), 0);
352
353        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x7F]);
354        assert_eq!(dec.read_integer().unwrap(), 127);
355
356        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x80]);
357        assert_eq!(dec.read_integer().unwrap(), 128);
358
359        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0xFF]);
360        assert_eq!(dec.read_integer().unwrap(), -1);
361
362        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x80]);
363        assert_eq!(dec.read_integer().unwrap(), -128);
364    }
365
366    #[test]
367    fn test_decode_null() {
368        let mut dec = Decoder::from_slice(&[0x05, 0x00]);
369        dec.read_null().unwrap();
370    }
371
372    #[test]
373    fn test_decode_octet_string() {
374        let mut dec = Decoder::from_slice(&[0x04, 0x05, b'h', b'e', b'l', b'l', b'o']);
375        let s = dec.read_octet_string().unwrap();
376        assert_eq!(&s[..], b"hello");
377    }
378
379    #[test]
380    fn test_decode_oid() {
381        // 1.3.6.1 = [0x2B, 0x06, 0x01]
382        let mut dec = Decoder::from_slice(&[0x06, 0x03, 0x2B, 0x06, 0x01]);
383        let oid = dec.read_oid().unwrap();
384        assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
385    }
386
387    #[test]
388    fn test_decode_sequence() {
389        // SEQUENCE { INTEGER 1, INTEGER 2 }
390        let mut dec = Decoder::from_slice(&[0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02]);
391        let mut seq = dec.read_sequence().unwrap();
392        assert_eq!(seq.read_integer().unwrap(), 1);
393        assert_eq!(seq.read_integer().unwrap(), 2);
394    }
395
396    #[test]
397    fn test_accept_non_minimal_integer() {
398        // Non-minimal encodings are accepted per X.690 permissive parsing (matches net-snmp)
399        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x01]);
400        assert_eq!(dec.read_integer().unwrap(), 1);
401
402        // 02 02 00 7F should decode as 127 (non-minimal: could be 02 01 7F)
403        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x7F]);
404        assert_eq!(dec.read_integer().unwrap(), 127);
405
406        // 02 03 00 00 80 should decode as 128 (non-minimal: could be 02 02 00 80)
407        let mut dec = Decoder::from_slice(&[0x02, 0x03, 0x00, 0x00, 0x80]);
408        assert_eq!(dec.read_integer().unwrap(), 128);
409
410        // 02 02 FF FF should decode as -1 (non-minimal: could be 02 01 FF)
411        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0xFF, 0xFF]);
412        assert_eq!(dec.read_integer().unwrap(), -1);
413    }
414
415    #[test]
416    fn test_integer_too_long_truncates() {
417        // 5-8 byte integers are accepted and truncated to i32, matching net-snmp CHECK_OVERFLOW_S.
418        // 5 bytes: 0x0102030405 -> truncated to 0x02030405
419        let mut dec = Decoder::from_slice(&[0x02, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]);
420        assert_eq!(dec.read_integer().unwrap(), 0x02_03_04_05_i32);
421
422        // 8 bytes: last 4 bytes kept
423        let mut dec =
424            Decoder::from_slice(&[0x02, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
425        assert_eq!(dec.read_integer().unwrap(), 0x05_06_07_08_i32);
426
427        // 9 bytes is rejected (exceeds net-snmp's sizeof(long)=8 limit)
428        let mut dec = Decoder::from_slice(&[
429            0x02, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
430        ]);
431        assert!(
432            dec.read_integer().is_err(),
433            "9-byte integer must be rejected"
434        );
435    }
436
437    #[test]
438    fn test_unsigned32_too_long_truncates() {
439        // 6-9 byte unsigned32 values are accepted and truncated to u32, matching net-snmp CHECK_OVERFLOW_U.
440        // 6 bytes: 0x010203040506 -> truncated to 0x03040506
441        let mut dec = Decoder::from_slice(&[0x42, 0x06, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
442        assert_eq!(dec.read_unsigned32(0x42).unwrap(), 0x03_04_05_06_u32);
443
444        // 9 bytes with leading zero: accepted, value fits in u32
445        let mut dec = Decoder::from_slice(&[
446            0x42, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
447        ]);
448        assert_eq!(dec.read_unsigned32(0x42).unwrap(), u32::MAX);
449
450        // 10 bytes is rejected (exceeds net-snmp's sizeof(long)+1=9 limit)
451        let mut dec = Decoder::from_slice(&[
452            0x42, 0x0A, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
453        ]);
454        assert!(
455            dec.read_unsigned32(0x42).is_err(),
456            "10-byte unsigned32 must be rejected"
457        );
458    }
459
460    #[test]
461    fn test_zero_length_counter64_accepted() {
462        // Net-snmp accepts zero-length Counter64, producing 0. We match that.
463        let mut dec = Decoder::from_slice(&[0x46, 0x00]);
464        let result = dec.read_integer64(0x46);
465        assert!(result.is_ok(), "zero-length Counter64 should be accepted");
466        assert_eq!(result.unwrap(), 0);
467    }
468
469    #[test]
470    fn test_counter64_nine_bytes_requires_leading_zero() {
471        // 9-byte Counter64 with a non-zero first byte must be rejected (BER requires 0x00)
472        // Tag 0x46 = Counter64
473        let mut dec = Decoder::from_slice(&[
474            0x46, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
475        ]);
476        let result = dec.read_integer64(0x46);
477        assert!(
478            result.is_err(),
479            "expected error for 9-byte Counter64 without leading zero"
480        );
481
482        // 9-byte Counter64 with 0x00 first byte must be accepted
483        let mut dec = Decoder::from_slice(&[
484            0x46, 0x09, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
485        ]);
486        let result = dec.read_integer64(0x46);
487        assert!(
488            result.is_ok(),
489            "expected success for 9-byte Counter64 with leading zero"
490        );
491        assert_eq!(result.unwrap(), u64::MAX);
492    }
493
494    #[test]
495    fn test_unsigned32_nine_bytes_requires_leading_zero() {
496        // 9-byte unsigned32 without a leading zero is rejected (matches net-snmp).
497        let mut dec = Decoder::from_slice(&[
498            0x42, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
499        ]);
500        assert!(
501            dec.read_unsigned32(0x42).is_err(),
502            "9-byte unsigned32 without leading zero must be rejected"
503        );
504
505        // 9-byte unsigned32 with 0x00 first byte is accepted and truncated to u32.
506        let mut dec = Decoder::from_slice(&[
507            0x42, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
508        ]);
509        assert_eq!(dec.read_unsigned32(0x42).unwrap(), u32::MAX);
510
511        // 5-byte unsigned32 with a non-zero first byte is accepted and truncated (matches net-snmp).
512        let mut dec = Decoder::from_slice(&[0x42, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00]);
513        assert_eq!(dec.read_unsigned32(0x42).unwrap(), 0u32);
514    }
515
516    #[test]
517    fn test_read_bytes_rejects_oversized_length() {
518        // When length exceeds remaining data, should return MalformedResponse error
519        let mut dec = Decoder::from_slice(&[0x01, 0x02, 0x03]);
520        // Try to read more bytes than available
521        let result = dec.read_bytes(100);
522        assert!(result.is_err());
523        let err = result.unwrap_err();
524        assert!(
525            matches!(*err, crate::error::Error::MalformedResponse { .. }),
526            "expected MalformedResponse error, got {err:?}"
527        );
528    }
529
530    #[test]
531    fn test_skip_tlv_rejects_oversized_length() {
532        // TLV with length claiming more bytes than available
533        // Tag 0x04 (OCTET STRING), Length 0x82 0x01 0x00 (256 bytes), but only 3 content bytes
534        let mut dec = Decoder::from_slice(&[0x04, 0x82, 0x01, 0x00, 0xAA, 0xBB, 0xCC]);
535        let result = dec.skip_tlv();
536        assert!(result.is_err());
537        let err = result.unwrap_err();
538        assert!(
539            matches!(*err, crate::error::Error::MalformedResponse { .. }),
540            "expected MalformedResponse error, got {err:?}"
541        );
542    }
543
544    #[test]
545    fn test_read_tag_rejects_multi_byte_tag() {
546        // A tag byte with all 5 lower bits set (0x1F) signals a multi-byte tag in BER.
547        // Valid SNMP uses single-byte tags only, so this must be rejected.
548        let mut dec = Decoder::from_slice(&[0x1F, 0x02, 0x00]);
549        let result = dec.read_tag();
550        assert!(result.is_err());
551        let err = result.unwrap_err();
552        assert!(
553            matches!(*err, crate::error::Error::MalformedResponse { .. }),
554            "expected MalformedResponse error for multi-byte tag, got {err:?}"
555        );
556
557        // 0x3F: constructed form with tag bits all set - also multi-byte
558        let mut dec = Decoder::from_slice(&[0x3F, 0x02, 0x00]);
559        let result = dec.read_tag();
560        assert!(result.is_err());
561
562        // 0x9F: context-specific, primitive, multi-byte
563        let mut dec = Decoder::from_slice(&[0x9F, 0x02, 0x00]);
564        let result = dec.read_tag();
565        assert!(result.is_err());
566
567        // Normal single-byte tags must still be accepted
568        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x00]);
569        let result = dec.read_tag();
570        assert!(result.is_ok());
571        assert_eq!(result.unwrap(), 0x02);
572    }
573
574    #[test]
575    fn test_peek_tag_rejects_multi_byte_tag() {
576        // peek_tag must also reject multi-byte tags
577        let dec = Decoder::from_slice(&[0x1F, 0x02, 0x00]);
578        let result = dec.peek_tag();
579        assert!(
580            result.is_none(),
581            "peek_tag should return None for multi-byte tag"
582        );
583
584        // Normal tag should peek as Some
585        let dec = Decoder::from_slice(&[0x30, 0x00]);
586        let result = dec.peek_tag();
587        assert_eq!(result, Some(0x30));
588    }
589}