Skip to main content

bacnet_objects/audit/
receipt.rs

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