Skip to main content

bacnet_services/
read_property.rs

1//! ReadProperty service per ASHRAE 135-2020 Clause 15.5.
2
3use bacnet_encoding::primitives;
4use bacnet_encoding::tags::{self, TagClass};
5use bacnet_types::enums::PropertyIdentifier;
6use bacnet_types::error::Error;
7use bacnet_types::primitives::ObjectIdentifier;
8use bytes::BytesMut;
9
10// ---------------------------------------------------------------------------
11// ReadPropertyRequest (Clause 15.5.1.1)
12// ---------------------------------------------------------------------------
13
14/// ReadProperty-Request service parameters.
15///
16/// ```text
17/// ReadProperty-Request ::= SEQUENCE {
18///     objectIdentifier    [0] BACnetObjectIdentifier,
19///     propertyIdentifier  [1] BACnetPropertyIdentifier,
20///     propertyArrayIndex  [2] Unsigned OPTIONAL
21/// }
22/// ```
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ReadPropertyRequest {
25    pub object_identifier: ObjectIdentifier,
26    pub property_identifier: PropertyIdentifier,
27    pub property_array_index: Option<u32>,
28}
29
30impl ReadPropertyRequest {
31    pub fn encode(&self, buf: &mut BytesMut) {
32        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
33        primitives::encode_ctx_unsigned(buf, 1, self.property_identifier.to_raw() as u64);
34        if let Some(idx) = self.property_array_index {
35            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
36        }
37    }
38
39    pub fn decode(data: &[u8]) -> Result<Self, Error> {
40        let mut offset = 0;
41
42        // [0] object-identifier
43        let (tag, pos) = tags::decode_tag(data, offset)?;
44        let end = pos + tag.length as usize;
45        if end > data.len() {
46            return Err(Error::decoding(
47                pos,
48                "ReadProperty request truncated at object-id",
49            ));
50        }
51        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
52        offset = end;
53
54        // [1] property-identifier
55        let (tag, pos) = tags::decode_tag(data, offset)?;
56        let end = pos + tag.length as usize;
57        if end > data.len() {
58            return Err(Error::decoding(
59                pos,
60                "ReadProperty request truncated at property-id",
61            ));
62        }
63        let prop_raw = primitives::decode_unsigned(&data[pos..end])? as u32;
64        let property_identifier = PropertyIdentifier::from_raw(prop_raw);
65        offset = end;
66
67        // [2] propertyArrayIndex (optional)
68        let mut property_array_index = None;
69        if offset < data.len() {
70            let (tag, pos) = tags::decode_tag(data, offset)?;
71            if tag.is_context(2) {
72                let end = pos + tag.length as usize;
73                if end > data.len() {
74                    return Err(Error::decoding(
75                        pos,
76                        "ReadProperty request truncated at array-index",
77                    ));
78                }
79                property_array_index = Some(primitives::decode_unsigned(&data[pos..end])? as u32);
80            }
81        }
82
83        Ok(Self {
84            object_identifier,
85            property_identifier,
86            property_array_index,
87        })
88    }
89}
90
91// ---------------------------------------------------------------------------
92// ReadPropertyACK (Clause 15.5.1.2)
93// ---------------------------------------------------------------------------
94
95/// ReadProperty-ACK service parameters.
96///
97/// ```text
98/// ReadProperty-ACK ::= SEQUENCE {
99///     objectIdentifier    [0] BACnetObjectIdentifier,
100///     propertyIdentifier  [1] BACnetPropertyIdentifier,
101///     propertyArrayIndex  [2] Unsigned OPTIONAL,
102///     propertyValue       [3] ABSTRACT-SYNTAX.&TYPE
103/// }
104/// ```
105///
106/// The `property_value` field contains raw application-tagged bytes.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ReadPropertyACK {
109    pub object_identifier: ObjectIdentifier,
110    pub property_identifier: PropertyIdentifier,
111    pub property_array_index: Option<u32>,
112    pub property_value: Vec<u8>,
113}
114
115impl ReadPropertyACK {
116    pub fn encode(&self, buf: &mut BytesMut) {
117        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
118        primitives::encode_ctx_unsigned(buf, 1, self.property_identifier.to_raw() as u64);
119        if let Some(idx) = self.property_array_index {
120            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
121        }
122        tags::encode_opening_tag(buf, 3);
123        buf.extend_from_slice(&self.property_value);
124        tags::encode_closing_tag(buf, 3);
125    }
126
127    pub fn decode(data: &[u8]) -> Result<Self, Error> {
128        let mut offset = 0;
129
130        // [0] object-identifier
131        let (tag, pos) = tags::decode_tag(data, offset)?;
132        let end = pos + tag.length as usize;
133        if end > data.len() {
134            return Err(Error::decoding(
135                pos,
136                "ReadPropertyACK truncated at object-id",
137            ));
138        }
139        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
140        offset = end;
141
142        // [1] property-identifier
143        let (tag, pos) = tags::decode_tag(data, offset)?;
144        let end = pos + tag.length as usize;
145        if end > data.len() {
146            return Err(Error::decoding(
147                pos,
148                "ReadPropertyACK truncated at property-id",
149            ));
150        }
151        let prop_raw = primitives::decode_unsigned(&data[pos..end])? as u32;
152        let property_identifier = PropertyIdentifier::from_raw(prop_raw);
153        offset = end;
154
155        // [2] propertyArrayIndex (optional) or [3] opening tag
156        let mut property_array_index = None;
157        let (tag, tag_end) = tags::decode_tag(data, offset)?;
158        if tag.class == TagClass::Context && tag.number == 2 && !tag.is_opening && !tag.is_closing {
159            let end = tag_end + tag.length as usize;
160            if end > data.len() {
161                return Err(Error::decoding(
162                    tag_end,
163                    "ReadPropertyACK truncated at array-index",
164                ));
165            }
166            property_array_index = Some(primitives::decode_unsigned(&data[tag_end..end])? as u32);
167            offset = end;
168            // Read opening tag 3
169            let (tag, tag_end) = tags::decode_tag(data, offset)?;
170            if !tag.is_opening_tag(3) {
171                return Err(Error::decoding(
172                    offset,
173                    "ReadPropertyACK expected opening tag 3",
174                ));
175            }
176            let (value_bytes, _end) = tags::extract_context_value(data, tag_end, 3)?;
177            return Ok(Self {
178                object_identifier,
179                property_identifier,
180                property_array_index,
181                property_value: value_bytes.to_vec(),
182            });
183        }
184
185        // tag should be opening tag 3
186        if !tag.is_opening_tag(3) {
187            return Err(Error::decoding(
188                offset,
189                "ReadPropertyACK expected opening tag 3",
190            ));
191        }
192        let (value_bytes, _) = tags::extract_context_value(data, tag_end, 3)?;
193
194        Ok(Self {
195            object_identifier,
196            property_identifier,
197            property_array_index,
198            property_value: value_bytes.to_vec(),
199        })
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Tests
205// ---------------------------------------------------------------------------
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use bacnet_types::enums::ObjectType;
211
212    #[test]
213    fn request_round_trip() {
214        let req = ReadPropertyRequest {
215            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
216            property_identifier: PropertyIdentifier::PRESENT_VALUE,
217            property_array_index: None,
218        };
219        let mut buf = BytesMut::new();
220        req.encode(&mut buf);
221        let decoded = ReadPropertyRequest::decode(&buf).unwrap();
222        assert_eq!(req, decoded);
223    }
224
225    #[test]
226    fn request_with_index_round_trip() {
227        let req = ReadPropertyRequest {
228            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 5).unwrap(),
229            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
230            property_array_index: Some(8),
231        };
232        let mut buf = BytesMut::new();
233        req.encode(&mut buf);
234        let decoded = ReadPropertyRequest::decode(&buf).unwrap();
235        assert_eq!(req, decoded);
236    }
237
238    #[test]
239    fn ack_round_trip() {
240        let ack = ReadPropertyACK {
241            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
242            property_identifier: PropertyIdentifier::PRESENT_VALUE,
243            property_array_index: None,
244            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00], // Real 72.5
245        };
246        let mut buf = BytesMut::new();
247        ack.encode(&mut buf);
248        let decoded = ReadPropertyACK::decode(&buf).unwrap();
249        assert_eq!(ack, decoded);
250    }
251
252    #[test]
253    fn ack_with_index_round_trip() {
254        let ack = ReadPropertyACK {
255            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 3).unwrap(),
256            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
257            property_array_index: Some(8),
258            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
259        };
260        let mut buf = BytesMut::new();
261        ack.encode(&mut buf);
262        let decoded = ReadPropertyACK::decode(&buf).unwrap();
263        assert_eq!(ack, decoded);
264    }
265
266    // -----------------------------------------------------------------------
267    // Malformed-input decode error tests
268    // -----------------------------------------------------------------------
269
270    #[test]
271    fn test_decode_read_property_request_empty_input() {
272        assert!(ReadPropertyRequest::decode(&[]).is_err());
273    }
274
275    #[test]
276    fn test_decode_read_property_request_truncated_1_byte() {
277        // Encode a valid request, then truncate to 1 byte
278        let req = ReadPropertyRequest {
279            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
280            property_identifier: PropertyIdentifier::PRESENT_VALUE,
281            property_array_index: None,
282        };
283        let mut buf = BytesMut::new();
284        req.encode(&mut buf);
285        assert!(ReadPropertyRequest::decode(&buf[..1]).is_err());
286    }
287
288    #[test]
289    fn test_decode_read_property_request_truncated_2_bytes() {
290        let req = ReadPropertyRequest {
291            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
292            property_identifier: PropertyIdentifier::PRESENT_VALUE,
293            property_array_index: None,
294        };
295        let mut buf = BytesMut::new();
296        req.encode(&mut buf);
297        assert!(ReadPropertyRequest::decode(&buf[..2]).is_err());
298    }
299
300    #[test]
301    fn test_decode_read_property_request_truncated_3_bytes() {
302        let req = ReadPropertyRequest {
303            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
304            property_identifier: PropertyIdentifier::PRESENT_VALUE,
305            property_array_index: None,
306        };
307        let mut buf = BytesMut::new();
308        req.encode(&mut buf);
309        assert!(ReadPropertyRequest::decode(&buf[..3]).is_err());
310    }
311
312    #[test]
313    fn test_decode_read_property_request_invalid_tag() {
314        // 0xFF is not a valid starting tag byte in BACnet context
315        assert!(ReadPropertyRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
316    }
317
318    #[test]
319    fn test_decode_read_property_request_oversized_length() {
320        // Tag byte claiming a length that exceeds available data
321        // Context tag 0, extended length indicator (5 = len in next byte), then huge length
322        assert!(ReadPropertyRequest::decode(&[0x05, 0xFF]).is_err());
323    }
324
325    #[test]
326    fn test_decode_read_property_ack_empty_input() {
327        assert!(ReadPropertyACK::decode(&[]).is_err());
328    }
329
330    #[test]
331    fn test_decode_read_property_ack_truncated_1_byte() {
332        let ack = ReadPropertyACK {
333            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
334            property_identifier: PropertyIdentifier::PRESENT_VALUE,
335            property_array_index: None,
336            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
337        };
338        let mut buf = BytesMut::new();
339        ack.encode(&mut buf);
340        assert!(ReadPropertyACK::decode(&buf[..1]).is_err());
341    }
342
343    #[test]
344    fn test_decode_read_property_ack_truncated_3_bytes() {
345        let ack = ReadPropertyACK {
346            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
347            property_identifier: PropertyIdentifier::PRESENT_VALUE,
348            property_array_index: None,
349            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
350        };
351        let mut buf = BytesMut::new();
352        ack.encode(&mut buf);
353        assert!(ReadPropertyACK::decode(&buf[..3]).is_err());
354    }
355
356    #[test]
357    fn test_decode_read_property_ack_truncated_half() {
358        let ack = ReadPropertyACK {
359            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
360            property_identifier: PropertyIdentifier::PRESENT_VALUE,
361            property_array_index: None,
362            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
363        };
364        let mut buf = BytesMut::new();
365        ack.encode(&mut buf);
366        let half = buf.len() / 2;
367        assert!(ReadPropertyACK::decode(&buf[..half]).is_err());
368    }
369
370    #[test]
371    fn test_decode_read_property_ack_invalid_tag() {
372        assert!(ReadPropertyACK::decode(&[0xFF, 0xFF, 0xFF]).is_err());
373    }
374}