Skip to main content

bacnet_encoding/primitives/
mod.rs

1//! Application-tagged primitive encode/decode per ASHRAE 135-2020 Clause 20.2.
2//!
3//! Provides both raw value codecs (no tag header) and application/context-tagged
4//! convenience functions for all BACnet primitive types.
5
6use bacnet_types::error::Error;
7use bacnet_types::primitives::{BACnetTimeStamp, Date, ObjectIdentifier, PropertyValue, Time};
8use bytes::{BufMut, BytesMut};
9
10use crate::tags::{self, app_tag, TagClass};
11
12// ===========================================================================
13// Raw value codecs (no tag header)
14// ===========================================================================
15
16// --- Unsigned Integer ---
17
18/// Encode an unsigned integer using the minimum number of big-endian octets.
19pub fn encode_unsigned(buf: &mut BytesMut, value: u64) {
20    if value <= 0xFF {
21        buf.put_u8(value as u8);
22    } else if value <= 0xFFFF {
23        buf.put_u16(value as u16);
24    } else if value <= 0xFF_FFFF {
25        buf.put_u8((value >> 16) as u8);
26        buf.put_u16(value as u16);
27    } else if value <= 0xFFFF_FFFF {
28        buf.put_u32(value as u32);
29    } else if value <= 0xFF_FFFF_FFFF {
30        buf.put_u8((value >> 32) as u8);
31        buf.put_u32(value as u32);
32    } else if value <= 0xFFFF_FFFF_FFFF {
33        buf.put_u16((value >> 32) as u16);
34        buf.put_u32(value as u32);
35    } else if value <= 0xFF_FFFF_FFFF_FFFF {
36        buf.put_u8((value >> 48) as u8);
37        buf.put_u16((value >> 32) as u16);
38        buf.put_u32(value as u32);
39    } else {
40        buf.put_u64(value);
41    }
42}
43
44/// Return the number of bytes needed to encode an unsigned value.
45pub fn unsigned_len(value: u64) -> u32 {
46    if value <= 0xFF {
47        1
48    } else if value <= 0xFFFF {
49        2
50    } else if value <= 0xFF_FFFF {
51        3
52    } else if value <= 0xFFFF_FFFF {
53        4
54    } else if value <= 0xFF_FFFF_FFFF {
55        5
56    } else if value <= 0xFFFF_FFFF_FFFF {
57        6
58    } else if value <= 0xFF_FFFF_FFFF_FFFF {
59        7
60    } else {
61        8
62    }
63}
64
65/// Decode an unsigned integer from big-endian bytes (1-8 bytes).
66pub fn decode_unsigned(data: &[u8]) -> Result<u64, Error> {
67    if data.is_empty() || data.len() > 8 {
68        return Err(Error::Decoding {
69            offset: 0,
70            message: format!("unsigned requires 1-8 bytes, got {}", data.len()),
71        });
72    }
73    let mut value: u64 = 0;
74    for &b in data {
75        value = (value << 8) | b as u64;
76    }
77    Ok(value)
78}
79
80// --- Signed Integer ---
81
82/// Encode a signed integer using minimum octets, two's complement, big-endian.
83pub fn encode_signed(buf: &mut BytesMut, value: i32) {
84    let n = signed_len(value);
85    let bytes = value.to_be_bytes();
86    buf.put_slice(&bytes[4 - n as usize..]);
87}
88
89/// Return the number of bytes needed to encode a signed value.
90pub fn signed_len(value: i32) -> u32 {
91    if (-128..=127).contains(&value) {
92        1
93    } else if (-32768..=32767).contains(&value) {
94        2
95    } else if (-8_388_608..=8_388_607).contains(&value) {
96        3
97    } else {
98        4
99    }
100}
101
102/// Decode a signed integer from two's-complement big-endian bytes (1-4 bytes).
103pub fn decode_signed(data: &[u8]) -> Result<i32, Error> {
104    if data.is_empty() || data.len() > 4 {
105        return Err(Error::Decoding {
106            offset: 0,
107            message: format!("signed requires 1-4 bytes, got {}", data.len()),
108        });
109    }
110    let sign_extend = if data[0] & 0x80 != 0 { 0xFF } else { 0x00 };
111    let mut bytes = [sign_extend; 4];
112    bytes[4 - data.len()..].copy_from_slice(data);
113    Ok(i32::from_be_bytes(bytes))
114}
115
116// --- Real ---
117
118/// Encode an IEEE-754 single-precision float (big-endian, 4 bytes).
119pub fn encode_real(buf: &mut BytesMut, value: f32) {
120    buf.put_f32(value);
121}
122
123/// Decode an IEEE-754 single-precision float from 4 big-endian bytes.
124pub fn decode_real(data: &[u8]) -> Result<f32, Error> {
125    if data.len() != 4 {
126        return Err(Error::decoding(
127            0,
128            format!("Real expects exactly 4 bytes, got {}", data.len()),
129        ));
130    }
131    Ok(f32::from_be_bytes([data[0], data[1], data[2], data[3]]))
132}
133
134// --- Double ---
135
136/// Encode an IEEE-754 double-precision float (big-endian, 8 bytes).
137pub fn encode_double(buf: &mut BytesMut, value: f64) {
138    buf.put_f64(value);
139}
140
141/// Decode an IEEE-754 double-precision float from 8 big-endian bytes.
142pub fn decode_double(data: &[u8]) -> Result<f64, Error> {
143    if data.len() != 8 {
144        return Err(Error::decoding(
145            0,
146            format!("Double expects exactly 8 bytes, got {}", data.len()),
147        ));
148    }
149    let bytes: [u8; 8] = [
150        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
151    ];
152    Ok(f64::from_be_bytes(bytes))
153}
154
155// --- Character String ---
156
157/// Character set identifiers.
158pub mod charset {
159    /// ISO 10646 (UTF-8) — X'00'
160    pub const UTF8: u8 = 0;
161    /// IBM/Microsoft DBCS — X'01'
162    pub const IBM_MICROSOFT_DBCS: u8 = 1;
163    /// JIS X 0208 — X'02'
164    pub const JIS_X_0208: u8 = 2;
165    /// ISO 10646 (UCS-4) — X'03'
166    pub const UCS4: u8 = 3;
167    /// ISO 10646 (UCS-2) — X'04'
168    pub const UCS2: u8 = 4;
169    /// ISO 8859-1 — X'05'
170    pub const ISO_8859_1: u8 = 5;
171}
172
173/// Encode a UTF-8 character string with leading charset byte.
174pub fn encode_character_string(buf: &mut BytesMut, value: &str) {
175    buf.put_u8(charset::UTF8);
176    buf.put_slice(value.as_bytes());
177}
178
179/// Return the encoded length of a character string (charset byte + UTF-8 bytes).
180pub fn character_string_len(value: &str) -> Result<u32, Error> {
181    u32::try_from(value.len())
182        .ok()
183        .and_then(|n| n.checked_add(1))
184        .ok_or_else(|| Error::Encoding("CharacterString too long for BACnet encoding".into()))
185}
186
187/// Decode a character string from content bytes.
188///
189/// The first byte is the charset identifier. Supported charsets:
190/// - 0 (UTF-8)
191/// - 4 (UCS-2, big-endian)
192/// - 5 (ISO-8859-1)
193///
194/// Other charsets return an error.
195pub fn decode_character_string(data: &[u8]) -> Result<String, Error> {
196    if data.is_empty() {
197        return Err(Error::Decoding {
198            offset: 0,
199            message: "CharacterString requires at least 1 byte for charset".into(),
200        });
201    }
202    let charset_id = data[0];
203    let payload = &data[1..];
204    match charset_id {
205        charset::UTF8 => String::from_utf8(payload.to_vec()).map_err(|e| Error::Decoding {
206            offset: 1,
207            message: format!("invalid UTF-8: {e}"),
208        }),
209        charset::UCS2 => {
210            if !payload.len().is_multiple_of(2) {
211                return Err(Error::Decoding {
212                    offset: 1,
213                    message: "UCS-2 data must have even length".into(),
214                });
215            }
216            let mut s = String::new();
217            for (i, chunk) in payload.chunks_exact(2).enumerate() {
218                let code_point = u16::from_be_bytes([chunk[0], chunk[1]]);
219                if let Some(c) = char::from_u32(code_point as u32) {
220                    s.push(c);
221                } else {
222                    return Err(Error::Decoding {
223                        offset: 1 + i * 2,
224                        message: "invalid UCS-2 code point".into(),
225                    });
226                }
227            }
228            Ok(s)
229        }
230        charset::ISO_8859_1 => Ok(payload.iter().map(|&b| b as char).collect()),
231        charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 | charset::UCS4 => Err(Error::Decoding {
232            offset: 0,
233            message: format!("unsupported charset: {charset_id}"),
234        }),
235        other => Err(Error::Decoding {
236            offset: 0,
237            message: format!("unknown charset: {other}"),
238        }),
239    }
240}
241
242// --- Bit String ---
243
244/// Encode a bit string: leading unused-bits count followed by data bytes.
245pub fn encode_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
246    buf.put_u8(unused_bits);
247    buf.put_slice(data);
248}
249
250/// Decode a bit string from content bytes.
251///
252/// Returns `(unused_bits, data)`.
253pub fn decode_bit_string(data: &[u8]) -> Result<(u8, Vec<u8>), Error> {
254    if data.is_empty() {
255        return Err(Error::Decoding {
256            offset: 0,
257            message: "BitString requires at least 1 byte for unused-bits count".into(),
258        });
259    }
260    let unused = data[0];
261    if unused > 7 {
262        return Err(Error::Decoding {
263            offset: 0,
264            message: format!("BitString unused_bits must be 0-7, got {unused}"),
265        });
266    }
267    Ok((unused, data[1..].to_vec()))
268}
269
270// ===========================================================================
271// Application-tagged encode helpers
272// ===========================================================================
273
274/// Encode an application-tagged Null.
275pub fn encode_app_null(buf: &mut BytesMut) {
276    tags::encode_tag(buf, app_tag::NULL, TagClass::Application, 0);
277}
278
279/// Encode an application-tagged Boolean.
280///
281/// The value is encoded in the tag's L/V/T bits with no content octets.
282pub fn encode_app_boolean(buf: &mut BytesMut, value: bool) {
283    tags::encode_tag(
284        buf,
285        app_tag::BOOLEAN,
286        TagClass::Application,
287        if value { 1 } else { 0 },
288    );
289}
290
291/// Encode an application-tagged Unsigned.
292pub fn encode_app_unsigned(buf: &mut BytesMut, value: u64) {
293    let len = unsigned_len(value);
294    tags::encode_tag(buf, app_tag::UNSIGNED, TagClass::Application, len);
295    encode_unsigned(buf, value);
296}
297
298/// Encode an application-tagged Signed.
299pub fn encode_app_signed(buf: &mut BytesMut, value: i32) {
300    let len = signed_len(value);
301    tags::encode_tag(buf, app_tag::SIGNED, TagClass::Application, len);
302    encode_signed(buf, value);
303}
304
305/// Encode an application-tagged Real (f32).
306pub fn encode_app_real(buf: &mut BytesMut, value: f32) {
307    tags::encode_tag(buf, app_tag::REAL, TagClass::Application, 4);
308    encode_real(buf, value);
309}
310
311/// Encode an application-tagged Double (f64).
312pub fn encode_app_double(buf: &mut BytesMut, value: f64) {
313    tags::encode_tag(buf, app_tag::DOUBLE, TagClass::Application, 8);
314    encode_double(buf, value);
315}
316
317/// Encode an application-tagged OctetString.
318pub fn encode_app_octet_string(buf: &mut BytesMut, data: &[u8]) {
319    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
320    tags::encode_tag(buf, app_tag::OCTET_STRING, TagClass::Application, data_len);
321    buf.put_slice(data);
322}
323
324/// Encode an application-tagged CharacterString (UTF-8).
325pub fn encode_app_character_string(buf: &mut BytesMut, value: &str) -> Result<(), Error> {
326    let len = character_string_len(value)?;
327    tags::encode_tag(buf, app_tag::CHARACTER_STRING, TagClass::Application, len);
328    encode_character_string(buf, value);
329    Ok(())
330}
331
332/// Encode an application-tagged BitString.
333pub fn encode_app_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
334    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
335    let len = data_len.saturating_add(1);
336    tags::encode_tag(buf, app_tag::BIT_STRING, TagClass::Application, len);
337    encode_bit_string(buf, unused_bits, data);
338}
339
340/// Encode an application-tagged Enumerated.
341pub fn encode_app_enumerated(buf: &mut BytesMut, value: u32) {
342    let len = unsigned_len(value as u64);
343    tags::encode_tag(buf, app_tag::ENUMERATED, TagClass::Application, len);
344    encode_unsigned(buf, value as u64);
345}
346
347/// Encode an application-tagged Date.
348pub fn encode_app_date(buf: &mut BytesMut, date: &Date) {
349    tags::encode_tag(buf, app_tag::DATE, TagClass::Application, 4);
350    buf.put_slice(&date.encode());
351}
352
353/// Encode an application-tagged Time.
354pub fn encode_app_time(buf: &mut BytesMut, time: &Time) {
355    tags::encode_tag(buf, app_tag::TIME, TagClass::Application, 4);
356    buf.put_slice(&time.encode());
357}
358
359/// Encode an application-tagged ObjectIdentifier.
360pub fn encode_app_object_id(buf: &mut BytesMut, oid: &ObjectIdentifier) {
361    tags::encode_tag(buf, app_tag::OBJECT_IDENTIFIER, TagClass::Application, 4);
362    buf.put_slice(&oid.encode());
363}
364
365// ===========================================================================
366// Context-tagged encode helpers
367// ===========================================================================
368
369/// Encode a context-tagged Unsigned.
370pub fn encode_ctx_unsigned(buf: &mut BytesMut, tag: u8, value: u64) {
371    let len = unsigned_len(value);
372    tags::encode_tag(buf, tag, TagClass::Context, len);
373    encode_unsigned(buf, value);
374}
375
376/// Encode a context-tagged Signed.
377pub fn encode_ctx_signed(buf: &mut BytesMut, tag: u8, value: i32) {
378    let len = signed_len(value);
379    tags::encode_tag(buf, tag, TagClass::Context, len);
380    encode_signed(buf, value);
381}
382
383/// Encode a context-tagged Real (f32).
384pub fn encode_ctx_real(buf: &mut BytesMut, tag: u8, value: f32) {
385    tags::encode_tag(buf, tag, TagClass::Context, 4);
386    encode_real(buf, value);
387}
388
389/// Encode a context-tagged Double (f64).
390pub fn encode_ctx_double(buf: &mut BytesMut, tag: u8, value: f64) {
391    tags::encode_tag(buf, tag, TagClass::Context, 8);
392    encode_double(buf, value);
393}
394
395/// Encode a context-tagged Enumerated.
396pub fn encode_ctx_enumerated(buf: &mut BytesMut, tag: u8, value: u32) {
397    let len = unsigned_len(value as u64);
398    tags::encode_tag(buf, tag, TagClass::Context, len);
399    encode_unsigned(buf, value as u64);
400}
401
402/// Encode a context-tagged Boolean.
403///
404/// Context-tagged booleans use a 1-byte content octet (unlike application-tagged).
405pub fn encode_ctx_boolean(buf: &mut BytesMut, tag: u8, value: bool) {
406    tags::encode_tag(buf, tag, TagClass::Context, 1);
407    buf.put_u8(if value { 1 } else { 0 });
408}
409
410/// Encode a context-tagged ObjectIdentifier.
411pub fn encode_ctx_object_id(buf: &mut BytesMut, tag: u8, oid: &ObjectIdentifier) {
412    tags::encode_tag(buf, tag, TagClass::Context, 4);
413    buf.put_slice(&oid.encode());
414}
415
416/// Encode a context-tagged OctetString.
417pub fn encode_ctx_octet_string(buf: &mut BytesMut, tag: u8, data: &[u8]) {
418    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
419    tags::encode_tag(buf, tag, TagClass::Context, data_len);
420    buf.put_slice(data);
421}
422
423/// Encode a context-tagged CharacterString (UTF-8).
424pub fn encode_ctx_character_string(buf: &mut BytesMut, tag: u8, value: &str) -> Result<(), Error> {
425    let len = character_string_len(value)?;
426    tags::encode_tag(buf, tag, TagClass::Context, len);
427    encode_character_string(buf, value);
428    Ok(())
429}
430
431/// Encode a context-tagged Date.
432pub fn encode_ctx_date(buf: &mut BytesMut, tag: u8, date: &Date) {
433    tags::encode_tag(buf, tag, TagClass::Context, 4);
434    buf.put_slice(&date.encode());
435}
436
437/// Encode a context-tagged BitString.
438pub fn encode_ctx_bit_string(buf: &mut BytesMut, tag: u8, unused_bits: u8, data: &[u8]) {
439    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
440    let len = data_len.saturating_add(1);
441    tags::encode_tag(buf, tag, TagClass::Context, len);
442    encode_bit_string(buf, unused_bits, data);
443}
444
445// ===========================================================================
446// Application-tagged decode (dispatches by tag number)
447// ===========================================================================
448
449/// Decode a single application-tagged value from `data` at `offset`.
450///
451/// Returns the decoded `PropertyValue` and the new offset past the consumed bytes.
452pub fn decode_application_value(
453    data: &[u8],
454    offset: usize,
455) -> Result<(PropertyValue, usize), Error> {
456    let (tag, new_offset) = tags::decode_tag(data, offset)?;
457    if tag.class != TagClass::Application {
458        return Err(Error::decoding(
459            offset,
460            format!("expected application tag, got context tag {}", tag.number),
461        ));
462    }
463    if tag.is_opening || tag.is_closing {
464        return Err(Error::decoding(offset, "unexpected opening/closing tag"));
465    }
466
467    let content_start = new_offset;
468    let content_len = tag.length as usize;
469    let content_end = content_start
470        .checked_add(content_len)
471        .ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
472
473    if tag.number == app_tag::BOOLEAN {
474        return Ok((PropertyValue::Boolean(tag.length != 0), content_start));
475    }
476
477    if data.len() < content_end {
478        return Err(Error::buffer_too_short(content_end, data.len()));
479    }
480
481    let content = &data[content_start..content_end];
482
483    let value = match tag.number {
484        app_tag::NULL => PropertyValue::Null,
485        app_tag::UNSIGNED => PropertyValue::Unsigned(decode_unsigned(content)?),
486        app_tag::SIGNED => PropertyValue::Signed(decode_signed(content)?),
487        app_tag::REAL => PropertyValue::Real(decode_real(content)?),
488        app_tag::DOUBLE => PropertyValue::Double(decode_double(content)?),
489        app_tag::OCTET_STRING => PropertyValue::OctetString(content.to_vec()),
490        app_tag::CHARACTER_STRING => {
491            PropertyValue::CharacterString(decode_character_string(content)?)
492        }
493        app_tag::BIT_STRING => {
494            let (unused, bits) = decode_bit_string(content)?;
495            PropertyValue::BitString {
496                unused_bits: unused,
497                data: bits,
498            }
499        }
500        app_tag::ENUMERATED => PropertyValue::Enumerated(decode_unsigned(content)? as u32),
501        app_tag::DATE => PropertyValue::Date(Date::decode(content)?),
502        app_tag::TIME => PropertyValue::Time(Time::decode(content)?),
503        app_tag::OBJECT_IDENTIFIER => {
504            PropertyValue::ObjectIdentifier(ObjectIdentifier::decode(content)?)
505        }
506        other => {
507            return Err(Error::decoding(
508                offset,
509                format!("unknown application tag number {other}"),
510            ));
511        }
512    };
513
514    Ok((value, content_end))
515}
516
517/// Encode a `PropertyValue` as an application-tagged value.
518pub fn encode_property_value(buf: &mut BytesMut, value: &PropertyValue) -> Result<(), Error> {
519    match value {
520        PropertyValue::Null => encode_app_null(buf),
521        PropertyValue::Boolean(v) => encode_app_boolean(buf, *v),
522        PropertyValue::Unsigned(v) => encode_app_unsigned(buf, *v),
523        PropertyValue::Signed(v) => encode_app_signed(buf, *v),
524        PropertyValue::Real(v) => encode_app_real(buf, *v),
525        PropertyValue::Double(v) => encode_app_double(buf, *v),
526        PropertyValue::OctetString(v) => encode_app_octet_string(buf, v),
527        PropertyValue::CharacterString(v) => encode_app_character_string(buf, v)?,
528        PropertyValue::BitString { unused_bits, data } => {
529            encode_app_bit_string(buf, *unused_bits, data)
530        }
531        PropertyValue::Enumerated(v) => encode_app_enumerated(buf, *v),
532        PropertyValue::Date(v) => encode_app_date(buf, v),
533        PropertyValue::Time(v) => encode_app_time(buf, v),
534        PropertyValue::ObjectIdentifier(v) => encode_app_object_id(buf, v),
535        PropertyValue::List(values) => {
536            for v in values {
537                encode_property_value(buf, v)?;
538            }
539        }
540    }
541    Ok(())
542}
543
544// ===========================================================================
545// BACnetTimeStamp encode/decode
546// ===========================================================================
547
548/// Encode a BACnetTimeStamp wrapped in a context opening/closing tag pair.
549///
550/// The outer `tag_number` is the context tag of the enclosing field.
551/// Inside, the CHOICE variant uses its own context tag (0=Time,
552/// 1=SequenceNumber, 2=DateTime).
553pub fn encode_timestamp(buf: &mut BytesMut, tag_number: u8, ts: &BACnetTimeStamp) {
554    tags::encode_opening_tag(buf, tag_number);
555    match ts {
556        BACnetTimeStamp::Time(t) => {
557            tags::encode_tag(buf, 0, TagClass::Context, 4);
558            buf.put_slice(&t.encode());
559        }
560        BACnetTimeStamp::SequenceNumber(n) => {
561            encode_ctx_unsigned(buf, 1, *n);
562        }
563        BACnetTimeStamp::DateTime { date, time } => {
564            tags::encode_opening_tag(buf, 2);
565            encode_app_date(buf, date);
566            encode_app_time(buf, time);
567            tags::encode_closing_tag(buf, 2);
568        }
569    }
570    tags::encode_closing_tag(buf, tag_number);
571}
572
573/// Decode a BACnetTimeStamp from inside a context opening/closing tag pair.
574///
575/// `data` should point to the start of the outer opening tag for `tag_number`.
576/// Returns the decoded timestamp and the new offset past the outer closing tag.
577pub fn decode_timestamp(
578    data: &[u8],
579    offset: usize,
580    tag_number: u8,
581) -> Result<(BACnetTimeStamp, usize), Error> {
582    let (tag, pos) = tags::decode_tag(data, offset)?;
583    if !tag.is_opening_tag(tag_number) {
584        return Err(Error::decoding(
585            offset,
586            format!("expected opening tag {tag_number} for BACnetTimeStamp"),
587        ));
588    }
589
590    let (inner_tag, inner_pos) = tags::decode_tag(data, pos)?;
591
592    let (ts, after_inner) = if inner_tag.is_context(0) {
593        let end = inner_pos
594            .checked_add(inner_tag.length as usize)
595            .ok_or_else(|| Error::decoding(inner_pos, "BACnetTimeStamp Time length overflow"))?;
596        if end > data.len() {
597            return Err(Error::decoding(inner_pos, "BACnetTimeStamp Time truncated"));
598        }
599        let t = Time::decode(&data[inner_pos..end])?;
600        (BACnetTimeStamp::Time(t), end)
601    } else if inner_tag.is_context(1) {
602        let end = inner_pos
603            .checked_add(inner_tag.length as usize)
604            .ok_or_else(|| {
605                Error::decoding(inner_pos, "BACnetTimeStamp SequenceNumber length overflow")
606            })?;
607        if end > data.len() {
608            return Err(Error::decoding(
609                inner_pos,
610                "BACnetTimeStamp SequenceNumber truncated",
611            ));
612        }
613        let n = decode_unsigned(&data[inner_pos..end])?;
614        (BACnetTimeStamp::SequenceNumber(n), end)
615    } else if inner_tag.is_opening_tag(2) {
616        let (date_tag, date_pos) = tags::decode_tag(data, inner_pos)?;
617        if date_tag.class != TagClass::Application || date_tag.number != app_tag::DATE {
618            return Err(Error::decoding(
619                inner_pos,
620                "BACnetTimeStamp DateTime expected Date",
621            ));
622        }
623        let date_end = date_pos
624            .checked_add(date_tag.length as usize)
625            .ok_or_else(|| {
626                Error::decoding(date_pos, "BACnetTimeStamp DateTime Date length overflow")
627            })?;
628        if date_end > data.len() {
629            return Err(Error::decoding(
630                date_pos,
631                "BACnetTimeStamp DateTime Date truncated",
632            ));
633        }
634        let date = Date::decode(&data[date_pos..date_end])?;
635
636        let (time_tag, time_pos) = tags::decode_tag(data, date_end)?;
637        if time_tag.class != TagClass::Application || time_tag.number != app_tag::TIME {
638            return Err(Error::decoding(
639                date_end,
640                "BACnetTimeStamp DateTime expected Time",
641            ));
642        }
643        let time_end = time_pos
644            .checked_add(time_tag.length as usize)
645            .ok_or_else(|| {
646                Error::decoding(time_pos, "BACnetTimeStamp DateTime Time length overflow")
647            })?;
648        if time_end > data.len() {
649            return Err(Error::decoding(
650                time_pos,
651                "BACnetTimeStamp DateTime Time truncated",
652            ));
653        }
654        let time = Time::decode(&data[time_pos..time_end])?;
655
656        let (close_tag, close_pos) = tags::decode_tag(data, time_end)?;
657        if !close_tag.is_closing_tag(2) {
658            return Err(Error::decoding(
659                time_end,
660                "BACnetTimeStamp DateTime missing closing tag 2",
661            ));
662        }
663        (BACnetTimeStamp::DateTime { date, time }, close_pos)
664    } else {
665        return Err(Error::decoding(
666            pos,
667            "BACnetTimeStamp: unexpected inner choice tag",
668        ));
669    };
670
671    let (close, final_pos) = tags::decode_tag(data, after_inner)?;
672    if !close.is_closing_tag(tag_number) {
673        return Err(Error::decoding(
674            after_inner,
675            format!("expected closing tag {tag_number} for BACnetTimeStamp"),
676        ));
677    }
678
679    Ok((ts, final_pos))
680}
681
682// ===========================================================================
683// Tests
684// ===========================================================================
685
686#[cfg(test)]
687mod tests;