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