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 a BER integer whose ASN.1 type constrains it to an `i32` range.
157    ///
158    /// Unlike [`Self::read_integer`], this checks the complete decoded value
159    /// before narrowing it. This prevents over-width encodings such as
160    /// `2^32` from aliasing an in-range value after truncation.
161    pub(crate) fn read_bounded_integer(&mut self, minimum: i32, maximum: i32) -> Result<i32> {
162        debug_assert!(minimum <= maximum);
163
164        let len = self.expect_tag(tag::universal::INTEGER)?;
165        let value = self.read_signed_integer_value(len)?;
166        if value < i64::from(minimum) || value > i64::from(maximum) {
167            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::IntegerOutOfRange { value, minimum, maximum } }, "integer outside constrained range");
168            return Err(self.malformed());
169        }
170
171        Ok(value as i32)
172    }
173
174    /// Read integer value given the length.
175    pub fn read_integer_value(&mut self, len: usize) -> Result<i32> {
176        // The generic SNMP INTEGER path deliberately follows net-snmp's
177        // permissive truncation behavior. ASN.1 fields with narrower ranges
178        // must use `read_bounded_integer` instead.
179        Ok(self.read_signed_integer_value(len)? as i32)
180    }
181
182    /// Read the complete signed value of an INTEGER accepted by the generic
183    /// BER parser, without narrowing it to the public `i32` representation.
184    fn read_signed_integer_value(&mut self, len: usize) -> Result<i64> {
185        if len == 0 {
186            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::ZeroLengthInteger }, "zero-length integer");
187            return Err(self.malformed());
188        }
189        if len > 8 {
190            // Net-snmp accepts up to sizeof(long)=8 bytes for INTEGER; longer is truly malformed.
191            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::IntegerTooLong { length: len } }, "integer encoding too long");
192            return Err(self.malformed());
193        }
194
195        let bytes = self.read_bytes(len)?;
196
197        // Sign-extend into i64. The generic caller truncates this to i32 to
198        // match net-snmp's CHECK_OVERFLOW_S compatibility behavior.
199        let is_negative = bytes[0] & 0x80 != 0;
200        let mut value: i64 = if is_negative { -1 } else { 0 };
201
202        for &byte in &bytes {
203            value = (value << 8) | i64::from(byte);
204        }
205
206        Ok(value)
207    }
208
209    /// Read a 64-bit unsigned integer (Counter64).
210    pub fn read_integer64(&mut self, expected_tag: u8) -> Result<u64> {
211        let len = self.expect_tag(expected_tag)?;
212        self.read_integer64_value(len)
213    }
214
215    /// Read 64-bit unsigned integer value given the length.
216    pub fn read_integer64_value(&mut self, len: usize) -> Result<u64> {
217        if len == 0 {
218            // Net-snmp accepts zero-length Counter64 silently (loop runs 0 times, value is 0).
219            tracing::warn!(target: "async_snmp::ber", { snmp.offset = %self.offset }, "zero-length Counter64; interpreting as 0");
220            return Ok(0);
221        }
222        if len > 9 {
223            // 9 bytes max: 1 leading zero + 8 bytes for u64
224            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Integer64TooLong { length: len } }, "integer64 too long");
225            return Err(self.malformed());
226        }
227
228        let bytes = self.read_bytes(len)?;
229
230        if len == 9 && bytes[0] != 0x00 {
231            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Integer64MissingLeadingZero }, "9-octet integer64 missing leading zero");
232            return Err(self.malformed());
233        }
234
235        let mut value: u64 = 0;
236
237        for &byte in &bytes {
238            value = (value << 8) | u64::from(byte);
239        }
240
241        Ok(value)
242    }
243
244    /// Read an unsigned 32-bit integer with specific tag.
245    pub fn read_unsigned32(&mut self, expected_tag: u8) -> Result<u32> {
246        let len = self.expect_tag(expected_tag)?;
247        self.read_unsigned32_value(len)
248    }
249
250    /// Read unsigned 32-bit integer value given length.
251    pub fn read_unsigned32_value(&mut self, len: usize) -> Result<u32> {
252        if len == 0 {
253            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::ZeroLengthInteger }, "zero-length integer");
254            return Err(self.malformed());
255        }
256        if len > 9 {
257            // Net-snmp accepts up to sizeof(long)+1=9 bytes for unsigned32; longer is truly malformed.
258            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Unsigned32TooLong { length: len } }, "unsigned32 encoding too long");
259            return Err(self.malformed());
260        }
261
262        let bytes = self.read_bytes(len)?;
263
264        if len == 9 && bytes[0] != 0x00 {
265            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::Unsigned32MissingLeadingZero }, "9-octet unsigned32 missing leading zero");
266            return Err(self.malformed());
267        }
268
269        // Accumulate into u64, then truncate to u32. Net-snmp does the same (CHECK_OVERFLOW_U)
270        // to stay compatible with devices that send oversized but otherwise valid encodings.
271        let mut value: u64 = 0;
272
273        for &byte in &bytes {
274            value = (value << 8) | u64::from(byte);
275        }
276
277        Ok(value as u32)
278    }
279
280    /// Read an OCTET STRING.
281    pub fn read_octet_string(&mut self) -> Result<Bytes> {
282        let len = self.expect_tag(tag::universal::OCTET_STRING)?;
283        self.read_bytes(len)
284    }
285
286    /// Read a NULL.
287    pub fn read_null(&mut self) -> Result<()> {
288        let len = self.expect_tag(tag::universal::NULL)?;
289        if len != 0 {
290            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::InvalidNull }, "NULL with non-zero length");
291            return Err(self.malformed());
292        }
293        Ok(())
294    }
295
296    /// Read an OBJECT IDENTIFIER.
297    pub fn read_oid(&mut self) -> Result<Oid> {
298        let len = self.expect_tag(tag::universal::OBJECT_IDENTIFIER)?;
299        let bytes = self.read_bytes(len)?;
300        Oid::from_ber(&bytes)
301    }
302
303    /// Read an OID given a pre-read length.
304    pub fn read_oid_value(&mut self, len: usize) -> Result<Oid> {
305        let bytes = self.read_bytes(len)?;
306        Oid::from_ber(&bytes)
307    }
308
309    /// Read a SEQUENCE, returning a decoder for its contents.
310    pub fn read_sequence(&mut self) -> Result<Decoder> {
311        self.read_constructed(tag::universal::SEQUENCE)
312    }
313
314    /// Read a constructed type with a specific tag, returning a decoder for its contents.
315    pub fn read_constructed(&mut self, expected_tag: u8) -> Result<Decoder> {
316        let len = self.expect_tag(expected_tag)?;
317        let content = self.read_bytes(len)?;
318        Ok(Decoder {
319            data: content,
320            offset: 0,
321            target: self.target,
322        })
323    }
324
325    /// Read an IP address.
326    pub fn read_ip_address(&mut self) -> Result<[u8; 4]> {
327        let len = self.expect_tag(tag::application::IP_ADDRESS)?;
328        if len != 4 {
329            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::InvalidIpAddressLength { length: len } }, "IP address must be 4 bytes");
330            return Err(self.malformed());
331        }
332        let bytes = self.read_bytes(4)?;
333        Ok([bytes[0], bytes[1], bytes[2], bytes[3]])
334    }
335
336    /// Skip a TLV (tag-length-value) without parsing.
337    pub fn skip_tlv(&mut self) -> Result<()> {
338        let _tag = self.read_tag()?;
339        let len = self.read_length()?;
340        // Use saturating_add and check BEFORE modifying offset to prevent overflow
341        let new_offset = self.offset.saturating_add(len);
342        if new_offset > self.data.len() {
343            tracing::debug!(target: "async_snmp::ber", { snmp.offset = %self.offset, kind = %DecodeErrorKind::TlvOverflow }, "TLV extends past end of data");
344            return Err(self.malformed());
345        }
346        self.offset = new_offset;
347        Ok(())
348    }
349
350    /// Create a sub-decoder for a portion of the remaining data.
351    pub fn sub_decoder(&mut self, len: usize) -> Result<Decoder> {
352        let content = self.read_bytes(len)?;
353        Ok(Decoder {
354            data: content,
355            offset: 0,
356            target: self.target,
357        })
358    }
359
360    /// Get the underlying bytes for the entire buffer.
361    pub fn as_bytes(&self) -> &Bytes {
362        &self.data
363    }
364
365    /// Get remaining data as a slice.
366    pub fn remaining_slice(&self) -> &[u8] {
367        &self.data[self.offset..]
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_decode_integer() {
377        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x00]);
378        assert_eq!(dec.read_integer().unwrap(), 0);
379
380        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x7F]);
381        assert_eq!(dec.read_integer().unwrap(), 127);
382
383        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x80]);
384        assert_eq!(dec.read_integer().unwrap(), 128);
385
386        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0xFF]);
387        assert_eq!(dec.read_integer().unwrap(), -1);
388
389        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x80]);
390        assert_eq!(dec.read_integer().unwrap(), -128);
391    }
392
393    #[test]
394    fn test_decode_null() {
395        let mut dec = Decoder::from_slice(&[0x05, 0x00]);
396        dec.read_null().unwrap();
397    }
398
399    #[test]
400    fn test_decode_octet_string() {
401        let mut dec = Decoder::from_slice(&[0x04, 0x05, b'h', b'e', b'l', b'l', b'o']);
402        let s = dec.read_octet_string().unwrap();
403        assert_eq!(&s[..], b"hello");
404    }
405
406    #[test]
407    fn test_decode_oid() {
408        // 1.3.6.1 = [0x2B, 0x06, 0x01]
409        let mut dec = Decoder::from_slice(&[0x06, 0x03, 0x2B, 0x06, 0x01]);
410        let oid = dec.read_oid().unwrap();
411        assert_eq!(oid.arcs(), &[1, 3, 6, 1]);
412    }
413
414    #[test]
415    fn test_decode_sequence() {
416        // SEQUENCE { INTEGER 1, INTEGER 2 }
417        let mut dec = Decoder::from_slice(&[0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02]);
418        let mut seq = dec.read_sequence().unwrap();
419        assert_eq!(seq.read_integer().unwrap(), 1);
420        assert_eq!(seq.read_integer().unwrap(), 2);
421    }
422
423    #[test]
424    fn test_accept_non_minimal_integer() {
425        // Non-minimal encodings are accepted per X.690 permissive parsing (matches net-snmp)
426        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x01]);
427        assert_eq!(dec.read_integer().unwrap(), 1);
428
429        // 02 02 00 7F should decode as 127 (non-minimal: could be 02 01 7F)
430        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0x00, 0x7F]);
431        assert_eq!(dec.read_integer().unwrap(), 127);
432
433        // 02 03 00 00 80 should decode as 128 (non-minimal: could be 02 02 00 80)
434        let mut dec = Decoder::from_slice(&[0x02, 0x03, 0x00, 0x00, 0x80]);
435        assert_eq!(dec.read_integer().unwrap(), 128);
436
437        // 02 02 FF FF should decode as -1 (non-minimal: could be 02 01 FF)
438        let mut dec = Decoder::from_slice(&[0x02, 0x02, 0xFF, 0xFF]);
439        assert_eq!(dec.read_integer().unwrap(), -1);
440    }
441
442    #[test]
443    fn test_integer_too_long_truncates() {
444        // 5-8 byte integers are accepted and truncated to i32, matching net-snmp CHECK_OVERFLOW_S.
445        // 5 bytes: 0x0102030405 -> truncated to 0x02030405
446        let mut dec = Decoder::from_slice(&[0x02, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]);
447        assert_eq!(dec.read_integer().unwrap(), 0x02_03_04_05_i32);
448
449        // 8 bytes: last 4 bytes kept
450        let mut dec =
451            Decoder::from_slice(&[0x02, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
452        assert_eq!(dec.read_integer().unwrap(), 0x05_06_07_08_i32);
453
454        // 9 bytes is rejected (exceeds net-snmp's sizeof(long)=8 limit)
455        let mut dec = Decoder::from_slice(&[
456            0x02, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
457        ]);
458        assert!(
459            dec.read_integer().is_err(),
460            "9-byte integer must be rejected"
461        );
462    }
463
464    #[test]
465    fn bounded_integer_rejects_values_that_generic_decode_truncates() {
466        const TWO_TO_32: &[u8] = &[0x02, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00];
467        const NEGATIVE_TWO_TO_32: &[u8] = &[0x02, 0x05, 0xFF, 0x00, 0x00, 0x00, 0x00];
468
469        let mut generic = Decoder::from_slice(TWO_TO_32);
470        assert_eq!(generic.read_integer().unwrap(), 0);
471        let mut bounded = Decoder::from_slice(TWO_TO_32);
472        assert!(bounded.read_bounded_integer(0, i32::MAX).is_err());
473
474        let mut generic = Decoder::from_slice(NEGATIVE_TWO_TO_32);
475        assert_eq!(generic.read_integer().unwrap(), 0);
476        let mut bounded = Decoder::from_slice(NEGATIVE_TWO_TO_32);
477        assert!(bounded.read_bounded_integer(0, i32::MAX).is_err());
478
479        let mut lower_bound = Decoder::from_slice(&[0x02, 0x01, 0x00]);
480        assert_eq!(lower_bound.read_bounded_integer(0, i32::MAX).unwrap(), 0);
481        let mut upper_bound = Decoder::from_slice(&[0x02, 0x04, 0x7F, 0xFF, 0xFF, 0xFF]);
482        assert_eq!(
483            upper_bound.read_bounded_integer(0, i32::MAX).unwrap(),
484            i32::MAX
485        );
486    }
487
488    #[test]
489    fn test_unsigned32_too_long_truncates() {
490        // 6-9 byte unsigned32 values are accepted and truncated to u32, matching net-snmp CHECK_OVERFLOW_U.
491        // 6 bytes: 0x010203040506 -> truncated to 0x03040506
492        let mut dec = Decoder::from_slice(&[0x42, 0x06, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
493        assert_eq!(dec.read_unsigned32(0x42).unwrap(), 0x03_04_05_06_u32);
494
495        // 9 bytes with leading zero: accepted, value fits in u32
496        let mut dec = Decoder::from_slice(&[
497            0x42, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
498        ]);
499        assert_eq!(dec.read_unsigned32(0x42).unwrap(), u32::MAX);
500
501        // 10 bytes is rejected (exceeds net-snmp's sizeof(long)+1=9 limit)
502        let mut dec = Decoder::from_slice(&[
503            0x42, 0x0A, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
504        ]);
505        assert!(
506            dec.read_unsigned32(0x42).is_err(),
507            "10-byte unsigned32 must be rejected"
508        );
509    }
510
511    #[test]
512    fn test_zero_length_counter64_accepted() {
513        // Net-snmp accepts zero-length Counter64, producing 0. We match that.
514        let mut dec = Decoder::from_slice(&[0x46, 0x00]);
515        let result = dec.read_integer64(0x46);
516        assert!(result.is_ok(), "zero-length Counter64 should be accepted");
517        assert_eq!(result.unwrap(), 0);
518    }
519
520    #[test]
521    fn test_counter64_nine_bytes_requires_leading_zero() {
522        // 9-byte Counter64 with a non-zero first byte must be rejected (BER requires 0x00)
523        // Tag 0x46 = Counter64
524        let mut dec = Decoder::from_slice(&[
525            0x46, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
526        ]);
527        let result = dec.read_integer64(0x46);
528        assert!(
529            result.is_err(),
530            "expected error for 9-byte Counter64 without leading zero"
531        );
532
533        // 9-byte Counter64 with 0x00 first byte must be accepted
534        let mut dec = Decoder::from_slice(&[
535            0x46, 0x09, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
536        ]);
537        let result = dec.read_integer64(0x46);
538        assert!(
539            result.is_ok(),
540            "expected success for 9-byte Counter64 with leading zero"
541        );
542        assert_eq!(result.unwrap(), u64::MAX);
543    }
544
545    #[test]
546    fn test_unsigned32_nine_bytes_requires_leading_zero() {
547        // 9-byte unsigned32 without a leading zero is rejected (matches net-snmp).
548        let mut dec = Decoder::from_slice(&[
549            0x42, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
550        ]);
551        assert!(
552            dec.read_unsigned32(0x42).is_err(),
553            "9-byte unsigned32 without leading zero must be rejected"
554        );
555
556        // 9-byte unsigned32 with 0x00 first byte is accepted and truncated to u32.
557        let mut dec = Decoder::from_slice(&[
558            0x42, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
559        ]);
560        assert_eq!(dec.read_unsigned32(0x42).unwrap(), u32::MAX);
561
562        // 5-byte unsigned32 with a non-zero first byte is accepted and truncated (matches net-snmp).
563        let mut dec = Decoder::from_slice(&[0x42, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00]);
564        assert_eq!(dec.read_unsigned32(0x42).unwrap(), 0u32);
565    }
566
567    #[test]
568    fn test_read_bytes_rejects_oversized_length() {
569        // When length exceeds remaining data, should return MalformedResponse error
570        let mut dec = Decoder::from_slice(&[0x01, 0x02, 0x03]);
571        // Try to read more bytes than available
572        let result = dec.read_bytes(100);
573        assert!(result.is_err());
574        let err = result.unwrap_err();
575        assert!(
576            matches!(*err, crate::error::Error::MalformedResponse { .. }),
577            "expected MalformedResponse error, got {err:?}"
578        );
579    }
580
581    #[test]
582    fn test_skip_tlv_rejects_oversized_length() {
583        // TLV with length claiming more bytes than available
584        // Tag 0x04 (OCTET STRING), Length 0x82 0x01 0x00 (256 bytes), but only 3 content bytes
585        let mut dec = Decoder::from_slice(&[0x04, 0x82, 0x01, 0x00, 0xAA, 0xBB, 0xCC]);
586        let result = dec.skip_tlv();
587        assert!(result.is_err());
588        let err = result.unwrap_err();
589        assert!(
590            matches!(*err, crate::error::Error::MalformedResponse { .. }),
591            "expected MalformedResponse error, got {err:?}"
592        );
593    }
594
595    #[test]
596    fn test_read_tag_rejects_multi_byte_tag() {
597        // A tag byte with all 5 lower bits set (0x1F) signals a multi-byte tag in BER.
598        // Valid SNMP uses single-byte tags only, so this must be rejected.
599        let mut dec = Decoder::from_slice(&[0x1F, 0x02, 0x00]);
600        let result = dec.read_tag();
601        assert!(result.is_err());
602        let err = result.unwrap_err();
603        assert!(
604            matches!(*err, crate::error::Error::MalformedResponse { .. }),
605            "expected MalformedResponse error for multi-byte tag, got {err:?}"
606        );
607
608        // 0x3F: constructed form with tag bits all set - also multi-byte
609        let mut dec = Decoder::from_slice(&[0x3F, 0x02, 0x00]);
610        let result = dec.read_tag();
611        assert!(result.is_err());
612
613        // 0x9F: context-specific, primitive, multi-byte
614        let mut dec = Decoder::from_slice(&[0x9F, 0x02, 0x00]);
615        let result = dec.read_tag();
616        assert!(result.is_err());
617
618        // Normal single-byte tags must still be accepted
619        let mut dec = Decoder::from_slice(&[0x02, 0x01, 0x00]);
620        let result = dec.read_tag();
621        assert!(result.is_ok());
622        assert_eq!(result.unwrap(), 0x02);
623    }
624
625    #[test]
626    fn test_peek_tag_rejects_multi_byte_tag() {
627        // peek_tag must also reject multi-byte tags
628        let dec = Decoder::from_slice(&[0x1F, 0x02, 0x00]);
629        let result = dec.peek_tag();
630        assert!(
631            result.is_none(),
632            "peek_tag should return None for multi-byte tag"
633        );
634
635        // Normal tag should peek as Some
636        let dec = Decoder::from_slice(&[0x30, 0x00]);
637        let result = dec.peek_tag();
638        assert_eq!(result, Some(0x30));
639    }
640}