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
116pub(crate) fn decode_signed_canonical(data: &[u8]) -> Result<i32, Error> {
117    if data.len() > 1
118        && ((data[0] == 0 && data[1] & 0x80 == 0) || (data[0] == 0xFF && data[1] & 0x80 != 0))
119    {
120        return Err(Error::decoding(0, "signed contains a redundant sign octet"));
121    }
122    decode_signed(data)
123}
124
125// --- Real ---
126
127/// Encode an IEEE-754 single-precision float (big-endian, 4 bytes).
128pub fn encode_real(buf: &mut BytesMut, value: f32) {
129    buf.put_f32(value);
130}
131
132/// Decode an IEEE-754 single-precision float from 4 big-endian bytes.
133pub fn decode_real(data: &[u8]) -> Result<f32, Error> {
134    if data.len() != 4 {
135        return Err(Error::decoding(
136            0,
137            format!("Real expects exactly 4 bytes, got {}", data.len()),
138        ));
139    }
140    Ok(f32::from_be_bytes([data[0], data[1], data[2], data[3]]))
141}
142
143// --- Double ---
144
145/// Encode an IEEE-754 double-precision float (big-endian, 8 bytes).
146pub fn encode_double(buf: &mut BytesMut, value: f64) {
147    buf.put_f64(value);
148}
149
150/// Decode an IEEE-754 double-precision float from 8 big-endian bytes.
151pub fn decode_double(data: &[u8]) -> Result<f64, Error> {
152    if data.len() != 8 {
153        return Err(Error::decoding(
154            0,
155            format!("Double expects exactly 8 bytes, got {}", data.len()),
156        ));
157    }
158    let bytes: [u8; 8] = [
159        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
160    ];
161    Ok(f64::from_be_bytes(bytes))
162}
163
164// --- Character String ---
165
166/// Character set identifiers.
167pub mod charset {
168    /// ISO 10646 (UTF-8) — X'00'
169    pub const UTF8: u8 = 0;
170    /// IBM/Microsoft DBCS — X'01'
171    pub const IBM_MICROSOFT_DBCS: u8 = 1;
172    /// JIS X 0208 — X'02'
173    pub const JIS_X_0208: u8 = 2;
174    /// ISO 10646 (UCS-4) — X'03'
175    pub const UCS4: u8 = 3;
176    /// ISO 10646 (UCS-2) — X'04'
177    pub const UCS2: u8 = 4;
178    /// ISO 8859-1 — X'05'
179    pub const ISO_8859_1: u8 = 5;
180}
181
182/// Encode a UTF-8 character string with leading charset byte.
183pub fn encode_character_string(buf: &mut BytesMut, value: &str) {
184    buf.put_u8(charset::UTF8);
185    buf.put_slice(value.as_bytes());
186}
187
188/// Return the encoded length of a character string (charset byte + UTF-8 bytes).
189pub fn character_string_len(value: &str) -> Result<u32, Error> {
190    u32::try_from(value.len())
191        .ok()
192        .and_then(|n| n.checked_add(1))
193        .ok_or_else(|| Error::Encoding("CharacterString too long for BACnet encoding".into()))
194}
195
196/// Decode a character string from content bytes.
197///
198/// The first byte is the charset identifier. Supported charsets:
199/// - 0 (UTF-8)
200/// - 4 (UCS-2, big-endian)
201/// - 5 (ISO-8859-1)
202///
203/// Other charsets return an error.
204pub fn decode_character_string(data: &[u8]) -> Result<String, Error> {
205    if data.is_empty() {
206        return Err(Error::Decoding {
207            offset: 0,
208            message: "CharacterString requires at least 1 byte for charset".into(),
209        });
210    }
211    let charset_id = data[0];
212    let payload = &data[1..];
213    match charset_id {
214        charset::UTF8 => String::from_utf8(payload.to_vec()).map_err(|e| Error::Decoding {
215            offset: 1,
216            message: format!("invalid UTF-8: {e}"),
217        }),
218        charset::UCS2 => {
219            if !payload.len().is_multiple_of(2) {
220                return Err(Error::Decoding {
221                    offset: 1,
222                    message: "UCS-2 data must have even length".into(),
223                });
224            }
225            let mut s = String::new();
226            for (i, chunk) in payload.chunks_exact(2).enumerate() {
227                let code_point = u16::from_be_bytes([chunk[0], chunk[1]]);
228                if let Some(c) = char::from_u32(code_point as u32) {
229                    s.push(c);
230                } else {
231                    return Err(Error::Decoding {
232                        offset: 1 + i * 2,
233                        message: "invalid UCS-2 code point".into(),
234                    });
235                }
236            }
237            Ok(s)
238        }
239        charset::ISO_8859_1 => Ok(payload.iter().map(|&b| b as char).collect()),
240        charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 | charset::UCS4 => Err(Error::Decoding {
241            offset: 0,
242            message: format!("unsupported charset: {charset_id}"),
243        }),
244        other => Err(Error::Decoding {
245            offset: 0,
246            message: format!("unknown charset: {other}"),
247        }),
248    }
249}
250
251// --- Bit String ---
252
253/// Encode a bit string: leading unused-bits count followed by data bytes.
254pub fn encode_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
255    buf.put_u8(unused_bits);
256    buf.put_slice(data);
257}
258
259/// Decode a bit string from content bytes.
260///
261/// Returns `(unused_bits, data)`.
262pub fn decode_bit_string(data: &[u8]) -> Result<(u8, Vec<u8>), Error> {
263    if data.is_empty() {
264        return Err(Error::Decoding {
265            offset: 0,
266            message: "BitString requires at least 1 byte for unused-bits count".into(),
267        });
268    }
269    let unused = data[0];
270    if unused > 7 {
271        return Err(Error::Decoding {
272            offset: 0,
273            message: format!("BitString unused_bits must be 0-7, got {unused}"),
274        });
275    }
276    Ok((unused, data[1..].to_vec()))
277}
278
279// ===========================================================================
280// Application-tagged encode helpers
281// ===========================================================================
282
283/// Encode an application-tagged Null.
284pub fn encode_app_null(buf: &mut BytesMut) {
285    tags::encode_tag(buf, app_tag::NULL, TagClass::Application, 0);
286}
287
288/// Encode an application-tagged Boolean.
289///
290/// The value is encoded in the tag's L/V/T bits with no content octets.
291pub fn encode_app_boolean(buf: &mut BytesMut, value: bool) {
292    tags::encode_tag(
293        buf,
294        app_tag::BOOLEAN,
295        TagClass::Application,
296        if value { 1 } else { 0 },
297    );
298}
299
300/// Encode an application-tagged Unsigned.
301pub fn encode_app_unsigned(buf: &mut BytesMut, value: u64) {
302    let len = unsigned_len(value);
303    tags::encode_tag(buf, app_tag::UNSIGNED, TagClass::Application, len);
304    encode_unsigned(buf, value);
305}
306
307/// Encode an application-tagged Signed.
308pub fn encode_app_signed(buf: &mut BytesMut, value: i32) {
309    let len = signed_len(value);
310    tags::encode_tag(buf, app_tag::SIGNED, TagClass::Application, len);
311    encode_signed(buf, value);
312}
313
314/// Encode an application-tagged Real (f32).
315pub fn encode_app_real(buf: &mut BytesMut, value: f32) {
316    tags::encode_tag(buf, app_tag::REAL, TagClass::Application, 4);
317    encode_real(buf, value);
318}
319
320/// Encode an application-tagged Double (f64).
321pub fn encode_app_double(buf: &mut BytesMut, value: f64) {
322    tags::encode_tag(buf, app_tag::DOUBLE, TagClass::Application, 8);
323    encode_double(buf, value);
324}
325
326/// Encode an application-tagged OctetString.
327pub fn encode_app_octet_string(buf: &mut BytesMut, data: &[u8]) {
328    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
329    tags::encode_tag(buf, app_tag::OCTET_STRING, TagClass::Application, data_len);
330    buf.put_slice(data);
331}
332
333/// Encode an application-tagged CharacterString (UTF-8).
334pub fn encode_app_character_string(buf: &mut BytesMut, value: &str) -> Result<(), Error> {
335    let len = character_string_len(value)?;
336    tags::encode_tag(buf, app_tag::CHARACTER_STRING, TagClass::Application, len);
337    encode_character_string(buf, value);
338    Ok(())
339}
340
341/// Encode an application-tagged BitString.
342pub fn encode_app_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
343    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
344    let len = data_len.saturating_add(1);
345    tags::encode_tag(buf, app_tag::BIT_STRING, TagClass::Application, len);
346    encode_bit_string(buf, unused_bits, data);
347}
348
349/// Encode an application-tagged Enumerated.
350pub fn encode_app_enumerated(buf: &mut BytesMut, value: u32) {
351    let len = unsigned_len(value as u64);
352    tags::encode_tag(buf, app_tag::ENUMERATED, TagClass::Application, len);
353    encode_unsigned(buf, value as u64);
354}
355
356/// Encode an application-tagged Date.
357pub fn encode_app_date(buf: &mut BytesMut, date: &Date) {
358    tags::encode_tag(buf, app_tag::DATE, TagClass::Application, 4);
359    buf.put_slice(&date.encode());
360}
361
362/// Encode an application-tagged Time.
363pub fn encode_app_time(buf: &mut BytesMut, time: &Time) {
364    tags::encode_tag(buf, app_tag::TIME, TagClass::Application, 4);
365    buf.put_slice(&time.encode());
366}
367
368/// Encode an application-tagged ObjectIdentifier.
369pub fn encode_app_object_id(buf: &mut BytesMut, oid: &ObjectIdentifier) {
370    tags::encode_tag(buf, app_tag::OBJECT_IDENTIFIER, TagClass::Application, 4);
371    buf.put_slice(&oid.encode());
372}
373
374// ===========================================================================
375// Context-tagged encode helpers
376// ===========================================================================
377
378/// Encode a context-tagged Unsigned.
379pub fn encode_ctx_unsigned(buf: &mut BytesMut, tag: u8, value: u64) {
380    let len = unsigned_len(value);
381    tags::encode_tag(buf, tag, TagClass::Context, len);
382    encode_unsigned(buf, value);
383}
384
385/// Encode a context-tagged Signed.
386pub fn encode_ctx_signed(buf: &mut BytesMut, tag: u8, value: i32) {
387    let len = signed_len(value);
388    tags::encode_tag(buf, tag, TagClass::Context, len);
389    encode_signed(buf, value);
390}
391
392/// Encode a context-tagged Real (f32).
393pub fn encode_ctx_real(buf: &mut BytesMut, tag: u8, value: f32) {
394    tags::encode_tag(buf, tag, TagClass::Context, 4);
395    encode_real(buf, value);
396}
397
398/// Encode a context-tagged Double (f64).
399pub fn encode_ctx_double(buf: &mut BytesMut, tag: u8, value: f64) {
400    tags::encode_tag(buf, tag, TagClass::Context, 8);
401    encode_double(buf, value);
402}
403
404/// Encode a context-tagged Enumerated.
405pub fn encode_ctx_enumerated(buf: &mut BytesMut, tag: u8, value: u32) {
406    let len = unsigned_len(value as u64);
407    tags::encode_tag(buf, tag, TagClass::Context, len);
408    encode_unsigned(buf, value as u64);
409}
410
411/// Encode a context-tagged Boolean.
412///
413/// Context-tagged booleans use a 1-byte content octet (unlike application-tagged).
414pub fn encode_ctx_boolean(buf: &mut BytesMut, tag: u8, value: bool) {
415    tags::encode_tag(buf, tag, TagClass::Context, 1);
416    buf.put_u8(if value { 1 } else { 0 });
417}
418
419/// Encode a context-tagged ObjectIdentifier.
420pub fn encode_ctx_object_id(buf: &mut BytesMut, tag: u8, oid: &ObjectIdentifier) {
421    tags::encode_tag(buf, tag, TagClass::Context, 4);
422    buf.put_slice(&oid.encode());
423}
424
425/// Encode a context-tagged OctetString.
426pub fn encode_ctx_octet_string(buf: &mut BytesMut, tag: u8, data: &[u8]) {
427    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
428    tags::encode_tag(buf, tag, TagClass::Context, data_len);
429    buf.put_slice(data);
430}
431
432/// Encode a context-tagged CharacterString (UTF-8).
433pub fn encode_ctx_character_string(buf: &mut BytesMut, tag: u8, value: &str) -> Result<(), Error> {
434    let len = character_string_len(value)?;
435    tags::encode_tag(buf, tag, TagClass::Context, len);
436    encode_character_string(buf, value);
437    Ok(())
438}
439
440/// Encode a context-tagged Date.
441pub fn encode_ctx_date(buf: &mut BytesMut, tag: u8, date: &Date) {
442    tags::encode_tag(buf, tag, TagClass::Context, 4);
443    buf.put_slice(&date.encode());
444}
445
446/// Encode a context-tagged BitString.
447pub fn encode_ctx_bit_string(buf: &mut BytesMut, tag: u8, unused_bits: u8, data: &[u8]) {
448    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
449    let len = data_len.saturating_add(1);
450    tags::encode_tag(buf, tag, TagClass::Context, len);
451    encode_bit_string(buf, unused_bits, data);
452}
453
454// ===========================================================================
455// Application-tagged decode (dispatches by tag number)
456// ===========================================================================
457
458/// Decode a single application-tagged value from `data` at `offset`.
459///
460/// A leading **context-tagged** element decodes to
461/// [`PropertyValue::ApplicationData`] holding the complete tagged element
462/// verbatim (tag header(s) through the matching closing tag for constructed
463/// elements) — such bytes belong to a context-tagged ASN.1 production
464/// (e.g. `BACnetEventParameter`) whose property-level framed codec must
465/// interpret them. Application-tagged content is decoded into the typed
466/// variants as before.
467///
468/// Returns the decoded `PropertyValue` and the new offset past the consumed bytes.
469pub fn decode_application_value(
470    data: &[u8],
471    offset: usize,
472) -> Result<(PropertyValue, usize), Error> {
473    let (tag, new_offset) = tags::decode_tag(data, offset)?;
474    if tag.class == TagClass::Context {
475        if tag.is_opening {
476            let (_content, after) = tags::extract_context_value(data, new_offset, tag.number)?;
477            return Ok((
478                PropertyValue::ApplicationData(data[offset..after].to_vec()),
479                after,
480            ));
481        }
482        if tag.is_closing {
483            return Err(Error::decoding(offset, "unexpected closing tag"));
484        }
485        let content_end = new_offset
486            .checked_add(tag.length as usize)
487            .ok_or_else(|| Error::decoding(new_offset, "length overflow"))?;
488        if data.len() < content_end {
489            return Err(Error::buffer_too_short(content_end, data.len()));
490        }
491        return Ok((
492            PropertyValue::ApplicationData(data[offset..content_end].to_vec()),
493            content_end,
494        ));
495    }
496    if tag.is_opening || tag.is_closing {
497        return Err(Error::decoding(offset, "unexpected opening/closing tag"));
498    }
499
500    if tag.number == app_tag::NULL && tag.length != 0 {
501        return Err(Error::decoding(
502            offset,
503            "application NULL must have no contents",
504        ));
505    }
506    if tag.number == app_tag::BOOLEAN && tag.length > 1 {
507        return Err(Error::decoding(
508            offset,
509            "application BOOLEAN L/V/T must be 0 or 1",
510        ));
511    }
512
513    let content_start = new_offset;
514    let content_len = tag.length as usize;
515    let content_end = content_start
516        .checked_add(content_len)
517        .ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
518
519    if tag.number == app_tag::BOOLEAN {
520        return Ok((PropertyValue::Boolean(tag.length != 0), content_start));
521    }
522
523    if data.len() < content_end {
524        return Err(Error::buffer_too_short(content_end, data.len()));
525    }
526
527    let content = &data[content_start..content_end];
528
529    let value = match tag.number {
530        app_tag::NULL => PropertyValue::Null,
531        app_tag::UNSIGNED => PropertyValue::Unsigned(decode_unsigned(content)?),
532        app_tag::SIGNED => PropertyValue::Signed(decode_signed(content)?),
533        app_tag::REAL => PropertyValue::Real(decode_real(content)?),
534        app_tag::DOUBLE => PropertyValue::Double(decode_double(content)?),
535        app_tag::OCTET_STRING => PropertyValue::OctetString(content.to_vec()),
536        app_tag::CHARACTER_STRING => {
537            PropertyValue::CharacterString(decode_character_string(content)?)
538        }
539        app_tag::BIT_STRING => {
540            let (unused, bits) = decode_bit_string(content)?;
541            PropertyValue::BitString {
542                unused_bits: unused,
543                data: bits,
544            }
545        }
546        app_tag::ENUMERATED => {
547            let value = u32::try_from(decode_unsigned(content)?)
548                .map_err(|_| Error::decoding(content_start, "ENUMERATED exceeds u32"))?;
549            PropertyValue::Enumerated(value)
550        }
551        app_tag::DATE => PropertyValue::Date(Date::decode(content)?),
552        app_tag::TIME => PropertyValue::Time(Time::decode(content)?),
553        app_tag::OBJECT_IDENTIFIER => {
554            PropertyValue::ObjectIdentifier(ObjectIdentifier::decode(content)?)
555        }
556        other => {
557            return Err(Error::decoding(
558                offset,
559                format!("unknown application tag number {other}"),
560            ));
561        }
562    };
563
564    Ok((value, content_end))
565}
566
567pub(crate) fn validate_application_value(data: &[u8], offset: usize) -> Result<usize, Error> {
568    let (tag, content_start) = tags::decode_tag(data, offset)?;
569    if tag.class != TagClass::Application || tag.is_opening || tag.is_closing {
570        return Err(Error::decoding(
571            offset,
572            "expected an application-tagged value",
573        ));
574    }
575    if tag.number != app_tag::CHARACTER_STRING && tag.number != app_tag::SIGNED {
576        return decode_application_value(data, offset).map(|(_, end)| end);
577    }
578
579    let content_end = content_start
580        .checked_add(tag.length as usize)
581        .ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
582    if content_end > data.len() {
583        return Err(Error::buffer_too_short(content_end, data.len()));
584    }
585    let content = &data[content_start..content_end];
586    if tag.number == app_tag::SIGNED {
587        decode_signed_canonical(content)?;
588        return Ok(content_end);
589    }
590    let Some((&charset_id, payload)) = content.split_first() else {
591        return Err(Error::decoding(
592            content_start,
593            "CharacterString requires a character-set octet",
594        ));
595    };
596    match charset_id {
597        charset::UTF8 | charset::UCS2 | charset::ISO_8859_1 => {
598            decode_character_string(content)?;
599        }
600        charset::IBM_MICROSOFT_DBCS if payload.len() < 2 => {
601            return Err(Error::decoding(
602                content_start + 1,
603                "DBCS CharacterString requires a two-octet code page",
604            ));
605        }
606        charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 => {}
607        charset::UCS4 => {
608            if !payload.len().is_multiple_of(4) {
609                return Err(Error::decoding(
610                    content_start + 1,
611                    "UCS-4 CharacterString length must be a multiple of four",
612                ));
613            }
614            for (index, encoded) in payload.chunks_exact(4).enumerate() {
615                let code_point = u32::from_be_bytes(encoded.try_into().unwrap());
616                if char::from_u32(code_point).is_none() {
617                    return Err(Error::decoding(
618                        content_start + 1 + index * 4,
619                        "invalid UCS-4 code point",
620                    ));
621                }
622            }
623        }
624        _ => {
625            return Err(Error::decoding(
626                content_start,
627                format!("reserved character set: {charset_id}"),
628            ));
629        }
630    }
631    Ok(content_end)
632}
633
634/// Encode a `PropertyValue` as an application-tagged value.
635pub fn encode_property_value(buf: &mut BytesMut, value: &PropertyValue) -> Result<(), Error> {
636    match value {
637        PropertyValue::Null => encode_app_null(buf),
638        PropertyValue::Boolean(v) => encode_app_boolean(buf, *v),
639        PropertyValue::Unsigned(v) => encode_app_unsigned(buf, *v),
640        PropertyValue::Signed(v) => encode_app_signed(buf, *v),
641        PropertyValue::Real(v) => encode_app_real(buf, *v),
642        PropertyValue::Double(v) => encode_app_double(buf, *v),
643        PropertyValue::OctetString(v) => encode_app_octet_string(buf, v),
644        PropertyValue::CharacterString(v) => encode_app_character_string(buf, v)?,
645        PropertyValue::BitString { unused_bits, data } => {
646            encode_app_bit_string(buf, *unused_bits, data)
647        }
648        PropertyValue::Enumerated(v) => encode_app_enumerated(buf, *v),
649        PropertyValue::Date(v) => encode_app_date(buf, v),
650        PropertyValue::Time(v) => encode_app_time(buf, v),
651        PropertyValue::ObjectIdentifier(v) => encode_app_object_id(buf, v),
652        PropertyValue::List(values) => {
653            for v in values {
654                encode_property_value(buf, v)?;
655            }
656        }
657        PropertyValue::ApplicationData(bytes) => {
658            // Already-encoded application-layer content (a context-tagged
659            // CHOICE/SEQUENCE production) is emitted verbatim.
660            buf.put_slice(bytes);
661        }
662    }
663    Ok(())
664}
665
666// ===========================================================================
667// BACnetTimeStamp encode/decode
668// ===========================================================================
669
670/// Largest encodable `sequence-number` value: the production constrains it to
671/// `Unsigned (0..65535)`.
672///
673/// `BACnetTimeStamp ::= CHOICE { time [0] Time, sequence-number [1] Unsigned
674/// (0..65535), datetime [2] BACnetDateTime }` (ASHRAE 135-2020 Clause 21).
675pub const MAX_TIMESTAMP_SEQUENCE_NUMBER: u64 = 65535;
676
677/// Encode a `BACnetTimeStamp` as a bare CHOICE element (no enclosing field
678/// tag).
679///
680/// Used where a production lists `BACnetTimeStamp` as a bare CHOICE item —
681/// e.g. the `eventTimeStamps [3] SEQUENCE OF BACnetTimeStamp` inside
682/// GetEventInformation-ACK (Clause 13.9). [`encode_timestamp`] wraps this in
683/// the enclosing field's context tag pair instead.
684///
685/// Tag forms per Clause 20.2.1.5: `time [0]` is the CHOICE's only alternative
686/// over a *primitive* base type, so it encodes as a context-specific
687/// *primitive* tag 0 of length 4 holding the raw `Time` octets (the
688/// opening-tag-0 wrapper around an application-tagged `Time` is NOT
689/// conformant); `sequence-number [1]` is a primitive context tag 1 (contents
690/// constrained to `0..=65535` by the public variant's `u16` payload);
691/// `datetime [2]` tags the *constructed* `BACnetDateTime`, so it encodes as opening tag 2 /
692/// application-tagged `Date` / application-tagged `Time` / closing tag 2.
693pub fn encode_timestamp_choice(buf: &mut BytesMut, ts: &BACnetTimeStamp) -> Result<(), Error> {
694    match ts {
695        BACnetTimeStamp::Time(t) => {
696            tags::encode_tag(buf, 0, TagClass::Context, 4);
697            buf.put_slice(&t.encode());
698        }
699        BACnetTimeStamp::SequenceNumber(n) => {
700            encode_ctx_unsigned(buf, 1, u64::from(*n));
701        }
702        BACnetTimeStamp::DateTime { date, time } => {
703            tags::encode_opening_tag(buf, 2);
704            encode_app_date(buf, date);
705            encode_app_time(buf, time);
706            tags::encode_closing_tag(buf, 2);
707        }
708    }
709    Ok(())
710}
711
712/// Decode a bare-CHOICE `BACnetTimeStamp` starting at `offset`.
713///
714/// Returns the decoded timestamp and the new offset past its encoding. Only
715/// the Clause 20.2.1.5-conformant tag forms described on
716/// [`encode_timestamp_choice`] are accepted; `sequence-number` contents
717/// beyond `0..=65535` are rejected.
718pub fn decode_timestamp_choice(
719    data: &[u8],
720    offset: usize,
721) -> Result<(BACnetTimeStamp, usize), Error> {
722    let (inner_tag, inner_pos) = tags::decode_tag(data, offset)?;
723
724    let (ts, after_inner) = if inner_tag.is_context(0) {
725        let end = inner_pos
726            .checked_add(inner_tag.length as usize)
727            .ok_or_else(|| Error::decoding(inner_pos, "BACnetTimeStamp Time length overflow"))?;
728        if end > data.len() {
729            return Err(Error::decoding(inner_pos, "BACnetTimeStamp Time truncated"));
730        }
731        let t = Time::decode(&data[inner_pos..end])?;
732        (BACnetTimeStamp::Time(t), end)
733    } else if inner_tag.is_context(1) {
734        let end = inner_pos
735            .checked_add(inner_tag.length as usize)
736            .ok_or_else(|| {
737                Error::decoding(inner_pos, "BACnetTimeStamp SequenceNumber length overflow")
738            })?;
739        if end > data.len() {
740            return Err(Error::decoding(
741                inner_pos,
742                "BACnetTimeStamp SequenceNumber truncated",
743            ));
744        }
745        let n = decode_unsigned(&data[inner_pos..end])?;
746        if n > MAX_TIMESTAMP_SEQUENCE_NUMBER {
747            return Err(Error::decoding(
748                inner_pos,
749                format!("BACnetTimeStamp sequence-number {n} exceeds 65535"),
750            ));
751        }
752        (
753            BACnetTimeStamp::SequenceNumber(
754                u16::try_from(n).expect("BACnetTimeStamp sequence range checked above"),
755            ),
756            end,
757        )
758    } else if inner_tag.is_opening_tag(2) {
759        let (date_tag, date_pos) = tags::decode_tag(data, inner_pos)?;
760        if date_tag.class != TagClass::Application || date_tag.number != app_tag::DATE {
761            return Err(Error::decoding(
762                inner_pos,
763                "BACnetTimeStamp DateTime expected Date",
764            ));
765        }
766        let date_end = date_pos
767            .checked_add(date_tag.length as usize)
768            .ok_or_else(|| {
769                Error::decoding(date_pos, "BACnetTimeStamp DateTime Date length overflow")
770            })?;
771        if date_end > data.len() {
772            return Err(Error::decoding(
773                date_pos,
774                "BACnetTimeStamp DateTime Date truncated",
775            ));
776        }
777        let date = Date::decode(&data[date_pos..date_end])?;
778
779        let (time_tag, time_pos) = tags::decode_tag(data, date_end)?;
780        if time_tag.class != TagClass::Application || time_tag.number != app_tag::TIME {
781            return Err(Error::decoding(
782                date_end,
783                "BACnetTimeStamp DateTime expected Time",
784            ));
785        }
786        let time_end = time_pos
787            .checked_add(time_tag.length as usize)
788            .ok_or_else(|| {
789                Error::decoding(time_pos, "BACnetTimeStamp DateTime Time length overflow")
790            })?;
791        if time_end > data.len() {
792            return Err(Error::decoding(
793                time_pos,
794                "BACnetTimeStamp DateTime Time truncated",
795            ));
796        }
797        let time = Time::decode(&data[time_pos..time_end])?;
798
799        let (close_tag, close_pos) = tags::decode_tag(data, time_end)?;
800        if !close_tag.is_closing_tag(2) {
801            return Err(Error::decoding(
802                time_end,
803                "BACnetTimeStamp DateTime missing closing tag 2",
804            ));
805        }
806        (BACnetTimeStamp::DateTime { date, time }, close_pos)
807    } else {
808        return Err(Error::decoding(
809            offset,
810            "BACnetTimeStamp: unexpected inner choice tag",
811        ));
812    };
813
814    Ok((ts, after_inner))
815}
816
817/// Encode a BACnetTimeStamp wrapped in a context opening/closing tag pair.
818///
819/// The outer `tag_number` is the context tag of the enclosing field.
820/// Inside, the CHOICE variant uses its own context tag (0=Time,
821/// 1=SequenceNumber, 2=DateTime) via [`encode_timestamp_choice`].
822pub fn encode_timestamp(
823    buf: &mut BytesMut,
824    tag_number: u8,
825    ts: &BACnetTimeStamp,
826) -> Result<(), Error> {
827    tags::encode_opening_tag(buf, tag_number);
828    encode_timestamp_choice(buf, ts)?;
829    tags::encode_closing_tag(buf, tag_number);
830    Ok(())
831}
832
833/// Decode a BACnetTimeStamp from inside a context opening/closing tag pair.
834///
835/// `data` should point to the start of the outer opening tag for `tag_number`.
836/// Returns the decoded timestamp and the new offset past the outer closing tag.
837pub fn decode_timestamp(
838    data: &[u8],
839    offset: usize,
840    tag_number: u8,
841) -> Result<(BACnetTimeStamp, usize), Error> {
842    let (tag, pos) = tags::decode_tag(data, offset)?;
843    if !tag.is_opening_tag(tag_number) {
844        return Err(Error::decoding(
845            offset,
846            format!("expected opening tag {tag_number} for BACnetTimeStamp"),
847        ));
848    }
849
850    let (ts, after_inner) = decode_timestamp_choice(data, pos)?;
851
852    let (close, final_pos) = tags::decode_tag(data, after_inner)?;
853    if !close.is_closing_tag(tag_number) {
854        return Err(Error::decoding(
855            after_inner,
856            format!("expected closing tag {tag_number} for BACnetTimeStamp"),
857        ));
858    }
859
860    Ok((ts, final_pos))
861}
862
863// ===========================================================================
864// Tests
865// ===========================================================================
866
867#[cfg(test)]
868mod tests;