bacnet_objects/audit/
receipt.rs1use crate::collections::HashSet;
2use crate::prelude::*;
3
4use bacnet_types::error::Error;
5
6pub const COMPLETED_AUDIT_RECEIPT_RETENTION_MILLIS: u64 = 60_000;
8pub const MAX_COMPLETED_AUDIT_RECEIPTS: usize = 256;
10pub const MAX_AUDIT_RECEIPT_KEY_BYTES: usize = 64 * 1024 + 1024;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct CompletedAuditReceipt {
20 key: Vec<u8>,
21 completed_at_unix_millis: u64,
22}
23
24impl CompletedAuditReceipt {
25 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 pub fn key(&self) -> &[u8] {
36 &self.key
37 }
38
39 pub fn completed_at_unix_millis(&self) -> u64 {
41 self.completed_at_unix_millis
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum ConfirmedAuditNotificationOutcome {
48 Stored,
50 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}