Skip to main content

bacnet_objects/audit/
buffer.rs

1//! Resizing and purging an Audit Log (#1238).
2//!
3//! Buffer_Size is writable, but only while Log_Enable is FALSE (Clause
4//! 12.64.9). The clause leaves the records already held to the
5//! implementation. This log keeps the newest of them, as many as the new
6//! size holds: the records its ring would hold had it been that size all
7//! along. Growing the buffer therefore loses nothing, and shrinking it drops
8//! the oldest records without a status record, as an overflowing ring does;
9//! their sequence numbers stay unused.
10//!
11//! No peer can empty an Audit Log. Its Record_Count is read-only, unlike the
12//! other logs' (Clause 12.64.11), and Clause 12.64.10 gives no other way to
13//! reset one. The application purges it instead, through
14//! [`AuditLogObject::purge`] or the bundled server's `purge_audit_log`. The
15//! purge leaves one BUFFER_PURGED status record, which also carries
16//! LOG_DISABLED while logging is off; it is written either way, since a
17//! log-status record ignores Log_Enable (Clause 12.64.10). Total_Record_Count
18//! and the completed receipts survive the purge, so sequence numbers
19//! keep counting and a confirmed notification sent again after it is still
20//! recognized as a duplicate.
21//!
22//! Both changes reach storage before the log serves them, through the same
23//! writer as every other commit ([`super::staging`]). A commit that fails
24//! refuses the change with DEVICE / OPERATIONAL_PROBLEM and leaves the log as
25//! it was.
26
27use super::staging::{operational_problem, StagedChange};
28use super::*;
29
30/// The local date and time a record is stamped with.
31pub(super) type Timestamp = (
32    bacnet_types::primitives::Date,
33    bacnet_types::primitives::Time,
34);
35
36impl AuditLogObject {
37    /// Clear the records and append a BUFFER_PURGED status record. Returns
38    /// that record's sequence number.
39    ///
40    /// This is the application's purge: peers cannot purge an Audit Log,
41    /// whose Record_Count is read-only (Clause 12.64.11). The record is
42    /// appended whether or not logging is enabled, flagged LOG_DISABLED too
43    /// while it is not, and Total_Record_Count keeps counting. Without a
44    /// valid clock, or when the commit fails, the purge fails with DEVICE /
45    /// OPERATIONAL_PROBLEM and the log is left as it was.
46    ///
47    /// Called directly, the purge commits in place and waits for the commit
48    /// there. The bundled server's `purge_audit_log` stages it instead, so
49    /// the commit runs with the object database guard dropped.
50    pub fn purge(&mut self) -> Result<u64, Error> {
51        self.commit_change(StagedChange::Purge)?;
52        Ok(self.total_record_count)
53    }
54
55    /// A Buffer_Size write. Writing the size the log already has changes
56    /// nothing.
57    pub(super) fn write_buffer_size(&mut self, value: &PropertyValue) -> Result<(), Error> {
58        let size = written_buffer_size(self.log_enable, value)?;
59        if size == self.buffer_size {
60            return Ok(());
61        }
62        self.commit_change(StagedChange::BufferSize(size))
63    }
64}
65
66/// The size a Buffer_Size write asks for, written while Log_Enable is
67/// `log_enable`, or the reason it is refused: PROPERTY / WRITE_ACCESS_DENIED
68/// while logging is on, INVALID_DATA_TYPE for a value that is not Unsigned,
69/// and VALUE_OUT_OF_RANGE above [`MAX_AUDIT_RECORDS`]. That includes 2^32-1,
70/// which Clause 12.64.9 sets aside for a log whose size is unknown.
71pub(super) fn written_buffer_size(log_enable: bool, value: &PropertyValue) -> Result<u32, Error> {
72    if log_enable {
73        return Err(property_error(ErrorCode::WRITE_ACCESS_DENIED));
74    }
75    let PropertyValue::Unsigned(size) = value else {
76        return Err(property_error(ErrorCode::INVALID_DATA_TYPE));
77    };
78    u32::try_from(*size)
79        .ok()
80        .filter(|size| *size <= MAX_AUDIT_RECORDS)
81        .ok_or_else(|| property_error(ErrorCode::VALUE_OUT_OF_RANGE))
82}
83
84/// Make `change` to `snapshot`. A smaller size keeps the newest records
85/// that fit. A Log_Enable change and a purge append a status record stamped
86/// with `timestamp`, and fail with DEVICE / OPERATIONAL_PROBLEM without one.
87pub(super) fn apply_change(
88    snapshot: &mut AuditLogSnapshot,
89    change: StagedChange,
90    timestamp: Option<Timestamp>,
91) -> Result<(), Error> {
92    match change {
93        StagedChange::LogEnable(log_enable) => {
94            let timestamp = timestamp.ok_or_else(operational_problem)?;
95            snapshot.log_enable = log_enable;
96            append_record(snapshot, log_enable_record(timestamp, log_enable));
97        }
98        StagedChange::BufferSize(size) => {
99            let dropped = snapshot.records.len().saturating_sub(size as usize);
100            snapshot.records.drain(..dropped);
101            snapshot.capacity = size;
102        }
103        StagedChange::Purge => {
104            let timestamp = timestamp.ok_or_else(operational_problem)?;
105            snapshot.records.clear();
106            let mut status = LogStatus::BUFFER_PURGED;
107            status.set(LogStatus::LOG_DISABLED, !snapshot.log_enable);
108            append_record(
109                snapshot,
110                BACnetAuditLogRecord {
111                    timestamp,
112                    datum: BACnetAuditLogDatum::LogStatus(status),
113                },
114            );
115        }
116    }
117    Ok(())
118}
119
120fn property_error(code: ErrorCode) -> Error {
121    Error::Protocol {
122        class: ErrorClass::PROPERTY.to_raw() as u32,
123        code: code.to_raw() as u32,
124    }
125}