Skip to main content

bacnet_objects/
audit.rs

1//! AuditLog (type 62) and AuditReporter (type 61) objects per Addendum 135-2016bj.
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5
6use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
9
10use crate::common::read_property_list_property;
11use crate::traits::BACnetObject;
12
13// ---------------------------------------------------------------------------
14// AuditLog (type 62)
15// ---------------------------------------------------------------------------
16
17/// A single audit log record.
18#[derive(Debug, Clone)]
19pub struct AuditRecord {
20    pub timestamp_secs: u64,
21    pub description: String,
22}
23
24/// BACnet AuditLog object — stores audit trail records in a ring buffer.
25pub struct AuditLogObject {
26    oid: ObjectIdentifier,
27    name: String,
28    description: String,
29    log_enable: bool,
30    buffer_size: u32,
31    buffer: VecDeque<AuditRecord>,
32    total_record_count: u64,
33    status_flags: StatusFlags,
34}
35
36impl AuditLogObject {
37    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
38        let oid = ObjectIdentifier::new(ObjectType::AUDIT_LOG, instance)?;
39        Ok(Self {
40            oid,
41            name: name.into(),
42            description: String::new(),
43            log_enable: true,
44            buffer_size,
45            buffer: VecDeque::new(),
46            total_record_count: 0,
47            status_flags: StatusFlags::empty(),
48        })
49    }
50
51    /// Add an audit record to the log.
52    pub fn add_record(&mut self, record: AuditRecord) {
53        if !self.log_enable {
54            return;
55        }
56        if self.buffer.len() >= self.buffer_size as usize {
57            self.buffer.pop_front();
58        }
59        self.buffer.push_back(record);
60        self.total_record_count += 1;
61    }
62
63    /// Get the current buffer contents.
64    pub fn records(&self) -> &VecDeque<AuditRecord> {
65        &self.buffer
66    }
67
68    /// Set the description string.
69    pub fn set_description(&mut self, desc: impl Into<String>) {
70        self.description = desc.into();
71    }
72}
73
74impl BACnetObject for AuditLogObject {
75    fn object_identifier(&self) -> ObjectIdentifier {
76        self.oid
77    }
78
79    fn object_name(&self) -> &str {
80        &self.name
81    }
82
83    fn read_property(
84        &self,
85        property: PropertyIdentifier,
86        array_index: Option<u32>,
87    ) -> Result<PropertyValue, Error> {
88        match property {
89            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
90                Ok(PropertyValue::ObjectIdentifier(self.oid))
91            }
92            p if p == PropertyIdentifier::OBJECT_NAME => {
93                Ok(PropertyValue::CharacterString(self.name.clone()))
94            }
95            p if p == PropertyIdentifier::DESCRIPTION => {
96                Ok(PropertyValue::CharacterString(self.description.clone()))
97            }
98            p if p == PropertyIdentifier::OBJECT_TYPE => {
99                Ok(PropertyValue::Enumerated(ObjectType::AUDIT_LOG.to_raw()))
100            }
101            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
102            p if p == PropertyIdentifier::BUFFER_SIZE => {
103                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
104            }
105            p if p == PropertyIdentifier::RECORD_COUNT => {
106                Ok(PropertyValue::Unsigned(self.buffer.len() as u64))
107            }
108            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
109                Ok(PropertyValue::Unsigned(self.total_record_count))
110            }
111            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
112                unused_bits: 4,
113                data: vec![self.status_flags.bits() << 4],
114            }),
115            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
116            p if p == PropertyIdentifier::PROPERTY_LIST => {
117                read_property_list_property(&self.property_list(), array_index)
118            }
119            _ => Err(Error::Protocol {
120                class: ErrorClass::PROPERTY.to_raw() as u32,
121                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
122            }),
123        }
124    }
125
126    fn write_property(
127        &mut self,
128        property: PropertyIdentifier,
129        _array_index: Option<u32>,
130        value: PropertyValue,
131        _priority: Option<u8>,
132    ) -> Result<(), Error> {
133        if property == PropertyIdentifier::LOG_ENABLE {
134            if let PropertyValue::Boolean(v) = value {
135                self.log_enable = v;
136                return Ok(());
137            }
138            return Err(Error::Protocol {
139                class: ErrorClass::PROPERTY.to_raw() as u32,
140                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
141            });
142        }
143        if property == PropertyIdentifier::RECORD_COUNT {
144            if let PropertyValue::Unsigned(0) = value {
145                self.buffer.clear();
146                return Ok(());
147            }
148            return Err(Error::Protocol {
149                class: ErrorClass::PROPERTY.to_raw() as u32,
150                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
151            });
152        }
153        if property == PropertyIdentifier::DESCRIPTION {
154            if let PropertyValue::CharacterString(s) = value {
155                self.description = s;
156                return Ok(());
157            }
158            return Err(Error::Protocol {
159                class: ErrorClass::PROPERTY.to_raw() as u32,
160                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
161            });
162        }
163        Err(Error::Protocol {
164            class: ErrorClass::PROPERTY.to_raw() as u32,
165            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
166        })
167    }
168
169    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
170        static PROPS: &[PropertyIdentifier] = &[
171            PropertyIdentifier::OBJECT_IDENTIFIER,
172            PropertyIdentifier::OBJECT_NAME,
173            PropertyIdentifier::DESCRIPTION,
174            PropertyIdentifier::OBJECT_TYPE,
175            PropertyIdentifier::LOG_ENABLE,
176            PropertyIdentifier::BUFFER_SIZE,
177            PropertyIdentifier::RECORD_COUNT,
178            PropertyIdentifier::TOTAL_RECORD_COUNT,
179            PropertyIdentifier::STATUS_FLAGS,
180            PropertyIdentifier::EVENT_STATE,
181        ];
182        Cow::Borrowed(PROPS)
183    }
184}
185
186// ---------------------------------------------------------------------------
187// AuditReporter (type 61)
188// ---------------------------------------------------------------------------
189
190/// BACnet AuditReporter object — configures which audit notifications to send.
191pub struct AuditReporterObject {
192    oid: ObjectIdentifier,
193    name: String,
194    description: String,
195    status_flags: StatusFlags,
196}
197
198impl AuditReporterObject {
199    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
200        let oid = ObjectIdentifier::new(ObjectType::AUDIT_REPORTER, instance)?;
201        Ok(Self {
202            oid,
203            name: name.into(),
204            description: String::new(),
205            status_flags: StatusFlags::empty(),
206        })
207    }
208
209    /// Set the description string.
210    pub fn set_description(&mut self, desc: impl Into<String>) {
211        self.description = desc.into();
212    }
213}
214
215impl BACnetObject for AuditReporterObject {
216    fn object_identifier(&self) -> ObjectIdentifier {
217        self.oid
218    }
219
220    fn object_name(&self) -> &str {
221        &self.name
222    }
223
224    fn read_property(
225        &self,
226        property: PropertyIdentifier,
227        array_index: Option<u32>,
228    ) -> Result<PropertyValue, Error> {
229        match property {
230            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
231                Ok(PropertyValue::ObjectIdentifier(self.oid))
232            }
233            p if p == PropertyIdentifier::OBJECT_NAME => {
234                Ok(PropertyValue::CharacterString(self.name.clone()))
235            }
236            p if p == PropertyIdentifier::DESCRIPTION => {
237                Ok(PropertyValue::CharacterString(self.description.clone()))
238            }
239            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
240                ObjectType::AUDIT_REPORTER.to_raw(),
241            )),
242            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
243                unused_bits: 4,
244                data: vec![self.status_flags.bits() << 4],
245            }),
246            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
247            p if p == PropertyIdentifier::PROPERTY_LIST => {
248                read_property_list_property(&self.property_list(), array_index)
249            }
250            _ => Err(Error::Protocol {
251                class: ErrorClass::PROPERTY.to_raw() as u32,
252                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
253            }),
254        }
255    }
256
257    fn write_property(
258        &mut self,
259        property: PropertyIdentifier,
260        _array_index: Option<u32>,
261        value: PropertyValue,
262        _priority: Option<u8>,
263    ) -> Result<(), Error> {
264        if property == PropertyIdentifier::DESCRIPTION {
265            if let PropertyValue::CharacterString(s) = value {
266                self.description = s;
267                return Ok(());
268            }
269            return Err(Error::Protocol {
270                class: ErrorClass::PROPERTY.to_raw() as u32,
271                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
272            });
273        }
274        Err(Error::Protocol {
275            class: ErrorClass::PROPERTY.to_raw() as u32,
276            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
277        })
278    }
279
280    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
281        static PROPS: &[PropertyIdentifier] = &[
282            PropertyIdentifier::OBJECT_IDENTIFIER,
283            PropertyIdentifier::OBJECT_NAME,
284            PropertyIdentifier::DESCRIPTION,
285            PropertyIdentifier::OBJECT_TYPE,
286            PropertyIdentifier::STATUS_FLAGS,
287            PropertyIdentifier::EVENT_STATE,
288        ];
289        Cow::Borrowed(PROPS)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    // --- AuditLog ---
298
299    #[test]
300    fn audit_log_add_records() {
301        let mut al = AuditLogObject::new(1, "AL-1", 100).unwrap();
302        al.add_record(AuditRecord {
303            timestamp_secs: 1000,
304            description: "User login".into(),
305        });
306        assert_eq!(al.records().len(), 1);
307        assert_eq!(
308            al.read_property(PropertyIdentifier::RECORD_COUNT, None)
309                .unwrap(),
310            PropertyValue::Unsigned(1)
311        );
312    }
313
314    #[test]
315    fn audit_log_ring_buffer() {
316        let mut al = AuditLogObject::new(1, "AL-1", 2).unwrap();
317        for i in 0..4 {
318            al.add_record(AuditRecord {
319                timestamp_secs: i * 60,
320                description: format!("Event {i}"),
321            });
322        }
323        assert_eq!(al.records().len(), 2);
324        assert_eq!(al.records()[0].description, "Event 2");
325        assert_eq!(
326            al.read_property(PropertyIdentifier::TOTAL_RECORD_COUNT, None)
327                .unwrap(),
328            PropertyValue::Unsigned(4)
329        );
330    }
331
332    #[test]
333    fn audit_log_disable() {
334        let mut al = AuditLogObject::new(1, "AL-1", 100).unwrap();
335        al.write_property(
336            PropertyIdentifier::LOG_ENABLE,
337            None,
338            PropertyValue::Boolean(false),
339            None,
340        )
341        .unwrap();
342        al.add_record(AuditRecord {
343            timestamp_secs: 1000,
344            description: "Should not appear".into(),
345        });
346        assert_eq!(al.records().len(), 0);
347    }
348
349    #[test]
350    fn audit_log_clear() {
351        let mut al = AuditLogObject::new(1, "AL-1", 100).unwrap();
352        al.add_record(AuditRecord {
353            timestamp_secs: 1000,
354            description: "Event".into(),
355        });
356        al.write_property(
357            PropertyIdentifier::RECORD_COUNT,
358            None,
359            PropertyValue::Unsigned(0),
360            None,
361        )
362        .unwrap();
363        assert_eq!(al.records().len(), 0);
364    }
365
366    #[test]
367    fn audit_log_read_object_type() {
368        let al = AuditLogObject::new(1, "AL-1", 100).unwrap();
369        assert_eq!(
370            al.read_property(PropertyIdentifier::OBJECT_TYPE, None)
371                .unwrap(),
372            PropertyValue::Enumerated(ObjectType::AUDIT_LOG.to_raw())
373        );
374    }
375
376    // --- AuditReporter ---
377
378    #[test]
379    fn audit_reporter_read_object_type() {
380        let ar = AuditReporterObject::new(1, "AR-1").unwrap();
381        assert_eq!(
382            ar.read_property(PropertyIdentifier::OBJECT_TYPE, None)
383                .unwrap(),
384            PropertyValue::Enumerated(ObjectType::AUDIT_REPORTER.to_raw())
385        );
386    }
387
388    #[test]
389    fn audit_reporter_write_denied() {
390        let mut ar = AuditReporterObject::new(1, "AR-1").unwrap();
391        assert!(ar
392            .write_property(
393                PropertyIdentifier::OBJECT_NAME,
394                None,
395                PropertyValue::CharacterString("new".into()),
396                None,
397            )
398            .is_err());
399    }
400}