Skip to main content

bacnet_services/
audit.rs

1//! Audit services per ASHRAE 135-2020 Clauses 15.2.8 / 15.2.9.
2//!
3//! Complex nested types (BACnetRecipient, BACnetPropertyReference inside
4//! AuditNotification, query options in AuditLogQuery) are stored as raw
5//! bytes to keep the codec minimal.
6
7use bacnet_encoding::primitives;
8use bacnet_encoding::tags;
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier};
11use bytes::BytesMut;
12
13use crate::common::PropertyReference;
14
15// ---------------------------------------------------------------------------
16// AuditNotificationRequest
17// ---------------------------------------------------------------------------
18
19/// AuditNotification-Request service parameters.
20///
21/// BACnetRecipient fields (sourceDevice, targetDevice) are stored as raw bytes
22/// between opening/closing tags since BACnetRecipient encode/decode is not
23/// available in the encoding crate.
24#[derive(Debug, Clone, PartialEq)]
25pub struct AuditNotificationRequest {
26    /// [0] sourceTimestamp
27    pub source_timestamp: BACnetTimeStamp,
28    /// [1] targetTimestamp OPTIONAL
29    pub target_timestamp: Option<BACnetTimeStamp>,
30    /// [2] sourceDevice — raw BACnetRecipient bytes
31    pub source_device: Vec<u8>,
32    /// [3] sourceObject OPTIONAL
33    pub source_object: Option<ObjectIdentifier>,
34    /// [4] operation
35    pub operation: u32,
36    /// [5] sourceComment OPTIONAL
37    pub source_comment: Option<String>,
38    /// [6] targetComment OPTIONAL
39    pub target_comment: Option<String>,
40    /// [7] invokeId OPTIONAL
41    pub invoke_id: Option<u8>,
42    /// [8] sourceUserInfo OPTIONAL — raw bytes
43    pub source_user_info: Option<Vec<u8>>,
44    /// [9] targetDevice — raw BACnetRecipient bytes
45    pub target_device: Vec<u8>,
46    /// [10] targetObject OPTIONAL
47    pub target_object: Option<ObjectIdentifier>,
48    /// [11] targetProperty OPTIONAL
49    pub target_property: Option<PropertyReference>,
50    /// [12] targetPriority OPTIONAL
51    pub target_priority: Option<u8>,
52    /// [13] targetValue OPTIONAL — raw bytes
53    pub target_value: Option<Vec<u8>>,
54    /// [14] currentValue OPTIONAL — raw bytes
55    pub current_value: Option<Vec<u8>>,
56    /// [15] result OPTIONAL — raw error bytes
57    pub result: Option<Vec<u8>>,
58}
59
60impl AuditNotificationRequest {
61    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
62        // [0] sourceTimestamp
63        primitives::encode_timestamp(buf, 0, &self.source_timestamp);
64        // [1] targetTimestamp OPTIONAL
65        if let Some(ref ts) = self.target_timestamp {
66            primitives::encode_timestamp(buf, 1, ts);
67        }
68        // [2] sourceDevice (raw BACnetRecipient)
69        tags::encode_opening_tag(buf, 2);
70        buf.extend_from_slice(&self.source_device);
71        tags::encode_closing_tag(buf, 2);
72        // [3] sourceObject OPTIONAL
73        if let Some(ref oid) = self.source_object {
74            primitives::encode_ctx_object_id(buf, 3, oid);
75        }
76        // [4] operation
77        primitives::encode_ctx_enumerated(buf, 4, self.operation);
78        // [5] sourceComment OPTIONAL
79        if let Some(ref s) = self.source_comment {
80            primitives::encode_ctx_character_string(buf, 5, s)?;
81        }
82        // [6] targetComment OPTIONAL
83        if let Some(ref s) = self.target_comment {
84            primitives::encode_ctx_character_string(buf, 6, s)?;
85        }
86        // [7] invokeId OPTIONAL
87        if let Some(id) = self.invoke_id {
88            primitives::encode_ctx_unsigned(buf, 7, id as u64);
89        }
90        // [8] sourceUserInfo OPTIONAL (raw)
91        if let Some(ref raw) = self.source_user_info {
92            tags::encode_opening_tag(buf, 8);
93            buf.extend_from_slice(raw);
94            tags::encode_closing_tag(buf, 8);
95        }
96        // [9] targetDevice (raw BACnetRecipient)
97        tags::encode_opening_tag(buf, 9);
98        buf.extend_from_slice(&self.target_device);
99        tags::encode_closing_tag(buf, 9);
100        // [10] targetObject OPTIONAL
101        if let Some(ref oid) = self.target_object {
102            primitives::encode_ctx_object_id(buf, 10, oid);
103        }
104        // [11] targetProperty OPTIONAL
105        if let Some(ref pr) = self.target_property {
106            tags::encode_opening_tag(buf, 11);
107            pr.encode(buf);
108            tags::encode_closing_tag(buf, 11);
109        }
110        // [12] targetPriority OPTIONAL
111        if let Some(prio) = self.target_priority {
112            primitives::encode_ctx_unsigned(buf, 12, prio as u64);
113        }
114        // [13] targetValue OPTIONAL (raw)
115        if let Some(ref raw) = self.target_value {
116            tags::encode_opening_tag(buf, 13);
117            buf.extend_from_slice(raw);
118            tags::encode_closing_tag(buf, 13);
119        }
120        // [14] currentValue OPTIONAL (raw)
121        if let Some(ref raw) = self.current_value {
122            tags::encode_opening_tag(buf, 14);
123            buf.extend_from_slice(raw);
124            tags::encode_closing_tag(buf, 14);
125        }
126        // [15] result OPTIONAL (raw)
127        if let Some(ref raw) = self.result {
128            tags::encode_opening_tag(buf, 15);
129            buf.extend_from_slice(raw);
130            tags::encode_closing_tag(buf, 15);
131        }
132        Ok(())
133    }
134
135    pub fn decode(data: &[u8]) -> Result<Self, Error> {
136        let mut offset = 0;
137
138        // [0] sourceTimestamp
139        let (source_timestamp, new_off) = primitives::decode_timestamp(data, offset, 0)?;
140        offset = new_off;
141
142        // [1] targetTimestamp OPTIONAL
143        let mut target_timestamp = None;
144        if offset < data.len() {
145            let (peek, _) = tags::decode_tag(data, offset)?;
146            if peek.is_opening_tag(1) {
147                let (ts, new_off) = primitives::decode_timestamp(data, offset, 1)?;
148                target_timestamp = Some(ts);
149                offset = new_off;
150            }
151        }
152
153        // [2] sourceDevice (raw)
154        let (tag, tag_end) = tags::decode_tag(data, offset)?;
155        if !tag.is_opening_tag(2) {
156            return Err(Error::decoding(
157                offset,
158                "AuditNotification expected opening tag 2 for source-device",
159            ));
160        }
161        let (raw, new_off) = tags::extract_context_value(data, tag_end, 2)?;
162        let source_device = raw.to_vec();
163        offset = new_off;
164
165        // [3] sourceObject OPTIONAL
166        let mut source_object = None;
167        if offset < data.len() {
168            let (opt, new_off) = tags::decode_optional_context(data, offset, 3)?;
169            if let Some(content) = opt {
170                source_object = Some(ObjectIdentifier::decode(content)?);
171                offset = new_off;
172            }
173        }
174
175        // [4] operation
176        let (tag, pos) = tags::decode_tag(data, offset)?;
177        let end = pos + tag.length as usize;
178        if end > data.len() {
179            return Err(Error::decoding(
180                pos,
181                "AuditNotification truncated at operation",
182            ));
183        }
184        let operation = primitives::decode_unsigned(&data[pos..end])? as u32;
185        offset = end;
186
187        // [5] sourceComment OPTIONAL
188        let mut source_comment = None;
189        if offset < data.len() {
190            let (opt, new_off) = tags::decode_optional_context(data, offset, 5)?;
191            if let Some(content) = opt {
192                source_comment = Some(primitives::decode_character_string(content)?);
193                offset = new_off;
194            }
195        }
196
197        // [6] targetComment OPTIONAL
198        let mut target_comment = None;
199        if offset < data.len() {
200            let (opt, new_off) = tags::decode_optional_context(data, offset, 6)?;
201            if let Some(content) = opt {
202                target_comment = Some(primitives::decode_character_string(content)?);
203                offset = new_off;
204            }
205        }
206
207        // [7] invokeId OPTIONAL
208        let mut invoke_id = None;
209        if offset < data.len() {
210            let (opt, new_off) = tags::decode_optional_context(data, offset, 7)?;
211            if let Some(content) = opt {
212                invoke_id = Some(primitives::decode_unsigned(content)? as u8);
213                offset = new_off;
214            }
215        }
216
217        // [8] sourceUserInfo OPTIONAL (raw)
218        let mut source_user_info = None;
219        if offset < data.len() {
220            let (peek, _) = tags::decode_tag(data, offset)?;
221            if peek.is_opening_tag(8) {
222                let (_, inner_start) = tags::decode_tag(data, offset)?;
223                let (raw, new_off) = tags::extract_context_value(data, inner_start, 8)?;
224                source_user_info = Some(raw.to_vec());
225                offset = new_off;
226            }
227        }
228
229        // [9] targetDevice (raw)
230        let (tag, tag_end) = tags::decode_tag(data, offset)?;
231        if !tag.is_opening_tag(9) {
232            return Err(Error::decoding(
233                offset,
234                "AuditNotification expected opening tag 9 for target-device",
235            ));
236        }
237        let (raw, new_off) = tags::extract_context_value(data, tag_end, 9)?;
238        let target_device = raw.to_vec();
239        offset = new_off;
240
241        // [10] targetObject OPTIONAL
242        let mut target_object = None;
243        if offset < data.len() {
244            let (opt, new_off) = tags::decode_optional_context(data, offset, 10)?;
245            if let Some(content) = opt {
246                target_object = Some(ObjectIdentifier::decode(content)?);
247                offset = new_off;
248            }
249        }
250
251        // [11] targetProperty OPTIONAL
252        let mut target_property = None;
253        if offset < data.len() {
254            let (peek, _) = tags::decode_tag(data, offset)?;
255            if peek.is_opening_tag(11) {
256                let (_, inner_start) = tags::decode_tag(data, offset)?;
257                let (pr, pr_end) = PropertyReference::decode(data, inner_start)?;
258                target_property = Some(pr);
259                let (_tag, tag_end) = tags::decode_tag(data, pr_end)?;
260                offset = tag_end;
261            }
262        }
263
264        // [12] targetPriority OPTIONAL
265        let mut target_priority = None;
266        if offset < data.len() {
267            let (opt, new_off) = tags::decode_optional_context(data, offset, 12)?;
268            if let Some(content) = opt {
269                target_priority = Some(primitives::decode_unsigned(content)? as u8);
270                offset = new_off;
271            }
272        }
273
274        // [13] targetValue OPTIONAL (raw)
275        let mut target_value = None;
276        if offset < data.len() {
277            let (peek, _) = tags::decode_tag(data, offset)?;
278            if peek.is_opening_tag(13) {
279                let (_, inner_start) = tags::decode_tag(data, offset)?;
280                let (raw, new_off) = tags::extract_context_value(data, inner_start, 13)?;
281                target_value = Some(raw.to_vec());
282                offset = new_off;
283            }
284        }
285
286        // [14] currentValue OPTIONAL (raw)
287        let mut current_value = None;
288        if offset < data.len() {
289            let (peek, _) = tags::decode_tag(data, offset)?;
290            if peek.is_opening_tag(14) {
291                let (_, inner_start) = tags::decode_tag(data, offset)?;
292                let (raw, new_off) = tags::extract_context_value(data, inner_start, 14)?;
293                current_value = Some(raw.to_vec());
294                offset = new_off;
295            }
296        }
297
298        // [15] result OPTIONAL (raw)
299        let mut result = None;
300        if offset < data.len() {
301            let (peek, _) = tags::decode_tag(data, offset)?;
302            if peek.is_opening_tag(15) {
303                let (_, inner_start) = tags::decode_tag(data, offset)?;
304                let (raw, new_off) = tags::extract_context_value(data, inner_start, 15)?;
305                result = Some(raw.to_vec());
306                offset = new_off;
307            }
308        }
309        let _ = offset;
310
311        Ok(Self {
312            source_timestamp,
313            target_timestamp,
314            source_device,
315            source_object,
316            operation,
317            source_comment,
318            target_comment,
319            invoke_id,
320            source_user_info,
321            target_device,
322            target_object,
323            target_property,
324            target_priority,
325            target_value,
326            current_value,
327            result,
328        })
329    }
330}
331
332// ---------------------------------------------------------------------------
333// AuditLogQueryRequest
334// ---------------------------------------------------------------------------
335
336/// AuditLogQuery-Request storing query options as raw bytes.
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct AuditLogQueryRequest {
339    /// [0] acknowledgmentFilter
340    pub acknowledgment_filter: u32,
341    /// Remaining query options as raw bytes (context tags 1+).
342    pub query_options_raw: Vec<u8>,
343}
344
345impl AuditLogQueryRequest {
346    pub fn encode(&self, buf: &mut BytesMut) {
347        // [0] acknowledgmentFilter
348        primitives::encode_ctx_enumerated(buf, 0, self.acknowledgment_filter);
349        buf.extend_from_slice(&self.query_options_raw);
350    }
351
352    pub fn decode(data: &[u8]) -> Result<Self, Error> {
353        let mut offset = 0;
354
355        // [0] acknowledgmentFilter
356        let (tag, pos) = tags::decode_tag(data, offset)?;
357        let end = pos + tag.length as usize;
358        if end > data.len() {
359            return Err(Error::decoding(
360                pos,
361                "AuditLogQuery truncated at acknowledgment-filter",
362            ));
363        }
364        let acknowledgment_filter = primitives::decode_unsigned(&data[pos..end])? as u32;
365        offset = end;
366
367        let query_options_raw = data[offset..].to_vec();
368
369        Ok(Self {
370            acknowledgment_filter,
371            query_options_raw,
372        })
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use bacnet_types::enums::{ObjectType, PropertyIdentifier};
380    use bacnet_types::primitives::Time;
381
382    #[test]
383    fn audit_notification_round_trip() {
384        let req = AuditNotificationRequest {
385            source_timestamp: BACnetTimeStamp::SequenceNumber(100),
386            target_timestamp: None,
387            source_device: vec![0x09, 0x01], // raw recipient
388            source_object: Some(ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap()),
389            operation: 3,
390            source_comment: Some("test audit".to_string()),
391            target_comment: None,
392            invoke_id: Some(5),
393            source_user_info: None,
394            target_device: vec![0x09, 0x02], // raw recipient
395            target_object: Some(ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap()),
396            target_property: Some(PropertyReference {
397                property_identifier: PropertyIdentifier::PRESENT_VALUE,
398                property_array_index: None,
399            }),
400            target_priority: Some(8),
401            target_value: Some(vec![0x44, 0x42, 0x90, 0x00, 0x00]),
402            current_value: Some(vec![0x44, 0x00, 0x00, 0x00, 0x00]),
403            result: None,
404        };
405        let mut buf = BytesMut::new();
406        req.encode(&mut buf).unwrap();
407        let decoded = AuditNotificationRequest::decode(&buf).unwrap();
408        assert_eq!(req, decoded);
409    }
410
411    #[test]
412    fn audit_notification_minimal() {
413        let req = AuditNotificationRequest {
414            source_timestamp: BACnetTimeStamp::Time(Time {
415                hour: 10,
416                minute: 0,
417                second: 0,
418                hundredths: 0,
419            }),
420            target_timestamp: None,
421            source_device: vec![0x09, 0x01],
422            source_object: None,
423            operation: 0,
424            source_comment: None,
425            target_comment: None,
426            invoke_id: None,
427            source_user_info: None,
428            target_device: vec![0x09, 0x02],
429            target_object: None,
430            target_property: None,
431            target_priority: None,
432            target_value: None,
433            current_value: None,
434            result: None,
435        };
436        let mut buf = BytesMut::new();
437        req.encode(&mut buf).unwrap();
438        let decoded = AuditNotificationRequest::decode(&buf).unwrap();
439        assert_eq!(req, decoded);
440    }
441
442    #[test]
443    fn audit_notification_empty_input() {
444        assert!(AuditNotificationRequest::decode(&[]).is_err());
445    }
446
447    #[test]
448    fn audit_log_query_round_trip() {
449        let req = AuditLogQueryRequest {
450            acknowledgment_filter: 1,
451            query_options_raw: vec![0x19, 0x05, 0x29, 0x0A],
452        };
453        let mut buf = BytesMut::new();
454        req.encode(&mut buf);
455        let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
456        assert_eq!(req, decoded);
457    }
458
459    #[test]
460    fn audit_log_query_no_options() {
461        let req = AuditLogQueryRequest {
462            acknowledgment_filter: 0,
463            query_options_raw: vec![],
464        };
465        let mut buf = BytesMut::new();
466        req.encode(&mut buf);
467        let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
468        assert_eq!(req, decoded);
469    }
470
471    #[test]
472    fn audit_log_query_empty_input() {
473        assert!(AuditLogQueryRequest::decode(&[]).is_err());
474    }
475}