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