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