1use bacnet_types::primitives::{BACnetTimeStamp, Date, Time};
2
3use super::*;
4
5pub trait AuditLogNotificationSink: Send + Sync {
11 fn notification_logging_enabled(&self) -> bool;
13
14 fn store_notifications(
19 &mut self,
20 notifications: &[BACnetAuditNotification],
21 apdu_timeout_ms: u32,
22 ) -> Result<(), Error>;
23
24 fn store_notifications_with_change(
27 &mut self,
28 notifications: &[BACnetAuditNotification],
29 apdu_timeout_ms: u32,
30 ) -> Result<bool, Error> {
31 self.store_notifications(notifications, apdu_timeout_ms)?;
32 Ok(false)
33 }
34
35 fn store_confirmed_notifications_with_change(
38 &mut self,
39 notifications: &[BACnetAuditNotification],
40 apdu_timeout_ms: u32,
41 receipt: CompletedAuditReceipt,
42 ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
43 self.store_confirmed_notifications(notifications, apdu_timeout_ms, receipt)
44 .map(|outcome| (outcome, false))
45 }
46
47 fn has_completed_confirmed_receipt(
49 &self,
50 _key: &[u8],
51 _now_unix_millis: u64,
52 ) -> Result<bool, Error> {
53 Ok(false)
54 }
55
56 fn store_confirmed_notifications(
58 &mut self,
59 notifications: &[BACnetAuditNotification],
60 apdu_timeout_ms: u32,
61 _receipt: CompletedAuditReceipt,
62 ) -> Result<ConfirmedAuditNotificationOutcome, Error> {
63 self.store_notifications(notifications, apdu_timeout_ms)?;
64 Ok(ConfirmedAuditNotificationOutcome::Stored)
65 }
66
67 fn stage_notification_batch(
80 &mut self,
81 notifications: &[BACnetAuditNotification],
82 apdu_timeout_ms: u32,
83 receipt: Option<CompletedAuditReceipt>,
84 ) -> Result<AuditBatchStage, Error> {
85 match receipt {
86 Some(receipt) => self
87 .store_confirmed_notifications_with_change(notifications, apdu_timeout_ms, receipt)
88 .map(|(outcome, changed)| AuditBatchStage::Done(outcome, changed)),
89 None => self
90 .store_notifications_with_change(notifications, apdu_timeout_ms)
91 .map(|changed| {
92 AuditBatchStage::Done(ConfirmedAuditNotificationOutcome::Stored, changed)
93 }),
94 }
95 }
96
97 fn finish_notification_batch(
104 &mut self,
105 staged: StagedAuditBatch,
106 ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
107 let _ = staged;
108 Err(Error::Protocol {
109 class: ErrorClass::DEVICE.to_raw() as u32,
110 code: ErrorCode::OPERATIONAL_PROBLEM.to_raw() as u32,
111 })
112 }
113}
114
115impl AuditLogObject {
116 fn store_notification_batch(
117 &mut self,
118 notifications: &[BACnetAuditNotification],
119 apdu_timeout_ms: u32,
120 ) -> Result<bool, Error> {
121 if !self.log_enable {
122 return Err(Error::Protocol {
123 class: ErrorClass::SERVICES.to_raw() as u32,
124 code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
125 });
126 }
127 if notifications.is_empty() {
128 return Err(Error::OutOfRange(
129 "Audit notification batch must not be empty".into(),
130 ));
131 }
132
133 let mut prospective = self.snapshot_for_next_generation()?;
134 let changed =
135 self.apply_notification_batch(&mut prospective, notifications, apdu_timeout_ms)?;
136 if changed {
137 self.commit_and_apply(prospective)?;
138 }
139 Ok(changed && self.buffer_size != 0)
140 }
141
142 fn store_confirmed_notification_batch(
143 &mut self,
144 notifications: &[BACnetAuditNotification],
145 apdu_timeout_ms: u32,
146 receipt: CompletedAuditReceipt,
147 ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
148 if !self.log_enable {
149 return Err(Error::Protocol {
150 class: ErrorClass::SERVICES.to_raw() as u32,
151 code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
152 });
153 }
154 if notifications.is_empty() {
155 return Err(Error::OutOfRange(
156 "Audit notification batch must not be empty".into(),
157 ));
158 }
159 if receipt::contains(
160 &self.completed_receipts,
161 receipt.key(),
162 receipt.completed_at_unix_millis(),
163 )? {
164 return Ok((ConfirmedAuditNotificationOutcome::Duplicate, false));
165 }
166
167 let mut prospective = self.snapshot_for_next_generation()?;
168 let changed =
169 self.apply_notification_batch(&mut prospective, notifications, apdu_timeout_ms)?;
170 let outcome = receipt::insert(&mut prospective.completed_receipts, receipt)?;
171 if outcome == ConfirmedAuditNotificationOutcome::Duplicate {
172 return Ok((outcome, false));
173 }
174 self.commit_and_apply(prospective)?;
175 Ok((outcome, changed && self.buffer_size != 0))
176 }
177
178 pub(super) fn apply_notification_batch(
179 &self,
180 prospective: &mut AuditLogSnapshot,
181 notifications: &[BACnetAuditNotification],
182 apdu_timeout_ms: u32,
183 ) -> Result<bool, Error> {
184 let mut changed = false;
185 for notification in notifications {
186 if let Some(index) = matching_record_index(
187 &prospective.records,
188 notification,
189 u64::from(apdu_timeout_ms) * 2,
190 ) {
191 let BACnetAuditLogDatum::AuditNotification(stored) =
192 &mut prospective.records[index].record.datum
193 else {
194 unreachable!("matching_record_index only returns Audit notifications");
195 };
196 if stored.source_timestamp.is_some() && stored.target_timestamp.is_some() {
197 continue;
198 }
199 merge_notification(stored, notification);
200 validate_record(&prospective.records[index].record)?;
201 changed = true;
202 } else {
203 let timestamp = self.valid_timestamp()?;
204 let record = BACnetAuditLogRecord {
205 timestamp,
206 datum: BACnetAuditLogDatum::AuditNotification(notification.clone()),
207 };
208 validate_record(&record)?;
209 append_record(prospective, record);
210 changed = true;
211 }
212 }
213 Ok(changed)
214 }
215}
216
217impl AuditLogNotificationSink for AuditLogObject {
218 fn notification_logging_enabled(&self) -> bool {
219 self.log_enable
220 }
221
222 fn store_notifications(
223 &mut self,
224 notifications: &[BACnetAuditNotification],
225 apdu_timeout_ms: u32,
226 ) -> Result<(), Error> {
227 self.store_notification_batch(notifications, apdu_timeout_ms)
228 .map(|_| ())
229 }
230
231 fn store_notifications_with_change(
232 &mut self,
233 notifications: &[BACnetAuditNotification],
234 apdu_timeout_ms: u32,
235 ) -> Result<bool, Error> {
236 self.store_notification_batch(notifications, apdu_timeout_ms)
237 }
238
239 fn store_confirmed_notifications_with_change(
240 &mut self,
241 notifications: &[BACnetAuditNotification],
242 apdu_timeout_ms: u32,
243 receipt: CompletedAuditReceipt,
244 ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
245 self.store_confirmed_notification_batch(notifications, apdu_timeout_ms, receipt)
246 }
247
248 fn has_completed_confirmed_receipt(
249 &self,
250 key: &[u8],
251 now_unix_millis: u64,
252 ) -> Result<bool, Error> {
253 receipt::contains(&self.completed_receipts, key, now_unix_millis)
254 }
255
256 fn store_confirmed_notifications(
257 &mut self,
258 notifications: &[BACnetAuditNotification],
259 apdu_timeout_ms: u32,
260 receipt: CompletedAuditReceipt,
261 ) -> Result<ConfirmedAuditNotificationOutcome, Error> {
262 self.store_confirmed_notification_batch(notifications, apdu_timeout_ms, receipt)
263 .map(|(outcome, _)| outcome)
264 }
265
266 fn stage_notification_batch(
267 &mut self,
268 notifications: &[BACnetAuditNotification],
269 apdu_timeout_ms: u32,
270 receipt: Option<CompletedAuditReceipt>,
271 ) -> Result<AuditBatchStage, Error> {
272 AuditLogObject::stage_notification_batch(self, notifications, apdu_timeout_ms, receipt)
273 }
274
275 fn finish_notification_batch(
276 &mut self,
277 staged: StagedAuditBatch,
278 ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
279 AuditLogObject::finish_notification_batch(self, staged)
280 }
281}
282
283fn matching_record_index(
284 records: &[BACnetAuditLogRecordResult],
285 incoming: &BACnetAuditNotification,
286 window_ms: u64,
287) -> Option<usize> {
288 records.iter().rposition(|result| {
289 let BACnetAuditLogDatum::AuditNotification(stored) = &result.record.datum else {
290 return false;
291 };
292 notification_identity_matches(stored, incoming)
293 && complementary_timestamps_match(stored, incoming, window_ms)
294 })
295}
296
297fn notification_identity_matches(
298 stored: &BACnetAuditNotification,
299 incoming: &BACnetAuditNotification,
300) -> bool {
301 stored.source_device == incoming.source_device
308 && stored.operation == incoming.operation
309 && stored.invoke_id == incoming.invoke_id
310 && stored.target_device == incoming.target_device
311 && stored.target_property == incoming.target_property
312 && optional_agrees(&stored.source_user_id, &incoming.source_user_id)
313 && optional_agrees(&stored.source_user_role, &incoming.source_user_role)
314 && optional_agrees(&stored.target_value, &incoming.target_value)
315}
316
317fn optional_agrees<T: PartialEq>(left: &Option<T>, right: &Option<T>) -> bool {
318 match (left, right) {
319 (Some(left), Some(right)) => left == right,
320 _ => true,
321 }
322}
323
324fn complementary_timestamps_match(
325 stored: &BACnetAuditNotification,
326 incoming: &BACnetAuditNotification,
327 window_ms: u64,
328) -> bool {
329 match (
330 stored.source_timestamp.as_ref(),
331 incoming.target_timestamp.as_ref(),
332 ) {
333 (Some(left), Some(right)) if timestamps_within(left, right, window_ms) => true,
334 _ => matches!(
335 (
336 stored.target_timestamp.as_ref(),
337 incoming.source_timestamp.as_ref()
338 ),
339 (Some(left), Some(right)) if timestamps_within(left, right, window_ms)
340 ),
341 }
342}
343
344fn timestamps_within(left: &BACnetTimeStamp, right: &BACnetTimeStamp, window_ms: u64) -> bool {
349 let window_centiseconds = window_ms / 10;
350 match (left, right) {
351 (BACnetTimeStamp::Time(left), BACnetTimeStamp::Time(right)) => {
352 let (Some(left), Some(right)) = (time_centiseconds(*left), time_centiseconds(*right))
353 else {
354 return false;
355 };
356 let distance = left.abs_diff(right);
357 distance.min(8_640_000 - distance) <= window_centiseconds
358 }
359 (
360 BACnetTimeStamp::DateTime {
361 date: left_date,
362 time: left_time,
363 },
364 BACnetTimeStamp::DateTime {
365 date: right_date,
366 time: right_time,
367 },
368 ) => {
369 let (Some(left), Some(right)) = (
370 datetime_centiseconds(*left_date, *left_time),
371 datetime_centiseconds(*right_date, *right_time),
372 ) else {
373 return false;
374 };
375 left.abs_diff(right) <= window_centiseconds
376 }
377 (BACnetTimeStamp::SequenceNumber(left), BACnetTimeStamp::SequenceNumber(right)) => {
378 left == right
379 }
380 _ => false,
381 }
382}
383
384fn time_centiseconds(time: Time) -> Option<u64> {
385 (time.hour <= 23 && time.minute <= 59 && time.second <= 59 && time.hundredths <= 99).then(
386 || {
387 (((u64::from(time.hour) * 60 + u64::from(time.minute)) * 60 + u64::from(time.second))
388 * 100)
389 + u64::from(time.hundredths)
390 },
391 )
392}
393
394fn datetime_centiseconds(date: Date, time: Time) -> Option<u64> {
395 let year = date.actual_year()?;
396 if !(1..=12).contains(&date.month) || !(1..=7).contains(&date.day_of_week) {
397 return None;
398 }
399 let max_day = days_in_month(year, date.month);
400 if date.day == 0 || date.day > max_day {
401 return None;
402 }
403 let days = days_from_civil(i64::from(year), i64::from(date.month), i64::from(date.day));
404 let expected_day_of_week = (days + 3).rem_euclid(7) as u8 + 1;
405 if date.day_of_week != expected_day_of_week {
406 return None;
407 }
408 let time = time_centiseconds(time)?;
409 let shifted_days = days.checked_add(719_468)?;
410 u64::try_from(shifted_days)
411 .ok()?
412 .checked_mul(8_640_000)?
413 .checked_add(time)
414}
415
416fn days_in_month(year: u16, month: u8) -> u8 {
417 match month {
418 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
419 4 | 6 | 9 | 11 => 30,
420 2 if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) => {
421 29
422 }
423 2 => 28,
424 _ => 0,
425 }
426}
427
428fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
429 let year = year - i64::from(month <= 2);
430 let era = year.div_euclid(400);
431 let year_of_era = year - era * 400;
432 let month_prime = month + if month > 2 { -3 } else { 9 };
433 let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
434 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
435 era * 146_097 + day_of_era - 719_468
436}
437
438fn merge_notification(stored: &mut BACnetAuditNotification, incoming: &BACnetAuditNotification) {
439 let incoming_is_target_report = incoming.target_timestamp.is_some();
440 fill_missing(&mut stored.source_timestamp, &incoming.source_timestamp);
441 fill_missing(&mut stored.target_timestamp, &incoming.target_timestamp);
442 fill_missing(&mut stored.source_object, &incoming.source_object);
443 fill_missing(&mut stored.source_comment, &incoming.source_comment);
444 fill_missing(&mut stored.target_comment, &incoming.target_comment);
445 fill_missing(&mut stored.source_user_id, &incoming.source_user_id);
446 fill_missing(&mut stored.source_user_role, &incoming.source_user_role);
447 fill_missing(&mut stored.target_object, &incoming.target_object);
448 fill_missing(&mut stored.target_property, &incoming.target_property);
449 fill_missing(&mut stored.target_priority, &incoming.target_priority);
450 fill_missing(&mut stored.target_value, &incoming.target_value);
451 fill_missing(&mut stored.result, &incoming.result);
452 if incoming_is_target_report && incoming.current_value.is_some() {
453 stored.current_value.clone_from(&incoming.current_value);
454 } else {
455 fill_missing(&mut stored.current_value, &incoming.current_value);
456 }
457}
458
459fn fill_missing<T: Clone>(stored: &mut Option<T>, incoming: &Option<T>) {
460 if stored.is_none() {
461 stored.clone_from(incoming);
462 }
463}