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