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