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
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
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            let (tag, tag_end) = tags::decode_tag(data, offset)?;
169            if !tag.is_opening_tag(3) {
170                return Err(Error::decoding(
171                    offset,
172                    "ReadPropertyACK expected opening tag 3",
173                ));
174            }
175            let (value_bytes, _end) = tags::extract_context_value(data, tag_end, 3)?;
176            return Ok(Self {
177                object_identifier,
178                property_identifier,
179                property_array_index,
180                property_value: value_bytes.to_vec(),
181            });
182        }
183
184        if !tag.is_opening_tag(3) {
185            return Err(Error::decoding(
186                offset,
187                "ReadPropertyACK expected opening tag 3",
188            ));
189        }
190        let (value_bytes, _) = tags::extract_context_value(data, tag_end, 3)?;
191
192        Ok(Self {
193            object_identifier,
194            property_identifier,
195            property_array_index,
196            property_value: value_bytes.to_vec(),
197        })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use bacnet_types::enums::ObjectType;
205
206    #[test]
207    fn request_round_trip() {
208        let req = ReadPropertyRequest {
209            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
210            property_identifier: PropertyIdentifier::PRESENT_VALUE,
211            property_array_index: None,
212        };
213        let mut buf = BytesMut::new();
214        req.encode(&mut buf);
215        let decoded = ReadPropertyRequest::decode(&buf).unwrap();
216        assert_eq!(req, decoded);
217    }
218
219    #[test]
220    fn request_with_index_round_trip() {
221        let req = ReadPropertyRequest {
222            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 5).unwrap(),
223            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
224            property_array_index: Some(8),
225        };
226        let mut buf = BytesMut::new();
227        req.encode(&mut buf);
228        let decoded = ReadPropertyRequest::decode(&buf).unwrap();
229        assert_eq!(req, decoded);
230    }
231
232    #[test]
233    fn ack_round_trip() {
234        let ack = ReadPropertyACK {
235            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
236            property_identifier: PropertyIdentifier::PRESENT_VALUE,
237            property_array_index: None,
238            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00], // Real 72.5
239        };
240        let mut buf = BytesMut::new();
241        ack.encode(&mut buf);
242        let decoded = ReadPropertyACK::decode(&buf).unwrap();
243        assert_eq!(ack, decoded);
244    }
245
246    #[test]
247    fn ack_with_index_round_trip() {
248        let ack = ReadPropertyACK {
249            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 3).unwrap(),
250            property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
251            property_array_index: Some(8),
252            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
253        };
254        let mut buf = BytesMut::new();
255        ack.encode(&mut buf);
256        let decoded = ReadPropertyACK::decode(&buf).unwrap();
257        assert_eq!(ack, decoded);
258    }
259
260    // -----------------------------------------------------------------------
261    // Malformed-input decode error tests
262    // -----------------------------------------------------------------------
263
264    #[test]
265    fn test_decode_read_property_request_empty_input() {
266        assert!(ReadPropertyRequest::decode(&[]).is_err());
267    }
268
269    #[test]
270    fn test_decode_read_property_request_truncated_1_byte() {
271        // Encode a valid request, then truncate to 1 byte
272        let req = ReadPropertyRequest {
273            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
274            property_identifier: PropertyIdentifier::PRESENT_VALUE,
275            property_array_index: None,
276        };
277        let mut buf = BytesMut::new();
278        req.encode(&mut buf);
279        assert!(ReadPropertyRequest::decode(&buf[..1]).is_err());
280    }
281
282    #[test]
283    fn test_decode_read_property_request_truncated_2_bytes() {
284        let req = ReadPropertyRequest {
285            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
286            property_identifier: PropertyIdentifier::PRESENT_VALUE,
287            property_array_index: None,
288        };
289        let mut buf = BytesMut::new();
290        req.encode(&mut buf);
291        assert!(ReadPropertyRequest::decode(&buf[..2]).is_err());
292    }
293
294    #[test]
295    fn test_decode_read_property_request_truncated_3_bytes() {
296        let req = ReadPropertyRequest {
297            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
298            property_identifier: PropertyIdentifier::PRESENT_VALUE,
299            property_array_index: None,
300        };
301        let mut buf = BytesMut::new();
302        req.encode(&mut buf);
303        assert!(ReadPropertyRequest::decode(&buf[..3]).is_err());
304    }
305
306    #[test]
307    fn test_decode_read_property_request_invalid_tag() {
308        // 0xFF is not a valid starting tag byte in BACnet context
309        assert!(ReadPropertyRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
310    }
311
312    #[test]
313    fn test_decode_read_property_request_oversized_length() {
314        // Tag byte claiming a length that exceeds available data
315        // Context tag 0, extended length indicator (5 = len in next byte), then huge length
316        assert!(ReadPropertyRequest::decode(&[0x05, 0xFF]).is_err());
317    }
318
319    #[test]
320    fn test_decode_read_property_ack_empty_input() {
321        assert!(ReadPropertyACK::decode(&[]).is_err());
322    }
323
324    #[test]
325    fn test_decode_read_property_ack_truncated_1_byte() {
326        let ack = ReadPropertyACK {
327            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
328            property_identifier: PropertyIdentifier::PRESENT_VALUE,
329            property_array_index: None,
330            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
331        };
332        let mut buf = BytesMut::new();
333        ack.encode(&mut buf);
334        assert!(ReadPropertyACK::decode(&buf[..1]).is_err());
335    }
336
337    #[test]
338    fn test_decode_read_property_ack_truncated_3_bytes() {
339        let ack = ReadPropertyACK {
340            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
341            property_identifier: PropertyIdentifier::PRESENT_VALUE,
342            property_array_index: None,
343            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
344        };
345        let mut buf = BytesMut::new();
346        ack.encode(&mut buf);
347        assert!(ReadPropertyACK::decode(&buf[..3]).is_err());
348    }
349
350    #[test]
351    fn test_decode_read_property_ack_truncated_half() {
352        let ack = ReadPropertyACK {
353            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
354            property_identifier: PropertyIdentifier::PRESENT_VALUE,
355            property_array_index: None,
356            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
357        };
358        let mut buf = BytesMut::new();
359        ack.encode(&mut buf);
360        let half = buf.len() / 2;
361        assert!(ReadPropertyACK::decode(&buf[..half]).is_err());
362    }
363
364    #[test]
365    fn test_decode_read_property_ack_invalid_tag() {
366        assert!(ReadPropertyACK::decode(&[0xFF, 0xFF, 0xFF]).is_err());
367    }
368}