Skip to main content

bacnet_encoding/constructed/property_value/
mod.rs

1//! `BACnetPropertyValue` (Clause 21) and the property-value field that the
2//! property access services carry.
3//!
4//! A property value is four context-tagged members: the property identifier
5//! `[0]`, an optional array index `[1]`, the value inside an opening/closing
6//! `[2]` pair, and an optional priority `[3]`. The value stays encoded; its
7//! datatype depends on the property, which the application layer knows.
8//!
9//! Decoding the value needs to know where it ends. Each caller names the tags
10//! that may follow a value in its own production ([`PropertyValueBoundary`]),
11//! which also lets a value encoded in the pre-framing EventParameters form be
12//! told apart from the frame around it.
13
14use bacnet_types::constructed::BACnetPropertyValue;
15use bacnet_types::enums::PropertyIdentifier;
16use bacnet_types::error::Error;
17use bytes::{BufMut, BytesMut};
18
19use crate::{primitives, tags};
20
21mod decode;
22
23pub use decode::{
24    decode_bacnet_property_value, decode_bacnet_property_value_in_list,
25    decode_bacnet_property_value_in_list_detailed, PropertyValueDecodeError,
26    PropertyValueDecodeFailure, PropertyValueDecodeStage,
27};
28
29/// Append the encoding of one property value to `buf`.
30///
31/// The value bytes are copied inside the `[2]` pair as they are; callers that
32/// accept values from outside check them first.
33pub fn encode_bacnet_property_value(value: &BACnetPropertyValue, buf: &mut BytesMut) {
34    // [0] propertyIdentifier
35    primitives::encode_ctx_unsigned(buf, 0, value.property_identifier.to_raw() as u64);
36    // [1] propertyArrayIndex (optional)
37    if let Some(idx) = value.property_array_index {
38        primitives::encode_ctx_unsigned(buf, 1, idx as u64);
39    }
40    // [2] value (opening/closing)
41    tags::encode_opening_tag(buf, 2);
42    buf.put_slice(&value.value);
43    tags::encode_closing_tag(buf, 2);
44    // [3] priority (optional)
45    if let Some(prio) = value.priority {
46        primitives::encode_ctx_unsigned(buf, 3, prio as u64);
47    }
48}
49
50/// A tag that may follow an encoded property value in the enclosing
51/// production, used to find where the value ends.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PropertyValueBoundary {
54    /// The end of the input.
55    End,
56    /// A context tag with this number, primitive or opening.
57    Context(u8),
58    /// A primitive context tag with this number that runs to the end of the
59    /// input.
60    ContextToEnd(u8),
61    /// A closing tag with this number.
62    Closing(u8),
63}
64
65/// Whether `offset` in `data` is at `boundary`.
66fn matches_property_boundary(data: &[u8], offset: usize, boundary: PropertyValueBoundary) -> bool {
67    match boundary {
68        PropertyValueBoundary::End => offset == data.len(),
69        PropertyValueBoundary::Context(number) => {
70            tags::decode_tag(data, offset).is_ok_and(|(tag, _)| tag.is_context(number))
71        }
72        PropertyValueBoundary::ContextToEnd(number) => {
73            tags::decode_tag(data, offset).is_ok_and(|(tag, content_start)| {
74                tag.is_context(number)
75                    && content_start
76                        .checked_add(tag.length as usize)
77                        .is_some_and(|end| end == data.len())
78            })
79        }
80        PropertyValueBoundary::Closing(number) => {
81            tags::decode_tag(data, offset).is_ok_and(|(tag, _)| tag.is_closing_tag(number))
82        }
83    }
84}
85
86/// Extract the encoded value of `property` that starts at `offset`, just
87/// inside the opening tag `closing_tag`, returning the value bytes and the
88/// offset past the matching closing tag.
89///
90/// `boundaries` lists what may follow the closing tag in the enclosing
91/// production. It is consulted only for an EVENT_PARAMETERS value in the
92/// pre-framing form, whose payload can hold octets that look like the closing
93/// tag.
94pub fn extract_property_value<'a>(
95    data: &'a [u8],
96    offset: usize,
97    closing_tag: u8,
98    property: PropertyIdentifier,
99    boundaries: &[PropertyValueBoundary],
100) -> Result<(&'a [u8], usize), Error> {
101    if property == PropertyIdentifier::EVENT_PARAMETERS
102        && data.get(offset..offset.saturating_add(2)) == Some(&[0xfe, 0xff])
103    {
104        // Before EventParameter framing, tag 255 wrapped arbitrary octets. Use
105        // the enclosing service grammar to distinguish a payload marker from
106        // the wrapper terminator without consuming a sibling property. Reject
107        // multiple valid boundaries because the old format cannot disambiguate them.
108        let outer_close = (closing_tag << 4) | 0x0f;
109        let mut candidate = None;
110        for pos in offset.saturating_add(4)..data.len() {
111            let end = pos + 1;
112            if data[pos] == outer_close
113                && data.get(pos - 2..pos) == Some(&[0xff, 0xff])
114                && boundaries
115                    .iter()
116                    .any(|boundary| matches_property_boundary(data, end, *boundary))
117            {
118                if candidate.is_some() {
119                    return Err(Error::invalid_tag(
120                        offset,
121                        "legacy EventParameters value has ambiguous closing tags",
122                    ));
123                }
124                candidate = Some((pos, end));
125            }
126        }
127        if let Some((pos, end)) = candidate {
128            return Ok((&data[offset..pos], end));
129        }
130        return Err(Error::missing(
131            offset,
132            "legacy EventParameters value is missing its closing tags",
133        ));
134    }
135
136    tags::extract_context_value(data, offset, closing_tag)
137}