Skip to main content

bacnet_services/
common.rs

1//! Shared BACnet service data types per ASHRAE 135-2020 Clause 21.
2
3use bacnet_encoding::primitives;
4use bacnet_encoding::tags;
5use bacnet_types::enums::PropertyIdentifier;
6use bacnet_types::error::Error;
7use bytes::{BufMut, BytesMut};
8
9/// Safety limit for decoded sequences to prevent unbounded allocations.
10pub const MAX_DECODED_ITEMS: usize = 10_000;
11
12// ---------------------------------------------------------------------------
13// PropertyReference
14// ---------------------------------------------------------------------------
15
16/// BACnetPropertyReference per Clause 21.
17///
18/// ```text
19/// BACnetPropertyReference ::= SEQUENCE {
20///     propertyIdentifier  [0] BACnetPropertyIdentifier,
21///     propertyArrayIndex  [1] Unsigned OPTIONAL
22/// }
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PropertyReference {
26    pub property_identifier: PropertyIdentifier,
27    pub property_array_index: Option<u32>,
28}
29
30impl PropertyReference {
31    pub fn encode(&self, buf: &mut BytesMut) {
32        primitives::encode_ctx_unsigned(buf, 0, self.property_identifier.to_raw() as u64);
33        if let Some(idx) = self.property_array_index {
34            primitives::encode_ctx_unsigned(buf, 1, idx as u64);
35        }
36    }
37
38    pub fn decode(data: &[u8], offset: usize) -> Result<(Self, usize), Error> {
39        // [0] propertyIdentifier
40        let (tag, pos) = tags::decode_tag(data, offset)?;
41        let end = pos + tag.length as usize;
42        if end > data.len() {
43            return Err(Error::decoding(
44                pos,
45                "PropertyReference truncated at property-id",
46            ));
47        }
48        let prop_id = primitives::decode_unsigned(&data[pos..end])? as u32;
49        let mut offset = end;
50
51        // [1] propertyArrayIndex (optional)
52        let mut array_index = None;
53        if offset < data.len() {
54            let (tag, new_pos) = tags::decode_tag(data, offset)?;
55            if tag.is_context(1) {
56                let end = new_pos + tag.length as usize;
57                if end > data.len() {
58                    return Err(Error::decoding(
59                        new_pos,
60                        "PropertyReference truncated at array-index",
61                    ));
62                }
63                array_index = Some(primitives::decode_unsigned(&data[new_pos..end])? as u32);
64                offset = end;
65            }
66        }
67
68        Ok((
69            Self {
70                property_identifier: PropertyIdentifier::from_raw(prop_id),
71                property_array_index: array_index,
72            },
73            offset,
74        ))
75    }
76}
77
78// ---------------------------------------------------------------------------
79// BACnetPropertyValue
80// ---------------------------------------------------------------------------
81
82/// BACnetPropertyValue per Clause 21.
83///
84/// ```text
85/// BACnetPropertyValue ::= SEQUENCE {
86///     propertyIdentifier  [0] BACnetPropertyIdentifier,
87///     propertyArrayIndex  [1] Unsigned OPTIONAL,
88///     value               [2] ABSTRACT-SYNTAX.&Type,
89///     priority            [3] Unsigned (1..16) OPTIONAL
90/// }
91/// ```
92///
93/// The `value` field contains raw application-tagged bytes. The application
94/// layer interprets the value based on the property type.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct BACnetPropertyValue {
97    pub property_identifier: PropertyIdentifier,
98    pub property_array_index: Option<u32>,
99    pub value: Vec<u8>,
100    pub priority: Option<u8>,
101}
102
103impl BACnetPropertyValue {
104    pub fn encode(&self, buf: &mut BytesMut) {
105        // [0] propertyIdentifier
106        primitives::encode_ctx_unsigned(buf, 0, self.property_identifier.to_raw() as u64);
107        // [1] propertyArrayIndex (optional)
108        if let Some(idx) = self.property_array_index {
109            primitives::encode_ctx_unsigned(buf, 1, idx as u64);
110        }
111        // [2] value (opening/closing)
112        tags::encode_opening_tag(buf, 2);
113        buf.put_slice(&self.value);
114        tags::encode_closing_tag(buf, 2);
115        // [3] priority (optional)
116        if let Some(prio) = self.priority {
117            primitives::encode_ctx_unsigned(buf, 3, prio as u64);
118        }
119    }
120
121    pub fn decode(data: &[u8], offset: usize) -> Result<(Self, usize), Error> {
122        // [0] propertyIdentifier
123        let (tag, pos) = tags::decode_tag(data, offset)?;
124        let end = pos + tag.length as usize;
125        if end > data.len() {
126            return Err(Error::decoding(
127                pos,
128                "BACnetPropertyValue truncated at property-id",
129            ));
130        }
131        let prop_id = primitives::decode_unsigned(&data[pos..end])? as u32;
132        let mut offset = end;
133
134        // [1] propertyArrayIndex (optional) — peek to see if it's tag 1
135        let mut array_index = None;
136        if offset < data.len() {
137            let (tag, new_pos) = tags::decode_tag(data, offset)?;
138            if tag.is_context(1) {
139                let end = new_pos + tag.length as usize;
140                if end > data.len() {
141                    return Err(Error::decoding(
142                        new_pos,
143                        "BACnetPropertyValue truncated at array-index",
144                    ));
145                }
146                array_index = Some(primitives::decode_unsigned(&data[new_pos..end])? as u32);
147                offset = end;
148            }
149        }
150
151        // [2] value — extract between opening/closing tag 2
152        // We need to skip the opening tag we already peeked at
153        let (tag, tag_end) = tags::decode_tag(data, offset)?;
154        if !tag.is_opening_tag(2) {
155            return Err(Error::decoding(
156                offset,
157                "BACnetPropertyValue expected opening tag 2",
158            ));
159        }
160        let (value_bytes, offset) = tags::extract_context_value(data, tag_end, 2)?;
161        let value = value_bytes.to_vec();
162
163        // [3] priority (optional)
164        let mut priority = None;
165        if offset < data.len() {
166            let (tag, new_pos) = tags::decode_tag(data, offset)?;
167            if tag.is_context(3) {
168                let end = new_pos + tag.length as usize;
169                if end > data.len() {
170                    return Err(Error::decoding(
171                        new_pos,
172                        "BACnetPropertyValue truncated at priority",
173                    ));
174                }
175                let prio = primitives::decode_unsigned(&data[new_pos..end])? as u8;
176                if !(1..=16).contains(&prio) {
177                    return Err(Error::decoding(
178                        new_pos,
179                        format!("BACnetPropertyValue priority {prio} out of range 1-16"),
180                    ));
181                }
182                priority = Some(prio);
183                return Ok((
184                    Self {
185                        property_identifier: PropertyIdentifier::from_raw(prop_id),
186                        property_array_index: array_index,
187                        value,
188                        priority,
189                    },
190                    end,
191                ));
192            }
193        }
194
195        Ok((
196            Self {
197                property_identifier: PropertyIdentifier::from_raw(prop_id),
198                property_array_index: array_index,
199                value,
200                priority,
201            },
202            offset,
203        ))
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Tests
209// ---------------------------------------------------------------------------
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn property_reference_round_trip() {
217        let pr = PropertyReference {
218            property_identifier: PropertyIdentifier::PRESENT_VALUE,
219            property_array_index: None,
220        };
221        let mut buf = BytesMut::new();
222        pr.encode(&mut buf);
223        let (decoded, _) = PropertyReference::decode(&buf, 0).unwrap();
224        assert_eq!(pr, decoded);
225    }
226
227    #[test]
228    fn property_reference_with_index_round_trip() {
229        let pr = PropertyReference {
230            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
231            property_array_index: Some(8),
232        };
233        let mut buf = BytesMut::new();
234        pr.encode(&mut buf);
235        let (decoded, _) = PropertyReference::decode(&buf, 0).unwrap();
236        assert_eq!(pr, decoded);
237    }
238
239    #[test]
240    fn bacnet_property_value_round_trip() {
241        let pv = BACnetPropertyValue {
242            property_identifier: PropertyIdentifier::PRESENT_VALUE,
243            property_array_index: None,
244            value: vec![0x44, 0x42, 0x90, 0x00, 0x00], // app-tagged Real 72.5
245            priority: None,
246        };
247        let mut buf = BytesMut::new();
248        pv.encode(&mut buf);
249        let (decoded, _) = BACnetPropertyValue::decode(&buf, 0).unwrap();
250        assert_eq!(pv, decoded);
251    }
252
253    #[test]
254    fn bacnet_property_value_with_all_fields() {
255        let pv = BACnetPropertyValue {
256            property_identifier: PropertyIdentifier::PRESENT_VALUE,
257            property_array_index: Some(5),
258            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
259            priority: Some(8),
260        };
261        let mut buf = BytesMut::new();
262        pv.encode(&mut buf);
263        let (decoded, _) = BACnetPropertyValue::decode(&buf, 0).unwrap();
264        assert_eq!(pv, decoded);
265    }
266
267    #[test]
268    fn bacnet_property_value_priority_validation() {
269        let pv = BACnetPropertyValue {
270            property_identifier: PropertyIdentifier::PRESENT_VALUE,
271            property_array_index: None,
272            value: vec![0x10], // app boolean true
273            priority: Some(8),
274        };
275        let mut buf = BytesMut::new();
276        pv.encode(&mut buf);
277
278        // Manually corrupt priority to 0
279        let data = buf.to_vec();
280        let mut corrupted = data.clone();
281        // Priority is the last encoded byte — find and change it
282        let last = corrupted.len() - 1;
283        corrupted[last] = 0; // set priority value to 0
284        assert!(BACnetPropertyValue::decode(&corrupted, 0).is_err());
285    }
286
287    // -----------------------------------------------------------------------
288    // Malformed-input decode error tests
289    // -----------------------------------------------------------------------
290
291    #[test]
292    fn test_decode_property_reference_empty_input() {
293        assert!(PropertyReference::decode(&[], 0).is_err());
294    }
295
296    #[test]
297    fn test_decode_property_reference_truncated_1_byte() {
298        let pr = PropertyReference {
299            property_identifier: PropertyIdentifier::PRESENT_VALUE,
300            property_array_index: Some(8),
301        };
302        let mut buf = BytesMut::new();
303        pr.encode(&mut buf);
304        assert!(PropertyReference::decode(&buf[..1], 0).is_err());
305    }
306
307    #[test]
308    fn test_decode_property_reference_invalid_tag() {
309        assert!(PropertyReference::decode(&[0xFF, 0xFF, 0xFF], 0).is_err());
310    }
311
312    #[test]
313    fn test_decode_bacnet_property_value_empty_input() {
314        assert!(BACnetPropertyValue::decode(&[], 0).is_err());
315    }
316
317    #[test]
318    fn test_decode_bacnet_property_value_truncated_1_byte() {
319        let pv = BACnetPropertyValue {
320            property_identifier: PropertyIdentifier::PRESENT_VALUE,
321            property_array_index: None,
322            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
323            priority: None,
324        };
325        let mut buf = BytesMut::new();
326        pv.encode(&mut buf);
327        assert!(BACnetPropertyValue::decode(&buf[..1], 0).is_err());
328    }
329
330    #[test]
331    fn test_decode_bacnet_property_value_truncated_2_bytes() {
332        let pv = BACnetPropertyValue {
333            property_identifier: PropertyIdentifier::PRESENT_VALUE,
334            property_array_index: None,
335            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
336            priority: None,
337        };
338        let mut buf = BytesMut::new();
339        pv.encode(&mut buf);
340        assert!(BACnetPropertyValue::decode(&buf[..2], 0).is_err());
341    }
342
343    #[test]
344    fn test_decode_bacnet_property_value_truncated_3_bytes() {
345        let pv = BACnetPropertyValue {
346            property_identifier: PropertyIdentifier::PRESENT_VALUE,
347            property_array_index: None,
348            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
349            priority: None,
350        };
351        let mut buf = BytesMut::new();
352        pv.encode(&mut buf);
353        assert!(BACnetPropertyValue::decode(&buf[..3], 0).is_err());
354    }
355
356    #[test]
357    fn test_decode_bacnet_property_value_invalid_tag() {
358        assert!(BACnetPropertyValue::decode(&[0xFF, 0xFF, 0xFF], 0).is_err());
359    }
360
361    #[test]
362    fn test_decode_bacnet_property_value_oversized_length() {
363        // Tag byte with extended length that exceeds data
364        assert!(BACnetPropertyValue::decode(&[0x05, 0xFF], 0).is_err());
365    }
366}