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.
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.
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)
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
152        let (tag, tag_end) = tags::decode_tag(data, offset)?;
153        if !tag.is_opening_tag(2) {
154            return Err(Error::decoding(
155                offset,
156                "BACnetPropertyValue expected opening tag 2",
157            ));
158        }
159        let (value_bytes, offset) = tags::extract_context_value(data, tag_end, 2)?;
160        let value = value_bytes.to_vec();
161
162        // [3] priority (optional)
163        let mut priority = None;
164        if offset < data.len() {
165            let (tag, new_pos) = tags::decode_tag(data, offset)?;
166            if tag.is_context(3) {
167                let end = new_pos + tag.length as usize;
168                if end > data.len() {
169                    return Err(Error::decoding(
170                        new_pos,
171                        "BACnetPropertyValue truncated at priority",
172                    ));
173                }
174                let prio = primitives::decode_unsigned(&data[new_pos..end])? as u8;
175                if !(1..=16).contains(&prio) {
176                    return Err(Error::decoding(
177                        new_pos,
178                        format!("BACnetPropertyValue priority {prio} out of range 1-16"),
179                    ));
180                }
181                priority = Some(prio);
182                return Ok((
183                    Self {
184                        property_identifier: PropertyIdentifier::from_raw(prop_id),
185                        property_array_index: array_index,
186                        value,
187                        priority,
188                    },
189                    end,
190                ));
191            }
192        }
193
194        Ok((
195            Self {
196                property_identifier: PropertyIdentifier::from_raw(prop_id),
197                property_array_index: array_index,
198                value,
199                priority,
200            },
201            offset,
202        ))
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn property_reference_round_trip() {
212        let pr = PropertyReference {
213            property_identifier: PropertyIdentifier::PRESENT_VALUE,
214            property_array_index: None,
215        };
216        let mut buf = BytesMut::new();
217        pr.encode(&mut buf);
218        let (decoded, _) = PropertyReference::decode(&buf, 0).unwrap();
219        assert_eq!(pr, decoded);
220    }
221
222    #[test]
223    fn property_reference_with_index_round_trip() {
224        let pr = PropertyReference {
225            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
226            property_array_index: Some(8),
227        };
228        let mut buf = BytesMut::new();
229        pr.encode(&mut buf);
230        let (decoded, _) = PropertyReference::decode(&buf, 0).unwrap();
231        assert_eq!(pr, decoded);
232    }
233
234    #[test]
235    fn bacnet_property_value_round_trip() {
236        let pv = BACnetPropertyValue {
237            property_identifier: PropertyIdentifier::PRESENT_VALUE,
238            property_array_index: None,
239            value: vec![0x44, 0x42, 0x90, 0x00, 0x00], // app-tagged Real 72.5
240            priority: None,
241        };
242        let mut buf = BytesMut::new();
243        pv.encode(&mut buf);
244        let (decoded, _) = BACnetPropertyValue::decode(&buf, 0).unwrap();
245        assert_eq!(pv, decoded);
246    }
247
248    #[test]
249    fn bacnet_property_value_with_all_fields() {
250        let pv = BACnetPropertyValue {
251            property_identifier: PropertyIdentifier::PRESENT_VALUE,
252            property_array_index: Some(5),
253            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
254            priority: Some(8),
255        };
256        let mut buf = BytesMut::new();
257        pv.encode(&mut buf);
258        let (decoded, _) = BACnetPropertyValue::decode(&buf, 0).unwrap();
259        assert_eq!(pv, decoded);
260    }
261
262    #[test]
263    fn bacnet_property_value_priority_validation() {
264        let pv = BACnetPropertyValue {
265            property_identifier: PropertyIdentifier::PRESENT_VALUE,
266            property_array_index: None,
267            value: vec![0x10], // app boolean true
268            priority: Some(8),
269        };
270        let mut buf = BytesMut::new();
271        pv.encode(&mut buf);
272
273        // Manually corrupt priority to 0
274        let data = buf.to_vec();
275        let mut corrupted = data.clone();
276        // Priority is the last encoded byte — find and change it
277        let last = corrupted.len() - 1;
278        corrupted[last] = 0; // set priority value to 0
279        assert!(BACnetPropertyValue::decode(&corrupted, 0).is_err());
280    }
281
282    // -----------------------------------------------------------------------
283    // Malformed-input decode error tests
284    // -----------------------------------------------------------------------
285
286    #[test]
287    fn test_decode_property_reference_empty_input() {
288        assert!(PropertyReference::decode(&[], 0).is_err());
289    }
290
291    #[test]
292    fn test_decode_property_reference_truncated_1_byte() {
293        let pr = PropertyReference {
294            property_identifier: PropertyIdentifier::PRESENT_VALUE,
295            property_array_index: Some(8),
296        };
297        let mut buf = BytesMut::new();
298        pr.encode(&mut buf);
299        assert!(PropertyReference::decode(&buf[..1], 0).is_err());
300    }
301
302    #[test]
303    fn test_decode_property_reference_invalid_tag() {
304        assert!(PropertyReference::decode(&[0xFF, 0xFF, 0xFF], 0).is_err());
305    }
306
307    #[test]
308    fn test_decode_bacnet_property_value_empty_input() {
309        assert!(BACnetPropertyValue::decode(&[], 0).is_err());
310    }
311
312    #[test]
313    fn test_decode_bacnet_property_value_truncated_1_byte() {
314        let pv = BACnetPropertyValue {
315            property_identifier: PropertyIdentifier::PRESENT_VALUE,
316            property_array_index: None,
317            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
318            priority: None,
319        };
320        let mut buf = BytesMut::new();
321        pv.encode(&mut buf);
322        assert!(BACnetPropertyValue::decode(&buf[..1], 0).is_err());
323    }
324
325    #[test]
326    fn test_decode_bacnet_property_value_truncated_2_bytes() {
327        let pv = BACnetPropertyValue {
328            property_identifier: PropertyIdentifier::PRESENT_VALUE,
329            property_array_index: None,
330            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
331            priority: None,
332        };
333        let mut buf = BytesMut::new();
334        pv.encode(&mut buf);
335        assert!(BACnetPropertyValue::decode(&buf[..2], 0).is_err());
336    }
337
338    #[test]
339    fn test_decode_bacnet_property_value_truncated_3_bytes() {
340        let pv = BACnetPropertyValue {
341            property_identifier: PropertyIdentifier::PRESENT_VALUE,
342            property_array_index: None,
343            value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
344            priority: None,
345        };
346        let mut buf = BytesMut::new();
347        pv.encode(&mut buf);
348        assert!(BACnetPropertyValue::decode(&buf[..3], 0).is_err());
349    }
350
351    #[test]
352    fn test_decode_bacnet_property_value_invalid_tag() {
353        assert!(BACnetPropertyValue::decode(&[0xFF, 0xFF, 0xFF], 0).is_err());
354    }
355
356    #[test]
357    fn test_decode_bacnet_property_value_oversized_length() {
358        // Tag byte with extended length that exceeds data
359        assert!(BACnetPropertyValue::decode(&[0x05, 0xFF], 0).is_err());
360    }
361}