Skip to main content

bacnet_services/
object_mgmt.rs

1//! Object management services per ASHRAE 135-2020 Clause 15.3-15.4.
2//!
3//! - CreateObject (Clause 15.3)
4//! - DeleteObject (Clause 15.4)
5
6use bacnet_encoding::primitives;
7use bacnet_encoding::tags;
8use bacnet_types::enums::ObjectType;
9use bacnet_types::error::Error;
10use bacnet_types::primitives::ObjectIdentifier;
11use bytes::BytesMut;
12
13use crate::common::{BACnetPropertyValue, MAX_DECODED_ITEMS};
14
15// ---------------------------------------------------------------------------
16// CreateObjectRequest
17// ---------------------------------------------------------------------------
18
19/// The object specifier: by type (server picks instance) or by identifier.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ObjectSpecifier {
22    /// Create by type — server assigns instance number ([0] context tag inside [0] constructed).
23    Type(ObjectType),
24    /// Create with a specific identifier ([1] context tag inside [0] constructed).
25    Identifier(ObjectIdentifier),
26}
27
28/// CreateObject-Request service parameters.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CreateObjectRequest {
31    pub object_specifier: ObjectSpecifier,
32    pub list_of_initial_values: Vec<BACnetPropertyValue>,
33}
34
35impl CreateObjectRequest {
36    pub fn encode(&self, buf: &mut BytesMut) {
37        // [0] object-specifier (constructed)
38        tags::encode_opening_tag(buf, 0);
39        match &self.object_specifier {
40            ObjectSpecifier::Type(obj_type) => {
41                primitives::encode_ctx_enumerated(buf, 0, obj_type.to_raw());
42            }
43            ObjectSpecifier::Identifier(oid) => {
44                primitives::encode_ctx_object_id(buf, 1, oid);
45            }
46        }
47        tags::encode_closing_tag(buf, 0);
48
49        // [1] list-of-initial-values (optional, constructed)
50        if !self.list_of_initial_values.is_empty() {
51            tags::encode_opening_tag(buf, 1);
52            for pv in &self.list_of_initial_values {
53                pv.encode(buf);
54            }
55            tags::encode_closing_tag(buf, 1);
56        }
57    }
58
59    pub fn decode(data: &[u8]) -> Result<Self, Error> {
60        let mut offset = 0;
61
62        // [0] object-specifier (opening tag 0)
63        let (tag, tag_end) = tags::decode_tag(data, offset)?;
64        if !tag.is_opening_tag(0) {
65            return Err(Error::decoding(
66                offset,
67                "CreateObject expected opening tag 0",
68            ));
69        }
70        offset = tag_end;
71
72        let (tag, pos) = tags::decode_tag(data, offset)?;
73        let end = pos + tag.length as usize;
74        if end > data.len() {
75            return Err(Error::decoding(
76                pos,
77                "CreateObject truncated at object-specifier",
78            ));
79        }
80
81        let object_specifier = if tag.is_context(0) {
82            let raw = primitives::decode_unsigned(&data[pos..end])?;
83            let raw = u32::try_from(raw)
84                .map_err(|_| Error::decoding(pos, "CreateObject object-type exceeds u32"))?;
85            ObjectSpecifier::Type(ObjectType::from_raw(raw))
86        } else if tag.is_context(1) {
87            ObjectSpecifier::Identifier(ObjectIdentifier::decode(&data[pos..end])?)
88        } else {
89            return Err(Error::decoding(
90                offset,
91                "CreateObject expected context tag 0 or 1 inside object-specifier",
92            ));
93        };
94        offset = end;
95
96        let (tag, tag_end) = tags::decode_tag(data, offset)?;
97        if !tag.is_closing_tag(0) {
98            return Err(Error::decoding(
99                offset,
100                "CreateObject expected closing tag 0",
101            ));
102        }
103        offset = tag_end;
104
105        // [1] list-of-initial-values (optional, opening tag 1)
106        let mut values = Vec::new();
107        if offset < data.len() {
108            let (tag, tag_end) = tags::decode_tag(data, offset)?;
109            if tag.is_opening_tag(1) {
110                offset = tag_end;
111                loop {
112                    if offset >= data.len() {
113                        return Err(Error::decoding(
114                            offset,
115                            "CreateObject missing closing tag 1",
116                        ));
117                    }
118                    let (tag, closing_end) = tags::decode_tag(data, offset)?;
119                    if tag.is_closing_tag(1) {
120                        offset = closing_end;
121                        break;
122                    }
123                    if values.len() >= MAX_DECODED_ITEMS {
124                        return Err(Error::decoding(offset, "CreateObject values exceeds max"));
125                    }
126                    let (pv, new_offset) = BACnetPropertyValue::decode_in_list(data, offset, 1)?;
127                    values.push(pv);
128                    offset = new_offset;
129                }
130            } else {
131                return Err(Error::decoding(
132                    offset,
133                    "CreateObject expected opening tag 1",
134                ));
135            }
136        }
137
138        if offset != data.len() {
139            return Err(Error::decoding(offset, "CreateObject has trailing data"));
140        }
141
142        Ok(Self {
143            object_specifier,
144            list_of_initial_values: values,
145        })
146    }
147}
148
149// ---------------------------------------------------------------------------
150// DeleteObjectRequest
151// ---------------------------------------------------------------------------
152
153/// DeleteObject-Request service parameters (APPLICATION-tagged).
154///
155/// Uses SimpleACK (no ACK struct needed).
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct DeleteObjectRequest {
158    pub object_identifier: ObjectIdentifier,
159}
160
161impl DeleteObjectRequest {
162    pub fn encode(&self, buf: &mut BytesMut) {
163        primitives::encode_app_object_id(buf, &self.object_identifier);
164    }
165
166    pub fn decode(data: &[u8]) -> Result<Self, Error> {
167        let (tag, pos) = tags::decode_tag(data, 0)?;
168        let end = pos + tag.length as usize;
169        if end > data.len() {
170            return Err(Error::decoding(pos, "DeleteObject truncated at object-id"));
171        }
172        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
173        Ok(Self { object_identifier })
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use bacnet_types::enums::{ObjectType, PropertyIdentifier};
181
182    fn create_object_type_bytes(object_type: &[u8]) -> BytesMut {
183        let mut buf = BytesMut::new();
184        tags::encode_opening_tag(&mut buf, 0);
185        primitives::encode_ctx_octet_string(&mut buf, 0, object_type);
186        tags::encode_closing_tag(&mut buf, 0);
187        buf
188    }
189
190    #[test]
191    fn create_object_by_type_round_trip() {
192        let req = CreateObjectRequest {
193            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
194            list_of_initial_values: vec![],
195        };
196        let mut buf = BytesMut::new();
197        req.encode(&mut buf);
198        let decoded = CreateObjectRequest::decode(&buf).unwrap();
199        assert_eq!(req, decoded);
200    }
201
202    #[test]
203    fn create_object_by_id_with_values() {
204        let req = CreateObjectRequest {
205            object_specifier: ObjectSpecifier::Identifier(
206                ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
207            ),
208            list_of_initial_values: vec![BACnetPropertyValue {
209                property_identifier: PropertyIdentifier::OBJECT_NAME,
210                property_array_index: None,
211                value: vec![0x75, 0x06, 0x00, 0x5A, 0x6F, 0x6E, 0x65, 0x31],
212                priority: None,
213            }],
214        };
215        let mut buf = BytesMut::new();
216        req.encode(&mut buf);
217        let decoded = CreateObjectRequest::decode(&buf).unwrap();
218        assert_eq!(req, decoded);
219    }
220
221    #[test]
222    fn create_object_type_must_fit_u32() {
223        let max_with_leading_zero = [0, 0xFF, 0xFF, 0xFF, 0xFF];
224        let decoded =
225            CreateObjectRequest::decode(&create_object_type_bytes(&max_with_leading_zero)).unwrap();
226        assert_eq!(
227            decoded.object_specifier,
228            ObjectSpecifier::Type(ObjectType::from_raw(u32::MAX))
229        );
230
231        for overflow in [u32::MAX as u64 + 1, u64::MAX] {
232            assert!(CreateObjectRequest::decode(&create_object_type_bytes(
233                &overflow.to_be_bytes(),
234            ))
235            .is_err());
236        }
237    }
238
239    #[test]
240    fn create_object_rejects_unexpected_or_trailing_fields() {
241        let encoded = create_object_type_bytes(&[0]);
242
243        let mut empty_values = encoded.clone();
244        tags::encode_opening_tag(&mut empty_values, 1);
245        tags::encode_closing_tag(&mut empty_values, 1);
246        assert!(CreateObjectRequest::decode(&empty_values).is_ok());
247
248        let mut unexpected = encoded.clone();
249        primitives::encode_ctx_unsigned(&mut unexpected, 1, 1);
250        assert!(CreateObjectRequest::decode(&unexpected).is_err());
251
252        let mut trailing = empty_values;
253        primitives::encode_app_null(&mut trailing);
254        assert!(CreateObjectRequest::decode(&trailing).is_err());
255    }
256
257    #[test]
258    fn create_object_accepts_exact_initial_value_limit() {
259        let value = BACnetPropertyValue {
260            property_identifier: PropertyIdentifier::PRESENT_VALUE,
261            property_array_index: None,
262            value: vec![0],
263            priority: None,
264        };
265        let request = CreateObjectRequest {
266            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
267            list_of_initial_values: vec![value.clone(); MAX_DECODED_ITEMS],
268        };
269        let mut encoded = BytesMut::new();
270        request.encode(&mut encoded);
271        assert_eq!(
272            CreateObjectRequest::decode(&encoded)
273                .unwrap()
274                .list_of_initial_values
275                .len(),
276            MAX_DECODED_ITEMS
277        );
278
279        let request = CreateObjectRequest {
280            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
281            list_of_initial_values: vec![value; MAX_DECODED_ITEMS + 1],
282        };
283        encoded.clear();
284        request.encode(&mut encoded);
285        assert!(CreateObjectRequest::decode(&encoded).is_err());
286    }
287
288    #[test]
289    fn delete_object_round_trip() {
290        let req = DeleteObjectRequest {
291            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
292        };
293        let mut buf = BytesMut::new();
294        req.encode(&mut buf);
295        let decoded = DeleteObjectRequest::decode(&buf).unwrap();
296        assert_eq!(req, decoded);
297    }
298
299    // -----------------------------------------------------------------------
300    // Malformed-input decode error tests
301    // -----------------------------------------------------------------------
302
303    #[test]
304    fn test_decode_create_object_empty_input() {
305        assert!(CreateObjectRequest::decode(&[]).is_err());
306    }
307
308    #[test]
309    fn test_decode_create_object_truncated_1_byte() {
310        let req = CreateObjectRequest {
311            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
312            list_of_initial_values: vec![],
313        };
314        let mut buf = BytesMut::new();
315        req.encode(&mut buf);
316        assert!(CreateObjectRequest::decode(&buf[..1]).is_err());
317    }
318
319    #[test]
320    fn test_decode_create_object_truncated_2_bytes() {
321        let req = CreateObjectRequest {
322            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
323            list_of_initial_values: vec![],
324        };
325        let mut buf = BytesMut::new();
326        req.encode(&mut buf);
327        assert!(CreateObjectRequest::decode(&buf[..2]).is_err());
328    }
329
330    #[test]
331    fn test_decode_create_object_truncated_3_bytes() {
332        let req = CreateObjectRequest {
333            object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
334            list_of_initial_values: vec![],
335        };
336        let mut buf = BytesMut::new();
337        req.encode(&mut buf);
338        if buf.len() > 3 {
339            assert!(CreateObjectRequest::decode(&buf[..3]).is_err());
340        }
341    }
342
343    #[test]
344    fn test_decode_create_object_invalid_tag() {
345        // First byte should be opening tag 0, not 0xFF
346        assert!(CreateObjectRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
347    }
348
349    #[test]
350    fn test_decode_create_object_missing_closing_tag() {
351        // Opening tag 0 (0x0E) but no closing tag
352        assert!(CreateObjectRequest::decode(&[0x0E, 0x09, 0x00]).is_err());
353    }
354
355    #[test]
356    fn test_decode_delete_object_empty_input() {
357        assert!(DeleteObjectRequest::decode(&[]).is_err());
358    }
359
360    #[test]
361    fn test_decode_delete_object_truncated_1_byte() {
362        let req = DeleteObjectRequest {
363            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
364        };
365        let mut buf = BytesMut::new();
366        req.encode(&mut buf);
367        assert!(DeleteObjectRequest::decode(&buf[..1]).is_err());
368    }
369
370    #[test]
371    fn test_decode_delete_object_truncated_2_bytes() {
372        let req = DeleteObjectRequest {
373            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
374        };
375        let mut buf = BytesMut::new();
376        req.encode(&mut buf);
377        assert!(DeleteObjectRequest::decode(&buf[..2]).is_err());
378    }
379
380    #[test]
381    fn test_decode_delete_object_invalid_tag() {
382        assert!(DeleteObjectRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
383    }
384}