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}