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