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//!
6//! The bounded scalar encoders also have `_to_sink` entrypoints accepting
7//! [`crate::sink::EncodeSink`]. Each appends its complete value, including any
8//! tag header, in one atomic write and returns the appended byte count. Known
9//! capacity shortages never call the sink; a custom sink's error is unchanged.
10//! The scalar staging buffer is at most eleven bytes. These functions preserve
11//! the existing encoders' input semantics and do not add value validation.
12//!
13//! [`encode_timestamp_choice_to_sink`] and [`encode_timestamp_to_sink`] extend
14//! the same one-write contract to complete timestamps, with twelve-byte and
15//! sixteen-byte staging bounds respectively. These bounds describe encoded
16//! output, not the full call stack.
17//!
18//! The CharacterString, OCTET STRING and BIT STRING `_to_sink` encoders write
19//! a header and a caller's payload as one value with
20//! [`EncodeSink::write_parts`](crate::sink::EncodeSink::write_parts).
21//! [`CharacterStringRef`] and [`ApplicationValueRef`] read strings and
22//! application-tagged values without copying them. Those, the scalar
23//! decoders and the sink encoders need no allocator; the `BytesMut` encoders
24//! and the owned `PropertyValue` decoders need `alloc`.
25
26#[cfg(all(feature = "alloc", not(feature = "std")))]
27use alloc::{string::String, vec::Vec};
28
29use bacnet_types::error::{DecodeError, DecodingKind, Error};
30#[cfg(feature = "alloc")]
31use bacnet_types::primitives::PropertyValue;
32use bacnet_types::primitives::{BACnetTimeStamp, Date, Time};
33#[cfg(feature = "alloc")]
34use bytes::{BufMut, BytesMut};
35
36#[cfg(feature = "alloc")]
37use crate::constructed::tagged::misplaced_tag;
38use crate::constructed::tagged::{misplaced_kind, unclosed_kind};
39#[cfg(feature = "alloc")]
40use crate::tags::TagClass;
41use crate::tags::{self, app_tag};
42
43mod priority_value;
44mod scalar;
45mod strings;
46mod timestamp;
47mod view;
48
49#[cfg(feature = "alloc")]
50pub use priority_value::encode_priority_date_time;
51pub use priority_value::{PRIORITY_DATE_TIME_TAG, decode_priority_date_time};
52pub use strings::{
53    encode_app_bit_string_to_sink, encode_app_character_string_to_sink,
54    encode_app_octet_string_to_sink, encode_bit_string_to_sink, encode_character_string_to_sink,
55    encode_ctx_bit_string_to_sink, encode_ctx_character_string_to_sink,
56    encode_ctx_octet_string_to_sink,
57};
58pub use timestamp::{encode_timestamp_choice_to_sink, encode_timestamp_to_sink};
59pub use view::{ApplicationValueRef, CharacterStringRef, decode_application_value_ref};
60
61#[cfg(feature = "alloc")]
62pub use scalar::{
63    encode_app_boolean, encode_app_date, encode_app_double, encode_app_enumerated, encode_app_null,
64    encode_app_object_id, encode_app_real, encode_app_signed, encode_app_time, encode_app_unsigned,
65    encode_ctx_boolean, encode_ctx_date, encode_ctx_double, encode_ctx_enumerated,
66    encode_ctx_object_id, encode_ctx_real, encode_ctx_signed, encode_ctx_unsigned, encode_double,
67    encode_real, encode_signed, encode_unsigned,
68};
69pub use scalar::{
70    encode_app_boolean_to_sink, encode_app_date_to_sink, encode_app_double_to_sink,
71    encode_app_enumerated_to_sink, encode_app_null_to_sink, encode_app_object_id_to_sink,
72    encode_app_real_to_sink, encode_app_signed_to_sink, encode_app_time_to_sink,
73    encode_app_unsigned_to_sink, encode_ctx_boolean_to_sink, encode_ctx_date_to_sink,
74    encode_ctx_double_to_sink, encode_ctx_enumerated_to_sink, encode_ctx_object_id_to_sink,
75    encode_ctx_real_to_sink, encode_ctx_signed_to_sink, encode_ctx_unsigned_to_sink,
76    encode_double_to_sink, encode_real_to_sink, encode_signed_to_sink, encode_unsigned_to_sink,
77    signed_len, unsigned_len,
78};
79
80/// A fault in contents decoded at offset 0 of the slice they were given,
81/// naming `value`.
82#[cold]
83fn contents_fault(kind: DecodingKind, label: &'static str, value: u64) -> Error {
84    DecodeError::new(kind, 0, label).with_value(value).into()
85}
86
87// ===========================================================================
88// Raw value codecs (no tag header)
89// ===========================================================================
90
91// --- Unsigned Integer ---
92
93/// Decode an unsigned integer from big-endian bytes (1-8 bytes).
94pub fn decode_unsigned(data: &[u8]) -> Result<u64, Error> {
95    if data.is_empty() || data.len() > 8 {
96        return Err(contents_fault(
97            DecodingKind::InvalidEncoding,
98            "unsigned requires 1-8 bytes, got {}",
99            data.len() as u64,
100        ));
101    }
102    let mut value: u64 = 0;
103    for &b in data {
104        value = (value << 8) | b as u64;
105    }
106    Ok(value)
107}
108
109/// Decode an unsigned integer that fits in a `u8` from 1-8 big-endian bytes.
110/// Leading zero bytes are accepted; values above `u8::MAX` are rejected.
111pub fn decode_unsigned_u8(data: &[u8]) -> Result<u8, Error> {
112    let value = decode_unsigned(data)?;
113    u8::try_from(value).map_err(|_| {
114        contents_fault(
115            DecodingKind::OutOfRange,
116            "unsigned value {} exceeds u8",
117            value,
118        )
119    })
120}
121
122/// Decode an unsigned integer that fits in a `u16` from 1-8 big-endian bytes.
123/// Leading zero bytes are accepted; values above `u16::MAX` are rejected.
124pub fn decode_unsigned_u16(data: &[u8]) -> Result<u16, Error> {
125    let value = decode_unsigned(data)?;
126    u16::try_from(value).map_err(|_| {
127        contents_fault(
128            DecodingKind::OutOfRange,
129            "unsigned value {} exceeds u16",
130            value,
131        )
132    })
133}
134
135/// Decode an unsigned integer that fits in a `u32` from 1-8 big-endian bytes.
136/// Leading zero bytes are accepted; values above `u32::MAX` are rejected.
137pub fn decode_unsigned_u32(data: &[u8]) -> Result<u32, Error> {
138    let value = decode_unsigned(data)?;
139    u32::try_from(value).map_err(|_| {
140        contents_fault(
141            DecodingKind::OutOfRange,
142            "unsigned value {} exceeds u32",
143            value,
144        )
145    })
146}
147
148// --- Signed Integer ---
149
150/// Decode a signed integer from two's-complement big-endian bytes (1-4 bytes).
151pub fn decode_signed(data: &[u8]) -> Result<i32, Error> {
152    if data.is_empty() || data.len() > 4 {
153        return Err(contents_fault(
154            DecodingKind::InvalidEncoding,
155            "signed requires 1-4 bytes, got {}",
156            data.len() as u64,
157        ));
158    }
159    let sign_extend = if data[0] & 0x80 != 0 { 0xFF } else { 0x00 };
160    let mut bytes = [sign_extend; 4];
161    bytes[4 - data.len()..].copy_from_slice(data);
162    Ok(i32::from_be_bytes(bytes))
163}
164
165#[cfg(feature = "alloc")]
166pub(crate) fn decode_signed_canonical(data: &[u8]) -> Result<i32, Error> {
167    if data.len() > 1
168        && ((data[0] == 0 && data[1] & 0x80 == 0) || (data[0] == 0xFF && data[1] & 0x80 != 0))
169    {
170        return Err(Error::decoding(0, "signed contains a redundant sign octet"));
171    }
172    decode_signed(data)
173}
174
175// --- Real ---
176
177/// Decode an IEEE-754 single-precision float from 4 big-endian bytes.
178pub fn decode_real(data: &[u8]) -> Result<f32, Error> {
179    if data.len() != 4 {
180        return Err(contents_fault(
181            DecodingKind::InvalidEncoding,
182            "Real expects exactly 4 bytes, got {}",
183            data.len() as u64,
184        ));
185    }
186    Ok(f32::from_be_bytes([data[0], data[1], data[2], data[3]]))
187}
188
189// --- Double ---
190
191/// Decode an IEEE-754 double-precision float from 8 big-endian bytes.
192pub fn decode_double(data: &[u8]) -> Result<f64, Error> {
193    if data.len() != 8 {
194        return Err(contents_fault(
195            DecodingKind::InvalidEncoding,
196            "Double expects exactly 8 bytes, got {}",
197            data.len() as u64,
198        ));
199    }
200    let bytes: [u8; 8] = [
201        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
202    ];
203    Ok(f64::from_be_bytes(bytes))
204}
205
206// --- Character String ---
207
208/// Character set identifiers.
209pub mod charset {
210    /// ISO 10646 (UTF-8) — X'00'
211    pub const UTF8: u8 = 0;
212    /// IBM/Microsoft DBCS — X'01'
213    pub const IBM_MICROSOFT_DBCS: u8 = 1;
214    /// JIS X 0208 — X'02'
215    pub const JIS_X_0208: u8 = 2;
216    /// ISO 10646 (UCS-4) — X'03'
217    pub const UCS4: u8 = 3;
218    /// ISO 10646 (UCS-2) — X'04'
219    pub const UCS2: u8 = 4;
220    /// ISO 8859-1 — X'05'
221    pub const ISO_8859_1: u8 = 5;
222}
223
224/// Encode a UTF-8 character string with leading charset byte. Delegates to
225/// [`encode_character_string_to_sink`].
226#[cfg(feature = "alloc")]
227pub fn encode_character_string(buf: &mut BytesMut, value: &str) {
228    encode_character_string_to_sink(buf, value).expect("the BytesMut sink cannot return SinkFull");
229}
230
231/// Return the encoded length of a character string (charset byte + UTF-8
232/// bytes). Only with `alloc`: the sink encoders refuse an over-long string
233/// with [`SinkFull`](crate::sink::SinkFull) instead.
234#[cfg(feature = "alloc")]
235pub fn character_string_len(value: &str) -> Result<u32, Error> {
236    u32::try_from(value.len())
237        .ok()
238        .and_then(|n| n.checked_add(1))
239        .ok_or_else(character_string_too_long)
240}
241
242/// A CharacterString whose contents a header's 32-bit length can't express.
243#[cfg(feature = "alloc")]
244#[cold]
245fn character_string_too_long() -> Error {
246    Error::encode("CharacterString too long for BACnet encoding")
247}
248
249/// Decode a character string from content bytes.
250///
251/// The first byte is the charset identifier. Supported charsets:
252/// - 0 (UTF-8)
253/// - 4 (UCS-2, big-endian)
254/// - 5 (ISO-8859-1)
255///
256/// Other charsets return an error. Only with `alloc`;
257/// [`CharacterStringRef::decode`] reads the contents without converting
258/// them.
259#[cfg(feature = "alloc")]
260pub fn decode_character_string(data: &[u8]) -> Result<String, Error> {
261    // Character sets the standard defines but this decoder doesn't convert.
262    if let Some(
263        &unsupported @ (charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 | charset::UCS4),
264    ) = data.first()
265    {
266        return Err(contents_fault(
267            DecodingKind::Unsupported,
268            "unsupported charset: {}",
269            unsupported.into(),
270        ));
271    }
272    // The structure is checked as the borrowed view checks it (#1778), so
273    // the conversions below can't fail.
274    view::check_character_string(data)?;
275    let payload = &data[1..];
276    Ok(match data[0] {
277        charset::UCS2 => payload
278            .as_chunks::<2>()
279            .0
280            .iter()
281            .filter_map(|c| char::from_u32(u16::from_be_bytes(*c).into()))
282            .collect(),
283        charset::ISO_8859_1 => payload.iter().map(|&octet| char::from(octet)).collect(),
284        _ => core::str::from_utf8(payload).unwrap_or_default().into(),
285    })
286}
287
288/// UTF-8 contents that aren't valid, reported at the first contents octet
289/// after the character set, naming how many octets before the fault were
290/// valid.
291#[cold]
292fn invalid_utf8(error: core::str::Utf8Error) -> Error {
293    DecodeError::new(
294        DecodingKind::InvalidEncoding,
295        1,
296        "invalid UTF-8 after {} valid octets",
297    )
298    .with_value(error.valid_up_to() as u64)
299    .into()
300}
301
302/// A character set Clause 20.2.9 doesn't define.
303#[cold]
304fn unknown_charset(charset: u8) -> Error {
305    contents_fault(
306        DecodingKind::InvalidEncoding,
307        "unknown charset: {}",
308        charset.into(),
309    )
310}
311
312// --- Bit String ---
313
314/// Encode a bit string: leading unused-bits count followed by data bytes.
315/// Delegates to [`encode_bit_string_to_sink`].
316#[cfg(feature = "alloc")]
317pub fn encode_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
318    encode_bit_string_to_sink(buf, unused_bits, data)
319        .expect("the BytesMut sink cannot return SinkFull");
320}
321
322/// Decode a bit string from content bytes.
323///
324/// Returns `(unused_bits, data)`. Only with `alloc`; [`decode_bit_string_ref`]
325/// borrows the data instead.
326#[cfg(feature = "alloc")]
327pub fn decode_bit_string(data: &[u8]) -> Result<(u8, Vec<u8>), Error> {
328    let (unused, bits) = decode_bit_string_ref(data)?;
329    Ok((unused, bits.to_vec()))
330}
331
332/// Decode a bit string from content bytes without copying its data octets.
333///
334/// Returns `(unused_bits, data)`, `data` borrowed from the contents.
335pub fn decode_bit_string_ref(data: &[u8]) -> Result<(u8, &[u8]), Error> {
336    let Some((&unused, bits)) = data.split_first() else {
337        return Err(Error::decoding(
338            0,
339            "BitString requires at least 1 byte for unused-bits count",
340        ));
341    };
342    if unused > 7 {
343        return Err(contents_fault(
344            DecodingKind::InvalidEncoding,
345            "BitString unused_bits must be 0-7, got {}",
346            unused.into(),
347        ));
348    }
349    Ok((unused, bits))
350}
351
352// ===========================================================================
353// Application-tagged encode helpers
354// ===========================================================================
355
356/// Encode an application-tagged OctetString. It keeps its own code rather
357/// than delegating to [`encode_app_octet_string_to_sink`]: past `u32::MAX`
358/// octets it saturates the length, where the sink encoder refuses.
359#[cfg(feature = "alloc")]
360pub fn encode_app_octet_string(buf: &mut BytesMut, data: &[u8]) {
361    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
362    tags::encode_tag(buf, app_tag::OCTET_STRING, TagClass::Application, data_len);
363    buf.put_slice(data);
364}
365
366/// Encode an application-tagged CharacterString (UTF-8). Delegates to
367/// [`encode_app_character_string_to_sink`]; a string too long for a header's
368/// 32-bit length is [`Error::Encode`], with nothing written.
369#[cfg(feature = "alloc")]
370pub fn encode_app_character_string(buf: &mut BytesMut, value: &str) -> Result<(), Error> {
371    // A BytesMut never refuses a write, so the only refusal is the length.
372    encode_app_character_string_to_sink(buf, value).map_err(|_| character_string_too_long())?;
373    Ok(())
374}
375
376/// Encode an application-tagged BitString. It keeps its own code rather than
377/// delegating to [`encode_app_bit_string_to_sink`]: past `u32::MAX` octets it
378/// saturates the length, where the sink encoder refuses.
379#[cfg(feature = "alloc")]
380pub fn encode_app_bit_string(buf: &mut BytesMut, unused_bits: u8, data: &[u8]) {
381    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
382    let len = data_len.saturating_add(1);
383    tags::encode_tag(buf, app_tag::BIT_STRING, TagClass::Application, len);
384    encode_bit_string(buf, unused_bits, data);
385}
386
387// ===========================================================================
388// Context-tagged encode helpers
389// ===========================================================================
390
391/// Encode a context-tagged OctetString. It keeps its own code rather than
392/// delegating to [`encode_ctx_octet_string_to_sink`]: past `u32::MAX` octets
393/// it saturates the length, where the sink encoder refuses.
394#[cfg(feature = "alloc")]
395pub fn encode_ctx_octet_string(buf: &mut BytesMut, tag: u8, data: &[u8]) {
396    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
397    tags::encode_tag(buf, tag, TagClass::Context, data_len);
398    buf.put_slice(data);
399}
400
401/// Encode a context-tagged CharacterString (UTF-8). Delegates to
402/// [`encode_ctx_character_string_to_sink`]; a string too long for a header's
403/// 32-bit length is [`Error::Encode`], with nothing written.
404#[cfg(feature = "alloc")]
405pub fn encode_ctx_character_string(buf: &mut BytesMut, tag: u8, value: &str) -> Result<(), Error> {
406    // A BytesMut never refuses a write, so the only refusal is the length.
407    encode_ctx_character_string_to_sink(buf, tag, value)
408        .map_err(|_| character_string_too_long())?;
409    Ok(())
410}
411
412/// Encode a context-tagged BitString. It keeps its own code rather than
413/// delegating to [`encode_ctx_bit_string_to_sink`]: past `u32::MAX` octets it
414/// saturates the length, where the sink encoder refuses.
415#[cfg(feature = "alloc")]
416pub fn encode_ctx_bit_string(buf: &mut BytesMut, tag: u8, unused_bits: u8, data: &[u8]) {
417    let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
418    let len = data_len.saturating_add(1);
419    tags::encode_tag(buf, tag, TagClass::Context, len);
420    encode_bit_string(buf, unused_bits, data);
421}
422
423// ===========================================================================
424// Application-tagged decode (dispatches by tag number)
425// ===========================================================================
426
427/// The label for an application tag of a fixed-size type whose header gives
428/// another length, or `None` for a tag of no fixed size.
429pub(crate) fn fixed_length_label(number: u8) -> Option<(&'static str, u32)> {
430    Some(match number {
431        app_tag::REAL => ("application REAL has {} contents octets, expected 4", 4),
432        app_tag::DOUBLE => ("application Double has {} contents octets, expected 8", 8),
433        app_tag::DATE => ("application Date has {} contents octets, expected 4", 4),
434        app_tag::TIME => ("application Time has {} contents octets, expected 4", 4),
435        app_tag::OBJECT_IDENTIFIER => (
436            "application BACnetObjectIdentifier has {} contents octets, expected 4",
437            4,
438        ),
439        _ => return None,
440    })
441}
442
443/// Decode a single application-tagged value from `data` at `offset`.
444///
445/// A leading **context-tagged** element decodes to
446/// [`PropertyValue::ApplicationData`] holding the complete tagged element
447/// verbatim (tag header(s) through the matching closing tag for constructed
448/// elements) — such bytes belong to a context-tagged ASN.1 production
449/// (e.g. `BACnetEventParameter`) whose property-level framed codec must
450/// interpret them. Application-tagged content is decoded into the typed
451/// variants as before.
452///
453/// Errors follow the rule of
454/// [`constructed::tagged`](crate::constructed::tagged): a fixed-size value
455/// (REAL, Double, Date, Time, object identifier) whose header gives another
456/// length is [`Error::Decoding`], and contents that run past the end of
457/// `data` are [`Error::BufferTooShort`].
458///
459/// Returns the decoded `PropertyValue` and the new offset past the consumed
460/// bytes. Only with `alloc`; [`decode_application_value_ref`] reads an
461/// application-tagged value without copying it.
462#[cfg(feature = "alloc")]
463pub fn decode_application_value(
464    data: &[u8],
465    offset: usize,
466) -> Result<(PropertyValue, usize), Error> {
467    let (tag, new_offset) = tags::decode_tag(data, offset)?;
468    if tag.class == TagClass::Context {
469        if tag.is_opening {
470            let (_content, after) = tags::extract_context_value(data, new_offset, tag.number)?;
471            return Ok((
472                PropertyValue::ApplicationData(data[offset..after].to_vec()),
473                after,
474            ));
475        }
476        if tag.is_closing {
477            return Err(Error::invalid_tag(offset, "unexpected closing tag"));
478        }
479        let content_end = new_offset
480            .checked_add(tag.length as usize)
481            .ok_or_else(|| Error::decoding(new_offset, "length overflow"))?;
482        if data.len() < content_end {
483            return Err(Error::buffer_too_short(content_end, data.len()));
484        }
485        return Ok((
486            PropertyValue::ApplicationData(data[offset..content_end].to_vec()),
487            content_end,
488        ));
489    }
490    let (value, end) = view::decode_application_primitive::<false>(data, offset, tag, new_offset)?;
491    let value = match value {
492        ApplicationValueRef::Null => PropertyValue::Null,
493        ApplicationValueRef::Boolean(value) => PropertyValue::Boolean(value),
494        ApplicationValueRef::Unsigned(value) => PropertyValue::Unsigned(value),
495        ApplicationValueRef::Signed(value) => PropertyValue::Signed(value),
496        ApplicationValueRef::Real(value) => PropertyValue::Real(value),
497        ApplicationValueRef::Double(value) => PropertyValue::Double(value),
498        ApplicationValueRef::OctetString(octets) => PropertyValue::OctetString(octets.to_vec()),
499        ApplicationValueRef::CharacterString(text) => {
500            PropertyValue::CharacterString(decode_character_string(text.contents())?)
501        }
502        ApplicationValueRef::BitString { unused_bits, data } => PropertyValue::BitString {
503            unused_bits,
504            data: data.to_vec(),
505        },
506        ApplicationValueRef::Enumerated(value) => PropertyValue::Enumerated(value),
507        ApplicationValueRef::Date(value) => PropertyValue::Date(value),
508        ApplicationValueRef::Time(value) => PropertyValue::Time(value),
509        ApplicationValueRef::ObjectIdentifier(value) => PropertyValue::ObjectIdentifier(value),
510    };
511    Ok((value, end))
512}
513
514/// Check that an application-tagged value starts at `offset` and is structurally well formed,
515/// returning the offset just past it.
516///
517/// Unlike [`decode_application_value`] this accepts a CharacterString in any character set
518/// defined by Clause 20.2.9 (including DBCS and JIS X 0208, which [`decode_application_value`]
519/// rejects) and does not build a [`PropertyValue`]. Errors carry absolute offsets into
520/// `data`. Only with `alloc`.
521#[cfg(feature = "alloc")]
522pub fn validate_application_value(data: &[u8], offset: usize) -> Result<usize, Error> {
523    let (tag, content_start) = tags::decode_tag(data, offset)?;
524    if tag.class != TagClass::Application || tag.is_opening || tag.is_closing {
525        return Err(misplaced_tag(
526            data,
527            &tag,
528            None,
529            offset,
530            "expected an application-tagged value",
531        ));
532    }
533    if tag.number != app_tag::CHARACTER_STRING && tag.number != app_tag::SIGNED {
534        return decode_application_value(data, offset).map(|(_, end)| end);
535    }
536
537    let content_end = content_start
538        .checked_add(tag.length as usize)
539        .ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
540    if content_end > data.len() {
541        return Err(Error::buffer_too_short(content_end, data.len()));
542    }
543    let content = &data[content_start..content_end];
544    if tag.number == app_tag::SIGNED {
545        decode_signed_canonical(content)?;
546        return Ok(content_end);
547    }
548    view::check_character_string(content).map_err(view::rebased(content_start))?;
549    Ok(content_end)
550}
551
552/// Encode a `PropertyValue` as an application-tagged value. Only with
553/// `alloc`.
554#[cfg(feature = "alloc")]
555pub fn encode_property_value(buf: &mut BytesMut, value: &PropertyValue) -> Result<(), Error> {
556    match value {
557        PropertyValue::Null => encode_app_null(buf),
558        PropertyValue::Boolean(v) => encode_app_boolean(buf, *v),
559        PropertyValue::Unsigned(v) => encode_app_unsigned(buf, *v),
560        PropertyValue::Signed(v) => encode_app_signed(buf, *v),
561        PropertyValue::Real(v) => encode_app_real(buf, *v),
562        PropertyValue::Double(v) => encode_app_double(buf, *v),
563        PropertyValue::OctetString(v) => encode_app_octet_string(buf, v),
564        PropertyValue::CharacterString(v) => encode_app_character_string(buf, v)?,
565        PropertyValue::BitString { unused_bits, data } => {
566            encode_app_bit_string(buf, *unused_bits, data)
567        }
568        PropertyValue::Enumerated(v) => encode_app_enumerated(buf, *v),
569        PropertyValue::Date(v) => encode_app_date(buf, v),
570        PropertyValue::Time(v) => encode_app_time(buf, v),
571        PropertyValue::ObjectIdentifier(v) => encode_app_object_id(buf, v),
572        PropertyValue::List(values) => {
573            for v in values {
574                encode_property_value(buf, v)?;
575            }
576        }
577        PropertyValue::ApplicationData(bytes) => {
578            // Already-encoded application-layer content (a context-tagged
579            // CHOICE/SEQUENCE production) is emitted verbatim.
580            buf.put_slice(bytes);
581        }
582    }
583    Ok(())
584}
585
586// ===========================================================================
587// BACnetTimeStamp encode/decode
588// ===========================================================================
589
590/// Largest encodable `sequence-number` value: the production constrains it to
591/// `Unsigned (0..65535)`.
592///
593/// `BACnetTimeStamp` (ASHRAE 135-2020 Clause 21) is a three-way CHOICE: a Time
594/// under context tag 0, this bounded sequence number under tag 1, or a
595/// `BACnetDateTime` under tag 2.
596pub const MAX_TIMESTAMP_SEQUENCE_NUMBER: u64 = 65535;
597
598/// Encode a `BACnetTimeStamp` as a bare CHOICE element (no enclosing field
599/// tag). Delegates to [`encode_timestamp_choice_to_sink`] through the
600/// growable buffer; the existing result type is retained.
601///
602/// Used where a production lists `BACnetTimeStamp` as a bare CHOICE item —
603/// e.g. the `eventTimeStamps [3] SEQUENCE OF BACnetTimeStamp` inside
604/// GetEventInformation-ACK. [`encode_timestamp`] wraps this in
605/// the enclosing field's context tag pair instead.
606///
607/// Tag forms per Clause 20.2.1.5: `time [0]` is the CHOICE's only alternative
608/// over a *primitive* base type, so it encodes as a context-specific
609/// *primitive* tag 0 of length 4 holding the raw `Time` octets (the
610/// opening-tag-0 wrapper around an application-tagged `Time` is NOT
611/// conformant); `sequence-number [1]` is a primitive context tag 1 (contents
612/// constrained to `0..=65535` by the public variant's `u16` payload);
613/// `datetime [2]` tags the *constructed* `BACnetDateTime`, so it encodes as opening tag 2 /
614/// application-tagged `Date` / application-tagged `Time` / closing tag 2.
615#[cfg(feature = "alloc")]
616pub fn encode_timestamp_choice(buf: &mut BytesMut, ts: &BACnetTimeStamp) -> Result<(), Error> {
617    encode_timestamp_choice_to_sink(buf, ts).expect("the BytesMut sink cannot return SinkFull");
618    Ok(())
619}
620
621/// Decode a bare-CHOICE `BACnetTimeStamp` starting at `offset`.
622///
623/// Returns the decoded timestamp and the new offset past its encoding. Only
624/// the Clause 20.2.1.5-conformant tag forms described on
625/// `encode_timestamp_choice` are accepted; `sequence-number` contents
626/// beyond `0..=65535` are rejected. Errors follow the rule of
627/// [`constructed::tagged`](crate::constructed::tagged): a Time or Date with
628/// the wrong length is [`Error::Decoding`], contents that run past the end of
629/// `data` are [`Error::BufferTooShort`].
630pub fn decode_timestamp_choice(
631    data: &[u8],
632    offset: usize,
633) -> Result<(BACnetTimeStamp, usize), Error> {
634    use crate::constructed::tagged;
635    const WHAT: &str = "BACnetTimeStamp";
636
637    let (inner_tag, inner_pos) = tags::decode_tag(data, offset)?;
638    if inner_tag.is_context(0) {
639        let (octets, end) = tagged::decode_ctx_fixed(data, offset, 0, tagged::Fixed::TIME, WHAT)?;
640        return Ok((BACnetTimeStamp::Time(Time::decode(octets)?), end));
641    }
642    if inner_tag.is_context(1) {
643        let (octets, end) = tagged::decode_ctx_primitive(data, offset, 1, WHAT)?;
644        let n = decode_unsigned(octets)?;
645        if n > MAX_TIMESTAMP_SEQUENCE_NUMBER {
646            return Err(DecodeError::new(
647                DecodingKind::OutOfRange,
648                inner_pos,
649                "BACnetTimeStamp sequence-number {} exceeds 65535",
650            )
651            .with_value(n)
652            .into());
653        }
654        let n = u16::try_from(n).expect("BACnetTimeStamp sequence range checked above");
655        return Ok((BACnetTimeStamp::SequenceNumber(n), end));
656    }
657    if inner_tag.is_opening_tag(2) {
658        const DATE_TIME: &str = "BACnetTimeStamp DateTime";
659        let (date, date_end) =
660            tagged::decode_app_fixed(data, inner_pos, app_tag::DATE, 4, DATE_TIME)?;
661        let (time, time_end) =
662            tagged::decode_app_fixed(data, date_end, app_tag::TIME, 4, DATE_TIME)?;
663        let end = tagged::expect_closing(data, time_end, 2, DATE_TIME)?;
664        let date_time = BACnetTimeStamp::DateTime {
665            date: Date::decode(date)?,
666            time: Time::decode(time)?,
667        };
668        return Ok((date_time, end));
669    }
670    Err(DecodeError::new(
671        misplaced_kind(data, offset, &inner_tag, None),
672        offset,
673        "BACnetTimeStamp: unexpected inner choice tag",
674    )
675    .into())
676}
677
678/// Encode a BACnetTimeStamp wrapped in a context opening/closing tag pair.
679///
680/// Delegates to [`encode_timestamp_to_sink`] through the growable buffer,
681/// retaining the existing result type.
682///
683/// The outer `tag_number` is the context tag of the enclosing field.
684/// Inside, the CHOICE variant uses its own context tag (0=Time,
685/// 1=SequenceNumber, 2=DateTime) via [`encode_timestamp_choice`].
686#[cfg(feature = "alloc")]
687pub fn encode_timestamp(
688    buf: &mut BytesMut,
689    tag_number: u8,
690    ts: &BACnetTimeStamp,
691) -> Result<(), Error> {
692    encode_timestamp_to_sink(buf, tag_number, ts)
693        .expect("the BytesMut sink cannot return SinkFull");
694    Ok(())
695}
696
697/// Decode a BACnetTimeStamp from inside a context opening/closing tag pair.
698///
699/// `data` should point to the start of the outer opening tag for `tag_number`.
700/// Returns the decoded timestamp and the new offset past the outer closing tag.
701pub fn decode_timestamp(
702    data: &[u8],
703    offset: usize,
704    tag_number: u8,
705) -> Result<(BACnetTimeStamp, usize), Error> {
706    let (tag, pos) = tags::decode_tag(data, offset)?;
707    if !tag.is_opening_tag(tag_number) {
708        return Err(DecodeError::new(
709            misplaced_kind(data, offset, &tag, Some(tag_number)),
710            offset,
711            "expected opening tag {} for BACnetTimeStamp",
712        )
713        .with_value(tag_number.into())
714        .into());
715    }
716
717    let (ts, after_inner) = decode_timestamp_choice(data, pos)?;
718
719    let (close, final_pos) = tags::decode_tag(data, after_inner)?;
720    if !close.is_closing_tag(tag_number) {
721        return Err(DecodeError::new(
722            unclosed_kind(&close),
723            after_inner,
724            "expected closing tag {} for BACnetTimeStamp",
725        )
726        .with_value(tag_number.into())
727        .into());
728    }
729
730    Ok((ts, final_pos))
731}
732
733// ===========================================================================
734// Tests
735// ===========================================================================
736
737#[cfg(all(test, feature = "alloc"))]
738mod tests;
739
740#[cfg(all(test, feature = "alloc"))]
741mod length_first_tests;