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.
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    /// Application-tagged booleans encode the value in the tag's L/V/T
59    /// 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        buf.put_u8((tag_number << 4) | cls_bit | (length as u8));
96        return;
97    }
98
99    let tag_nibble = if tag_number <= 14 {
100        tag_number << 4
101    } else {
102        0xF0
103    };
104
105    if length <= 4 {
106        buf.put_u8(tag_nibble | cls_bit | (length as u8));
107        if tag_number > 14 {
108            buf.put_u8(tag_number);
109        }
110        return;
111    }
112
113    buf.put_u8(tag_nibble | cls_bit | 5);
114    if tag_number > 14 {
115        buf.put_u8(tag_number);
116    }
117
118    if length <= 253 {
119        buf.put_u8(length as u8);
120    } else if length <= 65535 {
121        buf.put_u8(254);
122        buf.put_u16(length as u16);
123    } else {
124        buf.put_u8(255);
125        buf.put_u32(length);
126    }
127}
128
129/// Encode a context-specific opening tag.
130pub fn encode_opening_tag(buf: &mut BytesMut, tag_number: u8) {
131    if tag_number <= 14 {
132        buf.put_u8((tag_number << 4) | 0x0E);
133    } else {
134        buf.put_u8(0xFE);
135        buf.put_u8(tag_number);
136    }
137}
138
139/// Encode a context-specific closing tag.
140pub fn encode_closing_tag(buf: &mut BytesMut, tag_number: u8) {
141    if tag_number <= 14 {
142        buf.put_u8((tag_number << 4) | 0x0F);
143    } else {
144        buf.put_u8(0xFF);
145        buf.put_u8(tag_number);
146    }
147}
148
149// ---------------------------------------------------------------------------
150// Decoding
151// ---------------------------------------------------------------------------
152
153/// Maximum tag length sanity limit (1 MB). BACnet APDUs are at most ~64KB
154/// segmented; 1MB is generous. Prevents memory exhaustion from malformed packets.
155const MAX_TAG_LENGTH: u32 = 1_048_576;
156
157/// Maximum nesting depth for context tags to prevent stack overflow from crafted packets.
158pub const MAX_CONTEXT_NESTING_DEPTH: usize = 32;
159
160/// Decode a tag from `data` starting at `offset`.
161///
162/// Returns the decoded [`Tag`] and the new offset past the tag header.
163pub fn decode_tag(data: &[u8], offset: usize) -> Result<(Tag, usize), Error> {
164    if offset >= data.len() {
165        return Err(Error::decoding(
166            offset,
167            "tag decode: offset beyond buffer length",
168        ));
169    }
170
171    let initial = data[offset];
172    let mut pos = offset + 1;
173
174    let mut tag_number = (initial >> 4) & 0x0F;
175    let class = if (initial >> 3) & 0x01 == 1 {
176        TagClass::Context
177    } else {
178        TagClass::Application
179    };
180    let lvt = initial & 0x07;
181    if class == TagClass::Application && lvt > 5 {
182        return Err(Error::decoding(offset, "reserved application tag L/V/T"));
183    }
184
185    if tag_number == 0x0F {
186        if pos >= data.len() {
187            return Err(Error::decoding(pos, "truncated extended tag number"));
188        }
189        tag_number = data[pos];
190        if !(15..=254).contains(&tag_number) {
191            return Err(Error::decoding(
192                pos,
193                format!("invalid extended tag number {tag_number}"),
194            ));
195        }
196        pos += 1;
197    }
198
199    if class == TagClass::Context {
200        if lvt == 6 {
201            return Ok((
202                Tag {
203                    number: tag_number,
204                    class,
205                    length: 0,
206                    is_opening: true,
207                    is_closing: false,
208                },
209                pos,
210            ));
211        }
212        if lvt == 7 {
213            return Ok((
214                Tag {
215                    number: tag_number,
216                    class,
217                    length: 0,
218                    is_opening: false,
219                    is_closing: true,
220                },
221                pos,
222            ));
223        }
224    }
225
226    let length = if lvt < 5 {
227        lvt as u32
228    } else {
229        if pos >= data.len() {
230            return Err(Error::decoding(pos, "truncated extended length"));
231        }
232        let ext = data[pos];
233        pos += 1;
234
235        match ext {
236            0..=4 => {
237                return Err(Error::decoding(
238                    pos - 1,
239                    format!("non-canonical extended tag length {ext}"),
240                ));
241            }
242            5..=253 => ext as u32,
243            254 => {
244                if pos + 2 > data.len() {
245                    return Err(Error::decoding(pos, "truncated 2-byte extended length"));
246                }
247                let len = u16::from_be_bytes([data[pos], data[pos + 1]]) as u32;
248                if len < 254 {
249                    return Err(Error::decoding(
250                        pos,
251                        format!("non-canonical 2-byte extended tag length {len}"),
252                    ));
253                }
254                pos += 2;
255                len
256            }
257            255 => {
258                if pos + 4 > data.len() {
259                    return Err(Error::decoding(pos, "truncated 4-byte extended length"));
260                }
261                let len =
262                    u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
263                if len < 65_536 {
264                    return Err(Error::decoding(
265                        pos,
266                        format!("non-canonical 4-byte extended tag length {len}"),
267                    ));
268                }
269                pos += 4;
270                len
271            }
272        }
273    };
274
275    if length > MAX_TAG_LENGTH {
276        return Err(Error::decoding(
277            offset,
278            format!("tag length ({length}) exceeds sanity limit ({MAX_TAG_LENGTH})"),
279        ));
280    }
281
282    Ok((
283        Tag {
284            number: tag_number,
285            class,
286            length,
287            is_opening: false,
288            is_closing: false,
289        },
290        pos,
291    ))
292}
293
294/// Extract raw bytes enclosed by a context opening/closing tag pair.
295///
296/// Reads from `offset` (immediately after the opening tag) through the
297/// matching closing tag, handling nested opening/closing tags.
298///
299/// Returns the enclosed bytes and the offset past the closing tag.
300pub fn extract_context_value(
301    data: &[u8],
302    offset: usize,
303    tag_number: u8,
304) -> Result<(&[u8], usize), Error> {
305    let value_start = offset;
306    let mut pos = offset;
307    let mut depth: usize = 1;
308    let mut open_tags = [0u8; MAX_CONTEXT_NESTING_DEPTH];
309    open_tags[0] = tag_number;
310
311    while depth > 0 && pos < data.len() {
312        let (tag, new_pos) = decode_tag(data, pos)?;
313
314        if tag.is_opening {
315            if depth == MAX_CONTEXT_NESTING_DEPTH {
316                return Err(Error::decoding(
317                    pos,
318                    format!(
319                        "context tag nesting depth exceeds maximum ({MAX_CONTEXT_NESTING_DEPTH})"
320                    ),
321                ));
322            }
323            open_tags[depth] = tag.number;
324            depth += 1;
325            pos = new_pos;
326        } else if tag.is_closing {
327            let expected = open_tags[depth - 1];
328            if tag.number != expected {
329                return Err(Error::decoding(
330                    pos,
331                    format!(
332                        "closing tag {} does not match opening tag {expected}",
333                        tag.number
334                    ),
335                ));
336            }
337            depth -= 1;
338            if depth == 0 {
339                let value_end = pos;
340                return Ok((&data[value_start..value_end], new_pos));
341            }
342            pos = new_pos;
343        } else if tag.class == TagClass::Application && tag.number == app_tag::BOOLEAN {
344            pos = new_pos;
345        } else {
346            let content_end = new_pos
347                .checked_add(tag.length as usize)
348                .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
349            if content_end > data.len() {
350                return Err(Error::decoding(
351                    new_pos,
352                    format!(
353                        "tag data overflows buffer: need {} bytes at offset {new_pos}",
354                        tag.length
355                    ),
356                ));
357            }
358            pos = content_end;
359        }
360    }
361
362    Err(Error::decoding(
363        offset,
364        format!("missing closing tag {tag_number}"),
365    ))
366}
367
368/// Extract raw bytes enclosed by a context opening/closing tag pair WITHOUT
369/// parsing the enclosed content as BACnet tags.
370///
371/// [`extract_context_value`] walks the content as well-formed TLVs, which is
372/// the correct way to skip an unknown *conformant* constructed value (Clause
373/// 20.2.1.6 rule (d)). This raw scanner is only for compatibility payloads
374/// whose contract explicitly permits non-TLV bytes. It is not safe for a
375/// `SEQUENCE OF CHOICE`: a primitive value can contain a closing-tag octet.
376/// The scan balances raw opening/closing octets for `tag_number`.
377///
378/// `offset` points just past the opening tag for `tag_number`. Returns the
379/// enclosed bytes and the offset past the closing tag octet(s).
380///
381/// Known limitation: a payload that itself contains the target tag's opening
382/// or closing octet pattern (e.g. as unrelated content bytes) unbalances the
383/// walk; such payloads cannot round-trip through this path.
384pub fn extract_raw_context<'a>(
385    data: &'a [u8],
386    offset: usize,
387    tag_number: u8,
388) -> Result<(&'a [u8], usize), Error> {
389    let wide = tag_number > 14;
390    let open_first = if wide { 0xFE } else { (tag_number << 4) | 0x0E };
391    let close_first = if wide { 0xFF } else { (tag_number << 4) | 0x0F };
392    let width = if wide { 2 } else { 1 };
393
394    let value_start = offset;
395    let mut pos = offset;
396    let mut depth: usize = 1;
397
398    while pos < data.len() {
399        let b = data[pos];
400        if b == open_first && (!wide || data.get(pos + 1) == Some(&tag_number)) {
401            depth += 1;
402            pos += width;
403            continue;
404        }
405        if b == close_first && (!wide || data.get(pos + 1) == Some(&tag_number)) {
406            depth -= 1;
407            if depth == 0 {
408                return Ok((&data[value_start..pos], pos + width));
409            }
410            pos += width;
411            continue;
412        }
413        pos += 1;
414    }
415
416    Err(Error::decoding(
417        value_start,
418        format!("missing closing tag {tag_number}"),
419    ))
420}
421
422/// Try to decode an optional context-tagged primitive value.
423///
424/// Peeks at the next tag; if it matches the expected context tag number,
425/// returns the content slice and advances the offset. Otherwise returns
426/// `(None, offset)` unchanged.
427pub fn decode_optional_context(
428    data: &[u8],
429    offset: usize,
430    tag_number: u8,
431) -> Result<(Option<&[u8]>, usize), Error> {
432    if offset >= data.len() {
433        return Ok((None, offset));
434    }
435
436    let (tag, new_pos) = decode_tag(data, offset)?;
437    if tag.is_context(tag_number) {
438        let end = new_pos
439            .checked_add(tag.length as usize)
440            .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
441        if end > data.len() {
442            return Err(Error::buffer_too_short(end, data.len()));
443        }
444        Ok((Some(&data[new_pos..end]), end))
445    } else {
446        Ok((None, offset))
447    }
448}
449
450// ---------------------------------------------------------------------------
451// Tests
452// ---------------------------------------------------------------------------
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn encode_to_vec(tag_number: u8, class: TagClass, length: u32) -> Vec<u8> {
459        let mut buf = BytesMut::with_capacity(8);
460        encode_tag(&mut buf, tag_number, class, length);
461        buf.to_vec()
462    }
463
464    #[test]
465    fn encode_single_byte_application_tag() {
466        // Tag 0 (Null), Application, length 0
467        assert_eq!(encode_to_vec(0, TagClass::Application, 0), vec![0x00]);
468        // Tag 2 (Unsigned), Application, length 1
469        assert_eq!(encode_to_vec(2, TagClass::Application, 1), vec![0x21]);
470        // Tag 4 (Real), Application, length 4
471        assert_eq!(encode_to_vec(4, TagClass::Application, 4), vec![0x44]);
472        // Tag 12 (ObjectIdentifier), Application, length 4
473        assert_eq!(encode_to_vec(12, TagClass::Application, 4), vec![0xC4]);
474    }
475
476    #[test]
477    fn encode_single_byte_context_tag() {
478        // Context tag 0, length 1
479        assert_eq!(encode_to_vec(0, TagClass::Context, 1), vec![0x09]);
480        // Context tag 1, length 4
481        assert_eq!(encode_to_vec(1, TagClass::Context, 4), vec![0x1C]);
482        // Context tag 2, length 0: (2<<4) | (1<<3) | 0 = 0x28
483        assert_eq!(encode_to_vec(2, TagClass::Context, 0), vec![0x28]);
484    }
485
486    #[test]
487    fn encode_extended_tag_number() {
488        // Tag 15 (extended), Application, length 1
489        let encoded = encode_to_vec(15, TagClass::Application, 1);
490        assert_eq!(encoded, vec![0xF1, 15]);
491
492        // Tag 20 (extended), Context, length 2
493        let encoded = encode_to_vec(20, TagClass::Context, 2);
494        assert_eq!(encoded, vec![0xFA, 20]);
495    }
496
497    #[test]
498    fn encode_extended_length() {
499        // Tag 2, Application, length 100
500        let encoded = encode_to_vec(2, TagClass::Application, 100);
501        assert_eq!(encoded, vec![0x25, 100]);
502
503        // Tag 2, Application, length 1000 (2-byte extended)
504        let encoded = encode_to_vec(2, TagClass::Application, 1000);
505        assert_eq!(encoded, vec![0x25, 254, 0x03, 0xE8]);
506
507        // Tag 2, Application, length 100000 (4-byte extended)
508        let encoded = encode_to_vec(2, TagClass::Application, 100000);
509        assert_eq!(encoded, vec![0x25, 255, 0x00, 0x01, 0x86, 0xA0]);
510    }
511
512    #[test]
513    fn encode_extended_tag_and_length() {
514        // Tag 20, Context, length 100
515        let encoded = encode_to_vec(20, TagClass::Context, 100);
516        assert_eq!(encoded, vec![0xFD, 20, 100]);
517    }
518
519    #[test]
520    fn encode_opening_closing_tags() {
521        let mut buf = BytesMut::new();
522
523        // Opening tag 0: 0x0E
524        encode_opening_tag(&mut buf, 0);
525        assert_eq!(&buf[..], &[0x0E]);
526
527        buf.clear();
528        // Closing tag 0: 0x0F
529        encode_closing_tag(&mut buf, 0);
530        assert_eq!(&buf[..], &[0x0F]);
531
532        buf.clear();
533        // Opening tag 3: 0x3E
534        encode_opening_tag(&mut buf, 3);
535        assert_eq!(&buf[..], &[0x3E]);
536
537        buf.clear();
538        // Extended opening tag 20: 0xFE, 20
539        encode_opening_tag(&mut buf, 20);
540        assert_eq!(&buf[..], &[0xFE, 20]);
541
542        buf.clear();
543        // Extended closing tag 20: 0xFF, 20
544        encode_closing_tag(&mut buf, 20);
545        assert_eq!(&buf[..], &[0xFF, 20]);
546    }
547
548    #[test]
549    fn decode_single_byte_tag() {
550        // Application tag 2, length 1: 0x21
551        let (tag, pos) = decode_tag(&[0x21], 0).unwrap();
552        assert_eq!(tag.number, 2);
553        assert_eq!(tag.class, TagClass::Application);
554        assert_eq!(tag.length, 1);
555        assert!(!tag.is_opening);
556        assert!(!tag.is_closing);
557        assert_eq!(pos, 1);
558    }
559
560    #[test]
561    fn decode_context_tag() {
562        // Context tag 1, length 4: 0x1C
563        let (tag, pos) = decode_tag(&[0x1C], 0).unwrap();
564        assert_eq!(tag.number, 1);
565        assert_eq!(tag.class, TagClass::Context);
566        assert_eq!(tag.length, 4);
567        assert_eq!(pos, 1);
568    }
569
570    #[test]
571    fn decode_opening_closing_tags() {
572        let (tag, _) = decode_tag(&[0x0E], 0).unwrap();
573        assert!(tag.is_opening);
574        assert_eq!(tag.number, 0);
575
576        let (tag, _) = decode_tag(&[0x0F], 0).unwrap();
577        assert!(tag.is_closing);
578        assert_eq!(tag.number, 0);
579
580        let (tag, _) = decode_tag(&[0x3E], 0).unwrap();
581        assert!(tag.is_opening);
582        assert_eq!(tag.number, 3);
583    }
584
585    #[test]
586    fn decode_extended_tag_number() {
587        // Extended tag 20, Application, length 1: 0xF1, 20
588        let (tag, pos) = decode_tag(&[0xF1, 20], 0).unwrap();
589        assert_eq!(tag.number, 20);
590        assert_eq!(tag.class, TagClass::Application);
591        assert_eq!(tag.length, 1);
592        assert_eq!(pos, 2);
593    }
594
595    #[test]
596    fn decode_extended_length() {
597        // Tag 2, Application, length 100: 0x25, 100
598        let (tag, pos) = decode_tag(&[0x25, 100], 0).unwrap();
599        assert_eq!(tag.number, 2);
600        assert_eq!(tag.length, 100);
601        assert_eq!(pos, 2);
602
603        // Tag 2, Application, length 1000: 0x25, 254, 0x03, 0xE8
604        let (tag, pos) = decode_tag(&[0x25, 254, 0x03, 0xE8], 0).unwrap();
605        assert_eq!(tag.number, 2);
606        assert_eq!(tag.length, 1000);
607        assert_eq!(pos, 4);
608
609        // Tag 2, Application, length 100000: 0x25, 255, 0x00, 0x01, 0x86, 0xA0
610        let (tag, pos) = decode_tag(&[0x25, 255, 0x00, 0x01, 0x86, 0xA0], 0).unwrap();
611        assert_eq!(tag.number, 2);
612        assert_eq!(tag.length, 100000);
613        assert_eq!(pos, 6);
614    }
615
616    #[test]
617    fn decode_tag_round_trip() {
618        // Test that encode -> decode round-trips for various combinations
619        let cases = [
620            (0u8, TagClass::Application, 0u32),
621            (1, TagClass::Application, 1),
622            (4, TagClass::Application, 4),
623            (12, TagClass::Application, 4),
624            (0, TagClass::Context, 1),
625            (3, TagClass::Context, 4),
626            (15, TagClass::Application, 1),
627            (20, TagClass::Context, 2),
628            (2, TagClass::Application, 100),
629            (2, TagClass::Application, 1000),
630            (20, TagClass::Context, 100),
631        ];
632
633        for (tag_num, class, length) in cases {
634            let encoded = encode_to_vec(tag_num, class, length);
635            let (decoded, _) = decode_tag(&encoded, 0).unwrap();
636            assert_eq!(
637                decoded.number, tag_num,
638                "tag number mismatch for ({tag_num}, {class:?}, {length})"
639            );
640            assert_eq!(
641                decoded.class, class,
642                "class mismatch for ({tag_num}, {class:?}, {length})"
643            );
644            assert_eq!(
645                decoded.length, length,
646                "length mismatch for ({tag_num}, {class:?}, {length})"
647            );
648        }
649    }
650
651    #[test]
652    fn decode_tag_empty_buffer() {
653        assert!(decode_tag(&[], 0).is_err());
654    }
655
656    #[test]
657    fn decode_tag_truncated_extended() {
658        // Extended tag number but no second byte
659        assert!(decode_tag(&[0xF1], 0).is_err());
660    }
661
662    #[test]
663    fn decode_tag_excessive_length() {
664        // Craft a tag with length > 1MB
665        let data = [0x25, 255, 0x10, 0x00, 0x00, 0x00]; // length = 0x10000000
666        assert!(decode_tag(&data, 0).is_err());
667    }
668
669    #[test]
670    fn boolean_tag_detection() {
671        // Application boolean true: tag 1, LVT = 1
672        let (tag, _) = decode_tag(&[0x11], 0).unwrap();
673        assert!(tag.is_boolean_true());
674
675        // Application boolean false: tag 1, LVT = 0
676        let (tag, _) = decode_tag(&[0x10], 0).unwrap();
677        assert!(!tag.is_boolean_true());
678
679        // Not a boolean (tag 2): should not be detected as boolean
680        let (tag, _) = decode_tag(&[0x21], 0).unwrap();
681        assert!(!tag.is_boolean_true());
682    }
683
684    #[test]
685    fn extract_context_value_simple() {
686        // Opening tag 0, some data (tag 2, len 1, value 42), closing tag 0
687        let data = [0x0E, 0x21, 42, 0x0F];
688        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
689        assert_eq!(value, &[0x21, 42]);
690        assert_eq!(pos, 4);
691    }
692
693    // --- extract_raw_context ---
694
695    #[test]
696    fn extract_raw_context_non_tlv_payload() {
697        // Payload bytes that are NOT well-formed TLVs must pass through
698        // verbatim (this is the whole point of the raw scanner).
699        let data = [0x9E, 0xDE, 0xAD, 0xBE, 0xEF, 0x9F];
700        let (value, pos) = extract_raw_context(&data, 1, 9).unwrap();
701        assert_eq!(value, &[0xDE, 0xAD, 0xBE, 0xEF]);
702        assert_eq!(pos, 6);
703    }
704
705    #[test]
706    fn extract_raw_context_nested_same_tag() {
707        // opening 2, opening 2, payload, closing 2, more payload, closing 2
708        let data = [0x2E, 0x2E, 0x01, 0x2F, 0x02, 0x2F];
709        let (value, pos) = extract_raw_context(&data, 1, 2).unwrap();
710        assert_eq!(value, &[0x2E, 0x01, 0x2F, 0x02]);
711        assert_eq!(pos, 6);
712    }
713
714    #[test]
715    fn extract_raw_context_extended_tag() {
716        // Extended tag 200: 0xFE 0xC8 ... 0xFF 0xC8.
717        let data = [0xFE, 200, 0x01, 0x02, 0xFF, 200];
718        let (value, pos) = extract_raw_context(&data, 2, 200).unwrap();
719        assert_eq!(value, &[0x01, 0x02]);
720        assert_eq!(pos, 6);
721    }
722
723    #[test]
724    fn extract_raw_context_missing_closing() {
725        let data = [0x9E, 0xDE, 0xAD];
726        assert!(extract_raw_context(&data, 1, 9).is_err());
727    }
728
729    #[test]
730    fn extract_raw_context_truncated_extended_closing() {
731        // 0xFF at the very end with no tag-number octet — not a closing.
732        let data = [0xFE, 200, 0x01, 0x02, 0xFF];
733        assert!(extract_raw_context(&data, 2, 200).is_err());
734    }
735
736    #[test]
737    fn extract_context_value_nested() {
738        // Opening tag 0, opening tag 1, data, closing tag 1, closing tag 0
739        let data = [0x0E, 0x1E, 0x21, 42, 0x1F, 0x0F];
740        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
741        assert_eq!(value, &[0x1E, 0x21, 42, 0x1F]);
742        assert_eq!(pos, 6);
743    }
744
745    #[test]
746    fn extract_context_value_missing_close() {
747        let data = [0x0E, 0x21, 42]; // No closing tag
748        assert!(extract_context_value(&data, 1, 0).is_err());
749    }
750
751    #[test]
752    fn tag_is_context_helper() {
753        let (tag, _) = decode_tag(&[0x09], 0).unwrap(); // context 0, len 1
754        assert!(tag.is_context(0));
755        assert!(!tag.is_context(1));
756    }
757
758    #[test]
759    fn decode_optional_context_present() {
760        // Context tag 0, length 1, value byte 42
761        let data = [0x09, 42];
762        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
763        assert_eq!(value, Some(&[42u8][..]));
764        assert_eq!(pos, 2);
765    }
766
767    #[test]
768    fn decode_optional_context_absent() {
769        // Context tag 1, but we're looking for tag 0
770        let data = [0x19, 42];
771        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
772        assert!(value.is_none());
773        assert_eq!(pos, 0); // offset unchanged
774    }
775
776    #[test]
777    fn decode_optional_context_empty_buffer() {
778        let (value, pos) = decode_optional_context(&[], 0, 0).unwrap();
779        assert!(value.is_none());
780        assert_eq!(pos, 0);
781    }
782
783    // --- Edge case tests ---
784
785    #[test]
786    fn extract_context_value_mismatched_closing_tag() {
787        // Opening tag 0, data, closing tag 1 (mismatch!)
788        let data = [0x0E, 0x21, 42, 0x1F]; // open 0, data, close 1
789        assert!(extract_context_value(&data, 1, 0).is_err());
790    }
791
792    #[test]
793    fn extract_context_value_mismatched_nested_closing_tag() {
794        // Opening tag 0, opening tag 1, closing tag 2, closing tag 0.
795        let data = [0x0E, 0x1E, 0x2F, 0x0F];
796        assert!(extract_context_value(&data, 1, 0).is_err());
797    }
798
799    #[test]
800    fn extract_context_value_deeply_nested() {
801        // Verify correct extraction with multiple nesting levels
802        // open 0, open 1, open 2, data, close 2, close 1, close 0
803        let data = [
804            0x0E, // opening 0
805            0x1E, // opening 1
806            0x2E, // opening 2
807            0x21, 42,   // app tag 2 len 1, value 42
808            0x2F, // closing 2
809            0x1F, // closing 1
810            0x0F, // closing 0
811        ];
812        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
813        assert_eq!(value, &data[1..7]); // everything between open 0 and close 0
814        assert_eq!(pos, 8);
815    }
816
817    #[test]
818    fn extract_context_value_nesting_depth_exceeded() {
819        // Build a deeply nested structure that exceeds MAX_CONTEXT_NESTING_DEPTH
820        let mut data = Vec::new();
821        // We start at depth 1 (already inside opening tag), so we need
822        // MAX_CONTEXT_NESTING_DEPTH more opening tags to exceed the limit
823        for i in 0..MAX_CONTEXT_NESTING_DEPTH {
824            let tag = (i % 15) as u8; // cycle through tag numbers 0-14
825            data.push((tag << 4) | 0x0E); // opening tag
826        }
827        // The function starts at depth 1, so this should trigger the limit
828        let result = extract_context_value(&data, 0, 0);
829        assert!(result.is_err());
830    }
831
832    #[test]
833    fn decode_tag_at_nonzero_offset() {
834        // Verify decode works correctly when starting at a non-zero offset
835        let data = [0xFF, 0xFF, 0x21, 0x00]; // garbage, then tag 2 len 1
836        let (tag, pos) = decode_tag(&data, 2).unwrap();
837        assert_eq!(tag.number, 2);
838        assert_eq!(tag.class, TagClass::Application);
839        assert_eq!(tag.length, 1);
840        assert_eq!(pos, 3);
841    }
842
843    #[test]
844    fn decode_tag_offset_beyond_buffer() {
845        let data = [0x21];
846        assert!(decode_tag(&data, 5).is_err());
847    }
848
849    #[test]
850    fn decode_tag_truncated_2byte_extended_length() {
851        // Extended length indicator 254 but only 1 byte follows
852        let data = [0x25, 254, 0x03]; // need 2 bytes after 254
853        assert!(decode_tag(&data, 0).is_err());
854    }
855
856    #[test]
857    fn decode_tag_truncated_4byte_extended_length() {
858        // Extended length indicator 255 but only 2 bytes follow
859        let data = [0x25, 255, 0x00, 0x01];
860        assert!(decode_tag(&data, 0).is_err());
861    }
862
863    #[test]
864    fn extract_context_value_with_boolean_inside() {
865        // Boolean tags have no content octets, which is a special case
866        // Opening tag 0, boolean true (app tag 1, lvt=1), closing tag 0
867        let data = [0x0E, 0x11, 0x0F]; // open 0, bool true, close 0
868        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
869        assert_eq!(value, &[0x11]); // just the boolean tag
870        assert_eq!(pos, 3);
871    }
872
873    #[test]
874    fn extract_context_value_tag_data_overflows_buffer() {
875        // Opening tag 0, a data tag claiming 100 bytes of content but only 2 available
876        let data = [0x0E, 0x25, 100, 0x01, 0x02, 0x0F];
877        let result = extract_context_value(&data, 1, 0);
878        assert!(result.is_err());
879    }
880
881    #[test]
882    fn decode_optional_context_content_overflows() {
883        // Context tag 0, length 4, but only 2 content bytes available
884        let data = [0x0C, 0x01, 0x02]; // ctx 0, len 4, only 2 bytes
885        assert!(decode_optional_context(&data, 0, 0).is_err());
886    }
887
888    #[test]
889    fn opening_closing_tag_extended_round_trip() {
890        let mut buf = BytesMut::new();
891        // Extended tag numbers (>14) for opening/closing
892        for tag_num in [15u8, 20, 100, 254] {
893            buf.clear();
894            encode_opening_tag(&mut buf, tag_num);
895            let (tag, pos1) = decode_tag(&buf, 0).unwrap();
896            assert!(tag.is_opening);
897            assert_eq!(tag.number, tag_num);
898            assert_eq!(pos1, 2); // extended = 2 bytes
899
900            buf.clear();
901            encode_closing_tag(&mut buf, tag_num);
902            let (tag, pos2) = decode_tag(&buf, 0).unwrap();
903            assert!(tag.is_closing);
904            assert_eq!(tag.number, tag_num);
905            assert_eq!(pos2, 2);
906        }
907    }
908}