Skip to main content

bacnet_objects/audit/
receipt.rs

1use crate::collections::HashSet;
2use crate::prelude::*;
3
4use bacnet_types::error::Error;
5
6/// Repository retention policy for completed confirmed Audit receipts.
7pub const COMPLETED_AUDIT_RECEIPT_RETENTION_MILLIS: u64 = 60_000;
8/// Maximum completed confirmed Audit receipts retained by one Audit Log.
9pub const MAX_COMPLETED_AUDIT_RECEIPTS: usize = 256;
10/// Maximum exact-key size, including the bounded 64-KiB service payload.
11pub const MAX_AUDIT_RECEIPT_KEY_BYTES: usize = 64 * 1024 + 1024;
12
13/// One completed confirmed Audit request retained in an Audit Log snapshot.
14///
15/// The opaque key is an injective server-owned encoding of the canonical
16/// requester and complete confirmed request. Persistence implementations must
17/// retain this field with the records from the same snapshot transaction.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct CompletedAuditReceipt {
20    key: Vec<u8>,
21    completed_at_unix_millis: u64,
22}
23
24impl CompletedAuditReceipt {
25    /// Construct one bounded completed receipt at a Unix UTC millisecond.
26    pub fn new(key: Vec<u8>, completed_at_unix_millis: u64) -> Result<Self, Error> {
27        validate_key(&key)?;
28        Ok(Self {
29            key,
30            completed_at_unix_millis,
31        })
32    }
33
34    /// Exact opaque request identity.
35    pub fn key(&self) -> &[u8] {
36        &self.key
37    }
38
39    /// Restart-stable UTC completion time in Unix milliseconds.
40    pub fn completed_at_unix_millis(&self) -> u64 {
41        self.completed_at_unix_millis
42    }
43}
44
45/// Result of atomically checking and storing a confirmed Audit receipt.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum ConfirmedAuditNotificationOutcome {
48    /// Notifications and the newly completed receipt were durably accepted.
49    Stored,
50    /// The exact completed receipt was already retained and no state changed.
51    Duplicate,
52}
53
54pub(super) fn validate_key(key: &[u8]) -> Result<(), Error> {
55    if key.is_empty() {
56        return Err(Error::Encoding(
57            "AuditLog completed receipt key must not be empty".into(),
58        ));
59    }
60    if key.len() > MAX_AUDIT_RECEIPT_KEY_BYTES {
61        return Err(Error::OutOfRange(format!(
62            "AuditLog completed receipt key length {} exceeds {MAX_AUDIT_RECEIPT_KEY_BYTES}",
63            key.len()
64        )));
65    }
66    Ok(())
67}
68
69pub(super) fn validate_receipts(receipts: &[CompletedAuditReceipt]) -> Result<(), Error> {
70    if receipts.len() > MAX_COMPLETED_AUDIT_RECEIPTS {
71        return Err(Error::OutOfRange(format!(
72            "AuditLog completed receipt count {} exceeds {MAX_COMPLETED_AUDIT_RECEIPTS}",
73            receipts.len()
74        )));
75    }
76    let mut keys = HashSet::with_capacity(receipts.len());
77    for receipt in receipts {
78        validate_key(receipt.key())?;
79        if !keys.insert(receipt.key()) {
80            return Err(Error::Encoding(
81                "AuditLog completed receipt keys must be unique".into(),
82            ));
83        }
84    }
85    Ok(())
86}
87
88pub(super) fn contains(
89    receipts: &[CompletedAuditReceipt],
90    key: &[u8],
91    now_unix_millis: u64,
92) -> Result<bool, Error> {
93    validate_key(key)?;
94    Ok(receipts.iter().any(|receipt| {
95        receipt.key() == key
96            && now_unix_millis >= receipt.completed_at_unix_millis()
97            && now_unix_millis - receipt.completed_at_unix_millis()
98                < COMPLETED_AUDIT_RECEIPT_RETENTION_MILLIS
99    }))
100}
101
102pub(super) fn insert(
103    receipts: &mut Vec<CompletedAuditReceipt>,
104    receipt: CompletedAuditReceipt,
105) -> Result<ConfirmedAuditNotificationOutcome, Error> {
106    validate_key(receipt.key())?;
107    let now = receipt.completed_at_unix_millis();
108    receipts.retain(|stored| {
109        now >= stored.completed_at_unix_millis()
110            && now - stored.completed_at_unix_millis() < COMPLETED_AUDIT_RECEIPT_RETENTION_MILLIS
111    });
112    if receipts.iter().any(|stored| stored.key() == receipt.key()) {
113        return Ok(ConfirmedAuditNotificationOutcome::Duplicate);
114    }
115    if receipts.len() == MAX_COMPLETED_AUDIT_RECEIPTS {
116        let oldest = receipts
117            .iter()
118            .enumerate()
119            .min_by_key(|(index, stored)| (stored.completed_at_unix_millis(), *index))
120            .map(|(index, _)| index)
121            .expect("a full receipt list is nonempty");
122        receipts.remove(oldest);
123    }
124    receipts.push(receipt);
125    Ok(ConfirmedAuditNotificationOutcome::Stored)
126}