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
182    if tag_number == 0x0F {
183        if pos >= data.len() {
184            return Err(Error::decoding(pos, "truncated extended tag number"));
185        }
186        tag_number = data[pos];
187        pos += 1;
188    }
189
190    if class == TagClass::Context {
191        if lvt == 6 {
192            return Ok((
193                Tag {
194                    number: tag_number,
195                    class,
196                    length: 0,
197                    is_opening: true,
198                    is_closing: false,
199                },
200                pos,
201            ));
202        }
203        if lvt == 7 {
204            return Ok((
205                Tag {
206                    number: tag_number,
207                    class,
208                    length: 0,
209                    is_opening: false,
210                    is_closing: true,
211                },
212                pos,
213            ));
214        }
215    }
216
217    let length = if lvt < 5 {
218        lvt as u32
219    } else {
220        if pos >= data.len() {
221            return Err(Error::decoding(pos, "truncated extended length"));
222        }
223        let ext = data[pos];
224        pos += 1;
225
226        match ext {
227            0..=253 => ext as u32,
228            254 => {
229                if pos + 2 > data.len() {
230                    return Err(Error::decoding(pos, "truncated 2-byte extended length"));
231                }
232                let len = u16::from_be_bytes([data[pos], data[pos + 1]]) as u32;
233                pos += 2;
234                len
235            }
236            255 => {
237                if pos + 4 > data.len() {
238                    return Err(Error::decoding(pos, "truncated 4-byte extended length"));
239                }
240                let len =
241                    u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
242                pos += 4;
243                len
244            }
245        }
246    };
247
248    if length > MAX_TAG_LENGTH {
249        return Err(Error::decoding(
250            offset,
251            format!("tag length ({length}) exceeds sanity limit ({MAX_TAG_LENGTH})"),
252        ));
253    }
254
255    Ok((
256        Tag {
257            number: tag_number,
258            class,
259            length,
260            is_opening: false,
261            is_closing: false,
262        },
263        pos,
264    ))
265}
266
267/// Extract raw bytes enclosed by a context opening/closing tag pair.
268///
269/// Reads from `offset` (immediately after the opening tag) through the
270/// matching closing tag, handling nested opening/closing tags.
271///
272/// Returns the enclosed bytes and the offset past the closing tag.
273pub fn extract_context_value(
274    data: &[u8],
275    offset: usize,
276    tag_number: u8,
277) -> Result<(&[u8], usize), Error> {
278    let value_start = offset;
279    let mut pos = offset;
280    let mut depth: usize = 1;
281
282    while depth > 0 && pos < data.len() {
283        let (tag, new_pos) = decode_tag(data, pos)?;
284
285        if tag.is_opening {
286            depth += 1;
287            if depth > MAX_CONTEXT_NESTING_DEPTH {
288                return Err(Error::decoding(
289                    pos,
290                    format!(
291                        "context tag nesting depth exceeds maximum ({MAX_CONTEXT_NESTING_DEPTH})"
292                    ),
293                ));
294            }
295            pos = new_pos;
296        } else if tag.is_closing {
297            depth -= 1;
298            if depth == 0 {
299                if tag.number != tag_number {
300                    return Err(Error::decoding(
301                        pos,
302                        format!(
303                            "closing tag {} does not match opening tag {tag_number}",
304                            tag.number
305                        ),
306                    ));
307                }
308                let value_end = pos;
309                return Ok((&data[value_start..value_end], new_pos));
310            }
311            pos = new_pos;
312        } else if tag.class == TagClass::Application && tag.number == app_tag::BOOLEAN {
313            pos = new_pos;
314        } else {
315            let content_end = new_pos
316                .checked_add(tag.length as usize)
317                .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
318            if content_end > data.len() {
319                return Err(Error::decoding(
320                    new_pos,
321                    format!(
322                        "tag data overflows buffer: need {} bytes at offset {new_pos}",
323                        tag.length
324                    ),
325                ));
326            }
327            pos = content_end;
328        }
329    }
330
331    Err(Error::decoding(
332        offset,
333        format!("missing closing tag {tag_number}"),
334    ))
335}
336
337/// Try to decode an optional context-tagged primitive value.
338///
339/// Peeks at the next tag; if it matches the expected context tag number,
340/// returns the content slice and advances the offset. Otherwise returns
341/// `(None, offset)` unchanged.
342pub fn decode_optional_context(
343    data: &[u8],
344    offset: usize,
345    tag_number: u8,
346) -> Result<(Option<&[u8]>, usize), Error> {
347    if offset >= data.len() {
348        return Ok((None, offset));
349    }
350
351    let (tag, new_pos) = decode_tag(data, offset)?;
352    if tag.is_context(tag_number) {
353        let end = new_pos
354            .checked_add(tag.length as usize)
355            .ok_or_else(|| Error::decoding(new_pos, "tag length overflow"))?;
356        if end > data.len() {
357            return Err(Error::buffer_too_short(end, data.len()));
358        }
359        Ok((Some(&data[new_pos..end]), end))
360    } else {
361        Ok((None, offset))
362    }
363}
364
365// ---------------------------------------------------------------------------
366// Tests
367// ---------------------------------------------------------------------------
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    fn encode_to_vec(tag_number: u8, class: TagClass, length: u32) -> Vec<u8> {
374        let mut buf = BytesMut::with_capacity(8);
375        encode_tag(&mut buf, tag_number, class, length);
376        buf.to_vec()
377    }
378
379    #[test]
380    fn encode_single_byte_application_tag() {
381        // Tag 0 (Null), Application, length 0
382        assert_eq!(encode_to_vec(0, TagClass::Application, 0), vec![0x00]);
383        // Tag 2 (Unsigned), Application, length 1
384        assert_eq!(encode_to_vec(2, TagClass::Application, 1), vec![0x21]);
385        // Tag 4 (Real), Application, length 4
386        assert_eq!(encode_to_vec(4, TagClass::Application, 4), vec![0x44]);
387        // Tag 12 (ObjectIdentifier), Application, length 4
388        assert_eq!(encode_to_vec(12, TagClass::Application, 4), vec![0xC4]);
389    }
390
391    #[test]
392    fn encode_single_byte_context_tag() {
393        // Context tag 0, length 1
394        assert_eq!(encode_to_vec(0, TagClass::Context, 1), vec![0x09]);
395        // Context tag 1, length 4
396        assert_eq!(encode_to_vec(1, TagClass::Context, 4), vec![0x1C]);
397        // Context tag 2, length 0: (2<<4) | (1<<3) | 0 = 0x28
398        assert_eq!(encode_to_vec(2, TagClass::Context, 0), vec![0x28]);
399    }
400
401    #[test]
402    fn encode_extended_tag_number() {
403        // Tag 15 (extended), Application, length 1
404        let encoded = encode_to_vec(15, TagClass::Application, 1);
405        assert_eq!(encoded, vec![0xF1, 15]);
406
407        // Tag 20 (extended), Context, length 2
408        let encoded = encode_to_vec(20, TagClass::Context, 2);
409        assert_eq!(encoded, vec![0xFA, 20]);
410    }
411
412    #[test]
413    fn encode_extended_length() {
414        // Tag 2, Application, length 100
415        let encoded = encode_to_vec(2, TagClass::Application, 100);
416        assert_eq!(encoded, vec![0x25, 100]);
417
418        // Tag 2, Application, length 1000 (2-byte extended)
419        let encoded = encode_to_vec(2, TagClass::Application, 1000);
420        assert_eq!(encoded, vec![0x25, 254, 0x03, 0xE8]);
421
422        // Tag 2, Application, length 100000 (4-byte extended)
423        let encoded = encode_to_vec(2, TagClass::Application, 100000);
424        assert_eq!(encoded, vec![0x25, 255, 0x00, 0x01, 0x86, 0xA0]);
425    }
426
427    #[test]
428    fn encode_extended_tag_and_length() {
429        // Tag 20, Context, length 100
430        let encoded = encode_to_vec(20, TagClass::Context, 100);
431        assert_eq!(encoded, vec![0xFD, 20, 100]);
432    }
433
434    #[test]
435    fn encode_opening_closing_tags() {
436        let mut buf = BytesMut::new();
437
438        // Opening tag 0: 0x0E
439        encode_opening_tag(&mut buf, 0);
440        assert_eq!(&buf[..], &[0x0E]);
441
442        buf.clear();
443        // Closing tag 0: 0x0F
444        encode_closing_tag(&mut buf, 0);
445        assert_eq!(&buf[..], &[0x0F]);
446
447        buf.clear();
448        // Opening tag 3: 0x3E
449        encode_opening_tag(&mut buf, 3);
450        assert_eq!(&buf[..], &[0x3E]);
451
452        buf.clear();
453        // Extended opening tag 20: 0xFE, 20
454        encode_opening_tag(&mut buf, 20);
455        assert_eq!(&buf[..], &[0xFE, 20]);
456
457        buf.clear();
458        // Extended closing tag 20: 0xFF, 20
459        encode_closing_tag(&mut buf, 20);
460        assert_eq!(&buf[..], &[0xFF, 20]);
461    }
462
463    #[test]
464    fn decode_single_byte_tag() {
465        // Application tag 2, length 1: 0x21
466        let (tag, pos) = decode_tag(&[0x21], 0).unwrap();
467        assert_eq!(tag.number, 2);
468        assert_eq!(tag.class, TagClass::Application);
469        assert_eq!(tag.length, 1);
470        assert!(!tag.is_opening);
471        assert!(!tag.is_closing);
472        assert_eq!(pos, 1);
473    }
474
475    #[test]
476    fn decode_context_tag() {
477        // Context tag 1, length 4: 0x1C
478        let (tag, pos) = decode_tag(&[0x1C], 0).unwrap();
479        assert_eq!(tag.number, 1);
480        assert_eq!(tag.class, TagClass::Context);
481        assert_eq!(tag.length, 4);
482        assert_eq!(pos, 1);
483    }
484
485    #[test]
486    fn decode_opening_closing_tags() {
487        let (tag, _) = decode_tag(&[0x0E], 0).unwrap();
488        assert!(tag.is_opening);
489        assert_eq!(tag.number, 0);
490
491        let (tag, _) = decode_tag(&[0x0F], 0).unwrap();
492        assert!(tag.is_closing);
493        assert_eq!(tag.number, 0);
494
495        let (tag, _) = decode_tag(&[0x3E], 0).unwrap();
496        assert!(tag.is_opening);
497        assert_eq!(tag.number, 3);
498    }
499
500    #[test]
501    fn decode_extended_tag_number() {
502        // Extended tag 20, Application, length 1: 0xF1, 20
503        let (tag, pos) = decode_tag(&[0xF1, 20], 0).unwrap();
504        assert_eq!(tag.number, 20);
505        assert_eq!(tag.class, TagClass::Application);
506        assert_eq!(tag.length, 1);
507        assert_eq!(pos, 2);
508    }
509
510    #[test]
511    fn decode_extended_length() {
512        // Tag 2, Application, length 100: 0x25, 100
513        let (tag, pos) = decode_tag(&[0x25, 100], 0).unwrap();
514        assert_eq!(tag.number, 2);
515        assert_eq!(tag.length, 100);
516        assert_eq!(pos, 2);
517
518        // Tag 2, Application, length 1000: 0x25, 254, 0x03, 0xE8
519        let (tag, pos) = decode_tag(&[0x25, 254, 0x03, 0xE8], 0).unwrap();
520        assert_eq!(tag.number, 2);
521        assert_eq!(tag.length, 1000);
522        assert_eq!(pos, 4);
523
524        // Tag 2, Application, length 100000: 0x25, 255, 0x00, 0x01, 0x86, 0xA0
525        let (tag, pos) = decode_tag(&[0x25, 255, 0x00, 0x01, 0x86, 0xA0], 0).unwrap();
526        assert_eq!(tag.number, 2);
527        assert_eq!(tag.length, 100000);
528        assert_eq!(pos, 6);
529    }
530
531    #[test]
532    fn decode_tag_round_trip() {
533        // Test that encode -> decode round-trips for various combinations
534        let cases = [
535            (0u8, TagClass::Application, 0u32),
536            (1, TagClass::Application, 1),
537            (4, TagClass::Application, 4),
538            (12, TagClass::Application, 4),
539            (0, TagClass::Context, 1),
540            (3, TagClass::Context, 4),
541            (15, TagClass::Application, 1),
542            (20, TagClass::Context, 2),
543            (2, TagClass::Application, 100),
544            (2, TagClass::Application, 1000),
545            (20, TagClass::Context, 100),
546        ];
547
548        for (tag_num, class, length) in cases {
549            let encoded = encode_to_vec(tag_num, class, length);
550            let (decoded, _) = decode_tag(&encoded, 0).unwrap();
551            assert_eq!(
552                decoded.number, tag_num,
553                "tag number mismatch for ({tag_num}, {class:?}, {length})"
554            );
555            assert_eq!(
556                decoded.class, class,
557                "class mismatch for ({tag_num}, {class:?}, {length})"
558            );
559            assert_eq!(
560                decoded.length, length,
561                "length mismatch for ({tag_num}, {class:?}, {length})"
562            );
563        }
564    }
565
566    #[test]
567    fn decode_tag_empty_buffer() {
568        assert!(decode_tag(&[], 0).is_err());
569    }
570
571    #[test]
572    fn decode_tag_truncated_extended() {
573        // Extended tag number but no second byte
574        assert!(decode_tag(&[0xF1], 0).is_err());
575    }
576
577    #[test]
578    fn decode_tag_excessive_length() {
579        // Craft a tag with length > 1MB
580        let data = [0x25, 255, 0x10, 0x00, 0x00, 0x00]; // length = 0x10000000
581        assert!(decode_tag(&data, 0).is_err());
582    }
583
584    #[test]
585    fn boolean_tag_detection() {
586        // Application boolean true: tag 1, LVT = 1
587        let (tag, _) = decode_tag(&[0x11], 0).unwrap();
588        assert!(tag.is_boolean_true());
589
590        // Application boolean false: tag 1, LVT = 0
591        let (tag, _) = decode_tag(&[0x10], 0).unwrap();
592        assert!(!tag.is_boolean_true());
593
594        // Not a boolean (tag 2): should not be detected as boolean
595        let (tag, _) = decode_tag(&[0x21], 0).unwrap();
596        assert!(!tag.is_boolean_true());
597    }
598
599    #[test]
600    fn extract_context_value_simple() {
601        // Opening tag 0, some data (tag 2, len 1, value 42), closing tag 0
602        let data = [0x0E, 0x21, 42, 0x0F];
603        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
604        assert_eq!(value, &[0x21, 42]);
605        assert_eq!(pos, 4);
606    }
607
608    #[test]
609    fn extract_context_value_nested() {
610        // Opening tag 0, opening tag 1, data, closing tag 1, closing tag 0
611        let data = [0x0E, 0x1E, 0x21, 42, 0x1F, 0x0F];
612        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
613        assert_eq!(value, &[0x1E, 0x21, 42, 0x1F]);
614        assert_eq!(pos, 6);
615    }
616
617    #[test]
618    fn extract_context_value_missing_close() {
619        let data = [0x0E, 0x21, 42]; // No closing tag
620        assert!(extract_context_value(&data, 1, 0).is_err());
621    }
622
623    #[test]
624    fn tag_is_context_helper() {
625        let (tag, _) = decode_tag(&[0x09], 0).unwrap(); // context 0, len 1
626        assert!(tag.is_context(0));
627        assert!(!tag.is_context(1));
628    }
629
630    #[test]
631    fn decode_optional_context_present() {
632        // Context tag 0, length 1, value byte 42
633        let data = [0x09, 42];
634        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
635        assert_eq!(value, Some(&[42u8][..]));
636        assert_eq!(pos, 2);
637    }
638
639    #[test]
640    fn decode_optional_context_absent() {
641        // Context tag 1, but we're looking for tag 0
642        let data = [0x19, 42];
643        let (value, pos) = decode_optional_context(&data, 0, 0).unwrap();
644        assert!(value.is_none());
645        assert_eq!(pos, 0); // offset unchanged
646    }
647
648    #[test]
649    fn decode_optional_context_empty_buffer() {
650        let (value, pos) = decode_optional_context(&[], 0, 0).unwrap();
651        assert!(value.is_none());
652        assert_eq!(pos, 0);
653    }
654
655    // --- Edge case tests ---
656
657    #[test]
658    fn extract_context_value_mismatched_closing_tag() {
659        // Opening tag 0, data, closing tag 1 (mismatch!)
660        let data = [0x0E, 0x21, 42, 0x1F]; // open 0, data, close 1
661        assert!(extract_context_value(&data, 1, 0).is_err());
662    }
663
664    #[test]
665    fn extract_context_value_deeply_nested() {
666        // Verify correct extraction with multiple nesting levels
667        // open 0, open 1, open 2, data, close 2, close 1, close 0
668        let data = [
669            0x0E, // opening 0
670            0x1E, // opening 1
671            0x2E, // opening 2
672            0x21, 42,   // app tag 2 len 1, value 42
673            0x2F, // closing 2
674            0x1F, // closing 1
675            0x0F, // closing 0
676        ];
677        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
678        assert_eq!(value, &data[1..7]); // everything between open 0 and close 0
679        assert_eq!(pos, 8);
680    }
681
682    #[test]
683    fn extract_context_value_nesting_depth_exceeded() {
684        // Build a deeply nested structure that exceeds MAX_CONTEXT_NESTING_DEPTH
685        let mut data = Vec::new();
686        // We start at depth 1 (already inside opening tag), so we need
687        // MAX_CONTEXT_NESTING_DEPTH more opening tags to exceed the limit
688        for i in 0..MAX_CONTEXT_NESTING_DEPTH {
689            let tag = (i % 15) as u8; // cycle through tag numbers 0-14
690            data.push((tag << 4) | 0x0E); // opening tag
691        }
692        // The function starts at depth 1, so this should trigger the limit
693        let result = extract_context_value(&data, 0, 0);
694        assert!(result.is_err());
695    }
696
697    #[test]
698    fn decode_tag_at_nonzero_offset() {
699        // Verify decode works correctly when starting at a non-zero offset
700        let data = [0xFF, 0xFF, 0x21, 0x00]; // garbage, then tag 2 len 1
701        let (tag, pos) = decode_tag(&data, 2).unwrap();
702        assert_eq!(tag.number, 2);
703        assert_eq!(tag.class, TagClass::Application);
704        assert_eq!(tag.length, 1);
705        assert_eq!(pos, 3);
706    }
707
708    #[test]
709    fn decode_tag_offset_beyond_buffer() {
710        let data = [0x21];
711        assert!(decode_tag(&data, 5).is_err());
712    }
713
714    #[test]
715    fn decode_tag_truncated_2byte_extended_length() {
716        // Extended length indicator 254 but only 1 byte follows
717        let data = [0x25, 254, 0x03]; // need 2 bytes after 254
718        assert!(decode_tag(&data, 0).is_err());
719    }
720
721    #[test]
722    fn decode_tag_truncated_4byte_extended_length() {
723        // Extended length indicator 255 but only 2 bytes follow
724        let data = [0x25, 255, 0x00, 0x01];
725        assert!(decode_tag(&data, 0).is_err());
726    }
727
728    #[test]
729    fn extract_context_value_with_boolean_inside() {
730        // Boolean tags have no content octets, which is a special case
731        // Opening tag 0, boolean true (app tag 1, lvt=1), closing tag 0
732        let data = [0x0E, 0x11, 0x0F]; // open 0, bool true, close 0
733        let (value, pos) = extract_context_value(&data, 1, 0).unwrap();
734        assert_eq!(value, &[0x11]); // just the boolean tag
735        assert_eq!(pos, 3);
736    }
737
738    #[test]
739    fn extract_context_value_tag_data_overflows_buffer() {
740        // Opening tag 0, a data tag claiming 100 bytes of content but only 2 available
741        let data = [0x0E, 0x25, 100, 0x01, 0x02, 0x0F];
742        let result = extract_context_value(&data, 1, 0);
743        assert!(result.is_err());
744    }
745
746    #[test]
747    fn decode_optional_context_content_overflows() {
748        // Context tag 0, length 4, but only 2 content bytes available
749        let data = [0x0C, 0x01, 0x02]; // ctx 0, len 4, only 2 bytes
750        assert!(decode_optional_context(&data, 0, 0).is_err());
751    }
752
753    #[test]
754    fn opening_closing_tag_extended_round_trip() {
755        let mut buf = BytesMut::new();
756        // Extended tag numbers (>14) for opening/closing
757        for tag_num in [15u8, 20, 100, 254] {
758            buf.clear();
759            encode_opening_tag(&mut buf, tag_num);
760            let (tag, pos1) = decode_tag(&buf, 0).unwrap();
761            assert!(tag.is_opening);
762            assert_eq!(tag.number, tag_num);
763            assert_eq!(pos1, 2); // extended = 2 bytes
764
765            buf.clear();
766            encode_closing_tag(&mut buf, tag_num);
767            let (tag, pos2) = decode_tag(&buf, 0).unwrap();
768            assert!(tag.is_closing);
769            assert_eq!(tag.number, tag_num);
770            assert_eq!(pos2, 2);
771        }
772    }
773}