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