Skip to main content

bacnet_encoding/
tags.rs

1//! BACnet ASN.1 tag encoding and decoding per ASHRAE 135-2020 Clause 20.2.1.
2//!
3//! BACnet uses a tag-length-value (TLV) encoding with two tag classes:
4//! - **Application** tags identify the datatype (Null=0, Boolean=1, ..., ObjectIdentifier=12)
5//! - **Context** tags identify a field within a constructed type (tag number = field index)
6//!
7//! Tags also support opening/closing markers for constructed (nested) values.
8
9use bacnet_types::error::Error;
10use bytes::{BufMut, BytesMut};
11
12/// Tag class: application (datatype) or context (field identifier).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum TagClass {
16    /// Application tag -- tag number identifies the BACnet datatype.
17    Application = 0,
18    /// Context-specific tag -- tag number identifies a field in a constructed type.
19    Context = 1,
20}
21
22/// Application tag numbers per Clause 20.2.1.4.
23pub mod app_tag {
24    pub const NULL: u8 = 0;
25    pub const BOOLEAN: u8 = 1;
26    pub const UNSIGNED: u8 = 2;
27    pub const SIGNED: u8 = 3;
28    pub const REAL: u8 = 4;
29    pub const DOUBLE: u8 = 5;
30    pub const OCTET_STRING: u8 = 6;
31    pub const CHARACTER_STRING: u8 = 7;
32    pub const BIT_STRING: u8 = 8;
33    pub const ENUMERATED: u8 = 9;
34    pub const DATE: u8 = 10;
35    pub const TIME: u8 = 11;
36    pub const OBJECT_IDENTIFIER: u8 = 12;
37}
38
39/// A decoded BACnet tag header.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct Tag {
42    /// Tag number: datatype for application tags, field index for context tags.
43    pub number: u8,
44    /// Tag class (application or context).
45    pub class: TagClass,
46    /// Content length in bytes. For application booleans, this holds the raw
47    /// L/V/T value (0 = false, nonzero = true) with no content octets.
48    pub length: u32,
49    /// Whether this is a context-specific opening tag (L/V/T = 6).
50    pub is_opening: bool,
51    /// Whether this is a context-specific closing tag (L/V/T = 7).
52    pub is_closing: bool,
53}
54
55impl Tag {
56    /// Check if this is an application boolean tag with value true.
57    ///
58    /// Per Clause 20.2.3, application-tagged booleans encode the value
59    /// in the tag's L/V/T field with no content octets.
60    pub fn is_boolean_true(&self) -> bool {
61        self.class == TagClass::Application && self.number == app_tag::BOOLEAN && self.length != 0
62    }
63
64    /// Check if this is a context tag matching the given number (not opening/closing).
65    pub fn is_context(&self, number: u8) -> bool {
66        self.class == TagClass::Context
67            && self.number == number
68            && !self.is_opening
69            && !self.is_closing
70    }
71
72    /// Check if this is an opening tag matching the given number.
73    pub fn is_opening_tag(&self, number: u8) -> bool {
74        self.class == TagClass::Context && self.number == number && self.is_opening
75    }
76
77    /// Check if this is a closing tag matching the given number.
78    pub fn is_closing_tag(&self, number: u8) -> bool {
79        self.class == TagClass::Context && self.number == number && self.is_closing
80    }
81}
82
83// ---------------------------------------------------------------------------
84// Encoding
85// ---------------------------------------------------------------------------
86
87/// Encode a tag header into the buffer.
88///
89/// For application tags, `tag_number` identifies the datatype (0-12).
90/// For context tags, `tag_number` is the field identifier (0-254).
91pub fn encode_tag(buf: &mut BytesMut, tag_number: u8, class: TagClass, length: u32) {
92    let cls_bit = (class as u8) << 3;
93
94    if tag_number <= 14 && length <= 4 {
95        // Fast path: single byte (covers ~95% of cases)
96        buf.put_u8((tag_number << 4) | cls_bit | (length as u8));
97        return;
98    }
99
100    // Build initial octet
101    let tag_nibble = if tag_number <= 14 {
102        tag_number << 4
103    } else {
104        0xF0 // Extended tag number marker
105    };
106
107    if length <= 4 {
108        buf.put_u8(tag_nibble | cls_bit | (length as u8));
109        if tag_number > 14 {
110            buf.put_u8(tag_number);
111        }
112        return;
113    }
114
115    // Extended length (L/V/T = 5)
116    buf.put_u8(tag_nibble | cls_bit | 5);
117    if tag_number > 14 {
118        buf.put_u8(tag_number);
119    }
120
121    if length <= 253 {
122        buf.put_u8(length as u8);
123    } else if length <= 65535 {
124        buf.put_u8(254);
125        buf.put_u16(length as u16);
126    } else {
127        buf.put_u8(255);
128        buf.put_u32(length);
129    }
130}
131
132/// Encode a context-specific opening tag.
133pub fn encode_opening_tag(buf: &mut BytesMut, tag_number: u8) {
134    if tag_number <= 14 {
135        buf.put_u8((tag_number << 4) | 0x0E);
136    } else {
137        buf.put_u8(0xFE);
138        buf.put_u8(tag_number);
139    }
140}
141
142/// Encode a context-specific closing tag.
143pub fn encode_closing_tag(buf: &mut BytesMut, tag_number: u8) {
144    if tag_number <= 14 {
145        buf.put_u8((tag_number << 4) | 0x0F);
146    } else {
147        buf.put_u8(0xFF);
148        buf.put_u8(tag_number);
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Decoding
154// ---------------------------------------------------------------------------
155
156/// Maximum tag length sanity limit (1 MB). BACnet APDUs are at most ~64KB
157/// segmented; 1MB is generous. Prevents memory exhaustion from malformed packets.
158const MAX_TAG_LENGTH: u32 = 1_048_576;
159
160/// Maximum nesting depth for context tags to prevent stack overflow from crafted packets.
161pub const MAX_CONTEXT_NESTING_DEPTH: usize = 32;
162
163/// Decode a tag from `data` starting at `offset`.
164///
165/// Returns the decoded [`Tag`] and the new offset past the tag header.
166pub fn decode_tag(data: &[u8], offset: usize) -> Result<(Tag, usize), Error> {
167    if offset >= data.len() {
168        return Err(Error::decoding(
169            offset,
170            "tag decode: offset beyond buffer length",
171        ));
172    }
173
174    let initial = data[offset];
175    let mut pos = offset + 1;
176
177    // Extract fields from initial octet
178    let mut tag_number = (initial >> 4) & 0x0F;
179    let class = if (initial >> 3) & 0x01 == 1 {
180        TagClass::Context
181    } else {
182        TagClass::Application
183    };
184    let lvt = initial & 0x07;
185
186    // Extended tag number (tag nibble = 0x0F)
187    if tag_number == 0x0F {
188        if pos >= data.len() {
189            return Err(Error::decoding(pos, "truncated extended tag number"));
190        }
191        tag_number = data[pos];
192        // Note: for extended tags, tag_number is u8 (0-254).
193        // We store as u8 which is fine.
194        pos += 1;
195    }
196
197    // Opening/closing tags (context class only)
198    if class == TagClass::Context {
199        if lvt == 6 {
200            return Ok((
201                Tag {
202                    number: tag_number,
203                    class,
204                    length: 0,
205                    is_opening: true,
206                    is_closing: false,
207                },
208                pos,
209            ));
210        }
211        if lvt == 7 {
212            return Ok((
213                Tag {
214                    number: tag_number,
215                    class,
216                    length: 0,
217                    is_opening: false,
218                    is_closing: true,
219                },
220                pos,
221            ));
222        }
223    }
224
225    // Data length
226    let length = if lvt < 5 {
227        lvt as u32
228    } else {
229        // Extended length
230        if pos >= data.len() {
231            return Err(Error::decoding(pos, "truncated extended length"));
232        }
233        let ext = data[pos];
234        pos += 1;
235
236        match ext {
237            0..=253 => ext as u32,
238            254 => {
239                if pos + 2 > data.len() {
240                    return Err(Error::decoding(pos, "truncated 2-byte extended length"));
241                }
242                let len = u16::from_be_bytes([data[pos], data[pos + 1]]) as u32;
243                pos += 2;
244                len
245            }
246            255 => {
247                if pos + 4 > data.len() {
248                    return Err(Error::decoding(pos, "truncated 4-byte extended length"));
249                }
250                let len =
251                    u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
252                pos += 4;
253                len
254            }
255        }
256    };
257
258    // Sanity check against malformed packets
259    if length > MAX_TAG_LENGTH {
260        return Err(Error::decoding(
261            offset,
262            format!("tag length ({length}) exceeds sanity limit ({MAX_TAG_LENGTH})"),
263        ));
264    }
265
266    Ok((
267        Tag {
268            number: tag_number,
269            class,
270            length,
271            is_opening: false,
272            is_closing: false,
273        },
274        pos,
275    ))
276}
277
278/// Extract raw bytes enclosed by a context opening/closing tag pair.
279///
280/// Reads from `offset` (immediately after the opening tag) through the
281/// matching closing tag, handling nested opening/closing tags.
282///
283/// Returns the enclosed bytes and the offset past the closing tag.
284pub fn extract_context_value(
285    data: &[u8],
286    offset: usize,
287    tag_number: u8,
288) -> Result<(&[u8], usize), Error> {
289    let value_start = offset;
290    let mut pos = offset;
291    let mut depth: usize = 1;
292
293    while depth > 0 && pos < data.len() {
294        let (tag, new_pos) = decode_tag(data, pos)?;
295
296        if tag.is_opening {
297            depth += 1;
298            if depth > MAX_CONTEXT_NESTING_DEPTH {
299                return Err(Error::decoding(
300                    pos,
301                    format!(
302                        "context tag nesting depth exceeds maximum ({MAX_CONTEXT_NESTING_DEPTH})"
303                    ),
304                ));
305            }
306            pos = new_pos;
307        } else if tag.is_closing {
308            depth -= 1;
309            if depth == 0 {
310                if tag.number != tag_number {
311                    return Err(Error::decoding(
312                        pos,
313                        format!(
314                            "closing tag {} does not match opening tag {tag_number}",
315                            tag.number
316                        ),
317                    ));
318                }
319                let value_end = pos;
320                return Ok((&data[value_start..value_end], new_pos));
321            }
322            pos = new_pos;
323        } else {
324            // Skip past tag content
325            if tag.class == TagClass::Application && tag.number == app_tag::BOOLEAN {
326                // Application boolean: value is in LVT, no content octets
327                pos = new_pos;
328            } else {
329                let content_end = new_pos
330                    .checked_add(tag.length as usize)
331                    .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
332                if content_end > data.len() {
333                    return Err(Error::decoding(
334                        new_pos,
335                        format!(
336                            "tag data overflows buffer: need {} bytes at offset {new_pos}",
337                            tag.length
338                        ),
339                    ));
340                }
341                pos = content_end;
342            }
343        }
344    }
345
346    Err(Error::decoding(
347        offset,
348        format!("missing closing tag {tag_number}"),
349    ))
350}
351
352/// Try to decode an optional context-tagged primitive value.
353///
354/// Peeks at the next tag; if it matches the expected context tag number,
355/// returns the content slice and advances the offset. Otherwise returns
356/// `(None, offset)` unchanged.
357pub fn decode_optional_context(
358    data: &[u8],
359    offset: usize,
360    tag_number: u8,
361) -> Result<(Option<&[u8]>, usize), Error> {
362    if offset >= data.len() {
363        return Ok((None, offset));
364    }
365
366    let (tag, new_pos) = decode_tag(data, offset)?;
367    if tag.is_context(tag_number) {
368        let end = new_pos
369            .checked_add(tag.length as usize)
370            .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
371        if end > data.len() {
372            return Err(Error::buffer_too_short(end, data.len()));
373        }
374        Ok((Some(&data[new_pos..end]), end))
375    } else {
376        Ok((None, offset))
377    }
378}
379
380// ---------------------------------------------------------------------------
381// Tests
382// ---------------------------------------------------------------------------
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    fn encode_to_vec(tag_number: u8, class: TagClass, length: u32) -> Vec<u8> {
389        let mut buf = BytesMut::with_capacity(8);
390        encode_tag(&mut buf, tag_number, class, length);
391        buf.to_vec()
392    }
393
394    #[test]
395    fn encode_single_byte_application_tag() {
396        // Tag 0 (Null), Application, length 0
397        assert_eq!(encode_to_vec(0, TagClass::Application, 0), vec![0x00]);
398        // Tag 2 (Unsigned), Application, length 1
399        assert_eq!(encode_to_vec(2, TagClass::Application, 1), vec![0x21]);
400        // Tag 4 (Real), Application, length 4
401        assert_eq!(encode_to_vec(4, TagClass::Application, 4), vec![0x44]);
402        // Tag 12 (ObjectIdentifier), Application, length 4
403        assert_eq!(encode_to_vec(12, TagClass::Application, 4), vec![0xC4]);
404    }
405
406    #[test]
407    fn encode_single_byte_context_tag() {
408        // Context tag 0, length 1
409        assert_eq!(encode_to_vec(0, TagClass::Context, 1), vec![0x09]);
410        // Context tag 1, length 4
411        assert_eq!(encode_to_vec(1, TagClass::Context, 4), vec![0x1C]);
412        // Context tag 2, length 0: (2<<4) | (1<<3) | 0 = 0x28
413        assert_eq!(encode_to_vec(2, TagClass::Context, 0), vec![0x28]);
414    }
415
416    #[test]
417    fn encode_extended_tag_number() {
418        // Tag 15 (extended), Application, length 1
419        let encoded = encode_to_vec(15, TagClass::Application, 1);
420        assert_eq!(encoded, vec![0xF1, 15]);
421
422        // Tag 20 (extended), Context, length 2
423        let encoded = encode_to_vec(20, TagClass::Context, 2);
424        assert_eq!(encoded, vec![0xFA, 20]);
425    }
426
427    #[test]
428    fn encode_extended_length() {
429        // Tag 2, Application, length 100
430        let encoded = encode_to_vec(2, TagClass::Application, 100);
431        assert_eq!(encoded, vec![0x25, 100]);
432
433        // Tag 2, Application, length 1000 (2-byte extended)
434        let encoded = encode_to_vec(2, TagClass::Application, 1000);
435        assert_eq!(encoded, vec![0x25, 254, 0x03, 0xE8]);
436
437        // Tag 2, Application, length 100000 (4-byte extended)
438        let encoded = encode_to_vec(2, TagClass::Application, 100000);
439        assert_eq!(encoded, vec![0x25, 255, 0x00, 0x01, 0x86, 0xA0]);
440    }
441
442    #[test]
443    fn encode_extended_tag_and_length() {
444        // Tag 20, Context, length 100
445        let encoded = encode_to_vec(20, TagClass::Context, 100);
446        assert_eq!(encoded, vec![0xFD, 20, 100]);
447    }
448
449    #[test]
450    fn encode_opening_closing_tags() {
451        let mut buf = BytesMut::new();
452
453        // Opening tag 0: 0x0E
454        encode_opening_tag(&mut buf, 0);
455        assert_eq!(&buf[..], &[0x0E]);
456
457        buf.clear();
458        // Closing tag 0: 0x0F
459        encode_closing_tag(&mut buf, 0);
460        assert_eq!(&buf[..], &[0x0F]);
461
462        buf.clear();
463        // Opening tag 3: 0x3E
464        encode_opening_tag(&mut buf, 3);
465        assert_eq!(&buf[..], &[0x3E]);
466
467        buf.clear();
468        // Extended opening tag 20: 0xFE, 20
469        encode_opening_tag(&mut buf, 20);
470        assert_eq!(&buf[..], &[0xFE, 20]);
471
472        buf.clear();
473        // Extended closing tag 20: 0xFF, 20
474        encode_closing_tag(&mut buf, 20);
475        assert_eq!(&buf[..], &[0xFF, 20]);
476    }
477
478    #[test]
479    fn decode_single_byte_tag() {
480        // Application tag 2, length 1: 0x21
481        let (tag, pos) = decode_tag(&[0x21], 0).unwrap();
482        assert_eq!(tag.number, 2);
483        assert_eq!(tag.class, TagClass::Application);
484        assert_eq!(tag.length, 1);
485        assert!(!tag.is_opening);
486        assert!(!tag.is_closing);
487        assert_eq!(pos, 1);
488    }
489
490    #[test]
491    fn decode_context_tag() {
492        // Context tag 1, length 4: 0x1C
493        let (tag, pos) = decode_tag(&[0x1C], 0).unwrap();
494        assert_eq!(tag.number, 1);
495        assert_eq!(tag.class, TagClass::Context);
496        assert_eq!(tag.length, 4);
497        assert_eq!(pos, 1);
498    }
499
500    #[test]
501    fn decode_opening_closing_tags() {
502        let (tag, _) = decode_tag(&[0x0E], 0).unwrap();
503        assert!(tag.is_opening);
504        assert_eq!(tag.number, 0);
505
506        let (tag, _) = decode_tag(&[0x0F], 0).unwrap();
507        assert!(tag.is_closing);
508        assert_eq!(tag.number, 0);
509
510        let (tag, _) = decode_tag(&[0x3E], 0).unwrap();
511        assert!(tag.is_opening);
512        assert_eq!(tag.number, 3);
513    }
514
515    #[test]
516    fn decode_extended_tag_number() {
517        // Extended tag 20, Application, length 1: 0xF1, 20
518        let (tag, pos) = decode_tag(&[0xF1, 20], 0).unwrap();
519        assert_eq!(tag.number, 20);
520        assert_eq!(tag.class, TagClass::Application);
521        assert_eq!(tag.length, 1);
522        assert_eq!(pos, 2);
523    }
524
525    #[test]
526    fn decode_extended_length() {
527        // Tag 2, Application, length 100: 0x25, 100
528        let (tag, pos) = decode_tag(&[0x25, 100], 0).unwrap();
529        assert_eq!(tag.number, 2);
530        assert_eq!(tag.length, 100);
531        assert_eq!(pos, 2);
532
533        // Tag 2, Application, length 1000: 0x25, 254, 0x03, 0xE8
534        let (tag, pos) = decode_tag(&[0x25, 254, 0x03, 0xE8], 0).unwrap();
535        assert_eq!(tag.number, 2);
536        assert_eq!(tag.length, 1000);
537        assert_eq!(pos, 4);
538
539        // Tag 2, Application, length 100000: 0x25, 255, 0x00, 0x01, 0x86, 0xA0
540        let (tag, pos) = decode_tag(&[0x25, 255, 0x00, 0x01, 0x86, 0xA0], 0).unwrap();
541        assert_eq!(tag.number, 2);
542        assert_eq!(tag.length, 100000);
543        assert_eq!(pos, 6);
544    }
545
546    #[test]
547    fn decode_tag_round_trip() {
548        // Test that encode -> decode round-trips for various combinations
549        let cases = [
550            (0u8, TagClass::Application, 0u32),
551            (1, TagClass::Application, 1),
552            (4, TagClass::Application, 4),
553            (12, TagClass::Application, 4),
554            (0, TagClass::Context, 1),
555            (3, TagClass::Context, 4),
556            (15, TagClass::Application, 1),
557            (20, TagClass::Context, 2),
558            (2, TagClass::Application, 100),
559            (2, TagClass::Application, 1000),
560            (20, TagClass::Context, 100),
561        ];
562
563        for (tag_num, class, length) in cases {
564            let encoded = encode_to_vec(tag_num, class, length);
565            let (decoded, _) = decode_tag(&encoded, 0).unwrap();
566            assert_eq!(
567                decoded.number, tag_num,
568                "tag number mismatch for ({tag_num}, {class:?}, {length})"
569            );
570            assert_eq!(
571                decoded.class, class,
572                "class mismatch for ({tag_num}, {class:?}, {length})"
573            );
574            assert_eq!(
575                decoded.length, length,
576                "length mismatch for ({tag_num}, {class:?}, {length})"
577            );
578        }
579    }
580
581    #[test]
582    fn decode_tag_empty_buffer() {
583        assert!(decode_tag(&[], 0).is_err());
584    }
585
586    #[test]
587    fn decode_tag_truncated_extended() {
588        // Extended tag number but no second byte
589        assert!(decode_tag(&[0xF1], 0).is_err());
590    }
591
592    #[test]
593    fn decode_tag_excessive_length() {
594        // Craft a tag with length > 1MB
595        let data = [0x25, 255, 0x10, 0x00, 0x00, 0x00]; // length = 0x10000000
596        assert!(decode_tag(&data, 0).is_err());
597    }
598
599    #[test]
600    fn boolean_tag_detection() {
601        // Application boolean true: tag 1, LVT = 1
602        let (tag, _) = decode_tag(&[0x11], 0).unwrap();
603        assert!(tag.is_boolean_true());
604
605        // Application boolean false: tag 1, LVT = 0
606        let (tag, _) = decode_tag(&[0x10], 0).unwrap();
607        assert!(!tag.is_boolean_true());
608
609        // Not a boolean (tag 2): should not be detected as boolean
610        let (tag, _) = decode_tag(&[0x21], 0).unwrap();
611        assert!(!tag.is_boolean_true());
612    }
613
614    #[test]
615    fn extract_context_value_simple() {
616        // Opening tag 0, some data (tag 2, len 1, value 42), closing tag 0
617        let data = [0x0E, 0x21, 42, 0x0F];
618        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
619        assert_eq!(value, &[0x21, 42]);
620        assert_eq!(pos, 4);
621    }
622
623    #[test]
624    fn extract_context_value_nested() {
625        // Opening tag 0, opening tag 1, data, closing tag 1, closing tag 0
626        let data = [0x0E, 0x1E, 0x21, 42, 0x1F, 0x0F];
627        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
628        assert_eq!(value, &[0x1E, 0x21, 42, 0x1F]);
629        assert_eq!(pos, 6);
630    }
631
632    #[test]
633    fn extract_context_value_missing_close() {
634        let data = [0x0E, 0x21, 42]; // No closing tag
635        assert!(extract_context_value(&data, 1, 0).is_err());
636    }
637
638    #[test]
639    fn tag_is_context_helper() {
640        let (tag, _) = decode_tag(&[0x09], 0).unwrap(); // context 0, len 1
641        assert!(tag.is_context(0));
642        assert!(!tag.is_context(1));
643    }
644
645    #[test]
646    fn decode_optional_context_present() {
647        // Context tag 0, length 1, value byte 42
648        let data = [0x09, 42];
649        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
650        assert_eq!(value, Some(&[42u8][..]));
651        assert_eq!(pos, 2);
652    }
653
654    #[test]
655    fn decode_optional_context_absent() {
656        // Context tag 1, but we're looking for tag 0
657        let data = [0x19, 42];
658        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
659        assert!(value.is_none());
660        assert_eq!(pos, 0); // offset unchanged
661    }
662
663    #[test]
664    fn decode_optional_context_empty_buffer() {
665        let (value, pos) = decode_optional_context(&[], 0, 0).unwrap();
666        assert!(value.is_none());
667        assert_eq!(pos, 0);
668    }
669
670    // --- Edge case tests ---
671
672    #[test]
673    fn extract_context_value_mismatched_closing_tag() {
674        // Opening tag 0, data, closing tag 1 (mismatch!)
675        let data = [0x0E, 0x21, 42, 0x1F]; // open 0, data, close 1
676        assert!(extract_context_value(&data, 1, 0).is_err());
677    }
678
679    #[test]
680    fn extract_context_value_deeply_nested() {
681        // Verify correct extraction with multiple nesting levels
682        // open 0, open 1, open 2, data, close 2, close 1, close 0
683        let data = [
684            0x0E, // opening 0
685            0x1E, // opening 1
686            0x2E, // opening 2
687            0x21, 42,   // app tag 2 len 1, value 42
688            0x2F, // closing 2
689            0x1F, // closing 1
690            0x0F, // closing 0
691        ];
692        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
693        assert_eq!(value, &data[1..7]); // everything between open 0 and close 0
694        assert_eq!(pos, 8);
695    }
696
697    #[test]
698    fn extract_context_value_nesting_depth_exceeded() {
699        // Build a deeply nested structure that exceeds MAX_CONTEXT_NESTING_DEPTH
700        let mut data = Vec::new();
701        // We start at depth 1 (already inside opening tag), so we need
702        // MAX_CONTEXT_NESTING_DEPTH more opening tags to exceed the limit
703        for i in 0..MAX_CONTEXT_NESTING_DEPTH {
704            let tag = (i % 15) as u8; // cycle through tag numbers 0-14
705            data.push((tag << 4) | 0x0E); // opening tag
706        }
707        // The function starts at depth 1, so this should trigger the limit
708        let result = extract_context_value(&data, 0, 0);
709        assert!(result.is_err());
710    }
711
712    #[test]
713    fn decode_tag_at_nonzero_offset() {
714        // Verify decode works correctly when starting at a non-zero offset
715        let data = [0xFF, 0xFF, 0x21, 0x00]; // garbage, then tag 2 len 1
716        let (tag, pos) = decode_tag(&data, 2).unwrap();
717        assert_eq!(tag.number, 2);
718        assert_eq!(tag.class, TagClass::Application);
719        assert_eq!(tag.length, 1);
720        assert_eq!(pos, 3);
721    }
722
723    #[test]
724    fn decode_tag_offset_beyond_buffer() {
725        let data = [0x21];
726        assert!(decode_tag(&data, 5).is_err());
727    }
728
729    #[test]
730    fn decode_tag_truncated_2byte_extended_length() {
731        // Extended length indicator 254 but only 1 byte follows
732        let data = [0x25, 254, 0x03]; // need 2 bytes after 254
733        assert!(decode_tag(&data, 0).is_err());
734    }
735
736    #[test]
737    fn decode_tag_truncated_4byte_extended_length() {
738        // Extended length indicator 255 but only 2 bytes follow
739        let data = [0x25, 255, 0x00, 0x01];
740        assert!(decode_tag(&data, 0).is_err());
741    }
742
743    #[test]
744    fn extract_context_value_with_boolean_inside() {
745        // Boolean tags have no content octets, which is a special case
746        // Opening tag 0, boolean true (app tag 1, lvt=1), closing tag 0
747        let data = [0x0E, 0x11, 0x0F]; // open 0, bool true, close 0
748        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
749        assert_eq!(value, &[0x11]); // just the boolean tag
750        assert_eq!(pos, 3);
751    }
752
753    #[test]
754    fn extract_context_value_tag_data_overflows_buffer() {
755        // Opening tag 0, a data tag claiming 100 bytes of content but only 2 available
756        let data = [0x0E, 0x25, 100, 0x01, 0x02, 0x0F];
757        let result = extract_context_value(&data, 1, 0);
758        assert!(result.is_err());
759    }
760
761    #[test]
762    fn decode_optional_context_content_overflows() {
763        // Context tag 0, length 4, but only 2 content bytes available
764        let data = [0x0C, 0x01, 0x02]; // ctx 0, len 4, only 2 bytes
765        assert!(decode_optional_context(&data, 0, 0).is_err());
766    }
767
768    #[test]
769    fn opening_closing_tag_extended_round_trip() {
770        let mut buf = BytesMut::new();
771        // Extended tag numbers (>14) for opening/closing
772        for tag_num in [15u8, 20, 100, 254] {
773            buf.clear();
774            encode_opening_tag(&mut buf, tag_num);
775            let (tag, pos1) = decode_tag(&buf, 0).unwrap();
776            assert!(tag.is_opening);
777            assert_eq!(tag.number, tag_num);
778            assert_eq!(pos1, 2); // extended = 2 bytes
779
780            buf.clear();
781            encode_closing_tag(&mut buf, tag_num);
782            let (tag, pos2) = decode_tag(&buf, 0).unwrap();
783            assert!(tag.is_closing);
784            assert_eq!(tag.number, tag_num);
785            assert_eq!(pos2, 2);
786        }
787    }
788}