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