Skip to main content

bacnet_services/
write_property.rs

1//! WriteProperty service per ASHRAE 135-2020 Clause 15.9.
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// WritePropertyRequest (Clause 15.9.1.1)
12// ---------------------------------------------------------------------------
13
14/// WriteProperty-Request service parameters.
15///
16/// ```text
17/// WriteProperty-Request ::= SEQUENCE {
18///     objectIdentifier    [0] BACnetObjectIdentifier,
19///     propertyIdentifier  [1] BACnetPropertyIdentifier,
20///     propertyArrayIndex  [2] Unsigned OPTIONAL,
21///     propertyValue       [3] ABSTRACT-SYNTAX.&TYPE,
22///     priority            [4] Unsigned (1..16) OPTIONAL
23/// }
24/// ```
25///
26/// WriteProperty uses SimpleACK (no ACK struct needed).
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct WritePropertyRequest {
29    pub object_identifier: ObjectIdentifier,
30    pub property_identifier: PropertyIdentifier,
31    pub property_array_index: Option<u32>,
32    pub property_value: Vec<u8>,
33    pub priority: Option<u8>,
34}
35
36impl WritePropertyRequest {
37    pub fn encode(&self, buf: &mut BytesMut) {
38        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
39        primitives::encode_ctx_unsigned(buf, 1, self.property_identifier.to_raw() as u64);
40        if let Some(idx) = self.property_array_index {
41            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
42        }
43        tags::encode_opening_tag(buf, 3);
44        buf.extend_from_slice(&self.property_value);
45        tags::encode_closing_tag(buf, 3);
46        if let Some(prio) = self.priority {
47            primitives::encode_ctx_unsigned(buf, 4, prio as u64);
48        }
49    }
50
51    pub fn decode(data: &[u8]) -> Result<Self, Error> {
52        let mut offset = 0;
53
54        // [0] object-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(pos, "WriteProperty truncated at object-id"));
59        }
60        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
61        offset = end;
62
63        // [1] property-identifier
64        let (tag, pos) = tags::decode_tag(data, offset)?;
65        let end = pos + tag.length as usize;
66        if end > data.len() {
67            return Err(Error::decoding(
68                pos,
69                "WriteProperty truncated at property-id",
70            ));
71        }
72        let prop_raw = primitives::decode_unsigned(&data[pos..end])? as u32;
73        let property_identifier = PropertyIdentifier::from_raw(prop_raw);
74        offset = end;
75
76        // [2] propertyArrayIndex (optional) — peek for context tag 2
77        let mut property_array_index = None;
78        let (tag, tag_end) = tags::decode_tag(data, offset)?;
79        if tag.class == TagClass::Context && tag.number == 2 && !tag.is_opening && !tag.is_closing {
80            let end = tag_end + tag.length as usize;
81            if end > data.len() {
82                return Err(Error::decoding(
83                    tag_end,
84                    "WriteProperty truncated at array-index",
85                ));
86            }
87            property_array_index = Some(primitives::decode_unsigned(&data[tag_end..end])? as u32);
88            offset = end;
89        }
90
91        // [3] propertyValue (opening/closing tag 3)
92        let (tag, tag_end) = tags::decode_tag(data, offset)?;
93        if !tag.is_opening_tag(3) {
94            return Err(Error::decoding(
95                offset,
96                "WriteProperty expected opening tag 3",
97            ));
98        }
99        let (value_bytes, new_offset) = tags::extract_context_value(data, tag_end, 3)?;
100        let property_value = value_bytes.to_vec();
101        offset = new_offset;
102
103        // [4] priority (optional)
104        let mut priority = None;
105        if offset < data.len() {
106            let (tag, pos) = tags::decode_tag(data, offset)?;
107            if tag.is_context(4) {
108                let end = pos + tag.length as usize;
109                if end > data.len() {
110                    return Err(Error::decoding(pos, "WriteProperty truncated at priority"));
111                }
112                let prio = primitives::decode_unsigned(&data[pos..end])? as u8;
113                if !(1..=16).contains(&prio) {
114                    return Err(Error::decoding(
115                        pos,
116                        format!("WriteProperty priority {prio} out of range 1-16"),
117                    ));
118                }
119                priority = Some(prio);
120            }
121        }
122
123        Ok(Self {
124            object_identifier,
125            property_identifier,
126            property_array_index,
127            property_value,
128            priority,
129        })
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use bacnet_types::enums::ObjectType;
137
138    #[test]
139    fn request_round_trip() {
140        let req = WritePropertyRequest {
141            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
142            property_identifier: PropertyIdentifier::PRESENT_VALUE,
143            property_array_index: None,
144            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
145            priority: None,
146        };
147        let mut buf = BytesMut::new();
148        req.encode(&mut buf);
149        let decoded = WritePropertyRequest::decode(&buf).unwrap();
150        assert_eq!(req, decoded);
151    }
152
153    #[test]
154    fn request_with_all_fields() {
155        let req = WritePropertyRequest {
156            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
157            property_identifier: PropertyIdentifier::PRESENT_VALUE,
158            property_array_index: Some(5),
159            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
160            priority: Some(8),
161        };
162        let mut buf = BytesMut::new();
163        req.encode(&mut buf);
164        let decoded = WritePropertyRequest::decode(&buf).unwrap();
165        assert_eq!(req, decoded);
166    }
167
168    #[test]
169    fn priority_validation() {
170        let req = WritePropertyRequest {
171            object_identifier: ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 1).unwrap(),
172            property_identifier: PropertyIdentifier::PRESENT_VALUE,
173            property_array_index: None,
174            property_value: vec![0x91, 0x01], // enumerated 1 (active)
175            priority: Some(16),               // max valid
176        };
177        let mut buf = BytesMut::new();
178        req.encode(&mut buf);
179        let decoded = WritePropertyRequest::decode(&buf).unwrap();
180        assert_eq!(decoded.priority, Some(16));
181    }
182
183    // -----------------------------------------------------------------------
184    // Malformed-input decode error tests
185    // -----------------------------------------------------------------------
186
187    #[test]
188    fn test_decode_write_property_empty_input() {
189        assert!(WritePropertyRequest::decode(&[]).is_err());
190    }
191
192    #[test]
193    fn test_decode_write_property_truncated_1_byte() {
194        let req = WritePropertyRequest {
195            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
196            property_identifier: PropertyIdentifier::PRESENT_VALUE,
197            property_array_index: None,
198            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
199            priority: None,
200        };
201        let mut buf = BytesMut::new();
202        req.encode(&mut buf);
203        assert!(WritePropertyRequest::decode(&buf[..1]).is_err());
204    }
205
206    #[test]
207    fn test_decode_write_property_truncated_2_bytes() {
208        let req = WritePropertyRequest {
209            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
210            property_identifier: PropertyIdentifier::PRESENT_VALUE,
211            property_array_index: None,
212            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
213            priority: None,
214        };
215        let mut buf = BytesMut::new();
216        req.encode(&mut buf);
217        assert!(WritePropertyRequest::decode(&buf[..2]).is_err());
218    }
219
220    #[test]
221    fn test_decode_write_property_truncated_3_bytes() {
222        let req = WritePropertyRequest {
223            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
224            property_identifier: PropertyIdentifier::PRESENT_VALUE,
225            property_array_index: None,
226            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
227            priority: None,
228        };
229        let mut buf = BytesMut::new();
230        req.encode(&mut buf);
231        assert!(WritePropertyRequest::decode(&buf[..3]).is_err());
232    }
233
234    #[test]
235    fn test_decode_write_property_truncated_half() {
236        let req = WritePropertyRequest {
237            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
238            property_identifier: PropertyIdentifier::PRESENT_VALUE,
239            property_array_index: None,
240            property_value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
241            priority: Some(8),
242        };
243        let mut buf = BytesMut::new();
244        req.encode(&mut buf);
245        let half = buf.len() / 2;
246        assert!(WritePropertyRequest::decode(&buf[..half]).is_err());
247    }
248
249    #[test]
250    fn test_decode_write_property_invalid_tag() {
251        assert!(WritePropertyRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
252    }
253
254    #[test]
255    fn test_decode_write_property_oversized_length() {
256        // Tag with oversized length field
257        assert!(WritePropertyRequest::decode(&[0x05, 0xFF]).is_err());
258    }
259}