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