1#![cfg_attr(not(feature = "std"), allow(dead_code))]
6
7use crate::prelude::*;
8#[cfg(feature = "std")]
9use std::fs::{File, OpenOptions};
10#[cfg(feature = "std")]
11use std::io::{ErrorKind, Read, Write};
12#[cfg(feature = "std")]
13use std::path::{Path, PathBuf};
14
15use bacnet_encoding::constructed::{decode_audit_log_record, encode_audit_log_record};
16use bacnet_types::bitstring::LogStatus;
17use bacnet_types::constructed::{
18 BACnetAuditLogDatum, BACnetAuditLogRecord, BACnetAuditLogRecordResult,
19};
20use bacnet_types::error::Error;
21use bacnet_types::primitives::ObjectIdentifier;
22use bytes::BytesMut;
23
24use crate::durable::crc32;
25
26use super::receipt::{self, CompletedAuditReceipt};
27
28mod receipt_codec;
29
30pub const MAX_AUDIT_RECORDS: u32 = 10_000;
32
33const MAGIC: &[u8; 8] = b"RBALOG01";
34const SCHEMA_VERSION_V1: u16 = 1;
35const SCHEMA_VERSION_V2: u16 = 2;
38const SCHEMA_VERSION: u16 = 3;
41const HEADER_LEN: usize = 8 + 2 + 4 + 8 + 4;
42const TRAILER_LEN: usize = 4;
43const MAX_RECORD_BYTES: usize = 1024 * 1024;
44const MAX_SNAPSHOT_BYTES: usize = 64 * 1024 * 1024;
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct AuditLogSnapshot {
49 pub object_identifier: ObjectIdentifier,
51 pub generation: u64,
53 pub capacity: u32,
56 pub log_enable: bool,
58 pub total_record_count: u64,
60 pub records: Vec<BACnetAuditLogRecordResult>,
62 pub completed_receipts: Vec<CompletedAuditReceipt>,
64}
65
66pub trait AuditLogPersistence: Send + Sync {
72 fn load(&self, expected_object: ObjectIdentifier) -> Result<Option<AuditLogSnapshot>, Error>;
74
75 fn commit(&self, snapshot: &AuditLogSnapshot) -> Result<(), Error>;
77}
78
79#[derive(Clone, Debug)]
87#[cfg(feature = "std")]
88pub struct FileAuditLogPersistence {
89 slot_paths: [PathBuf; 2],
90}
91
92#[cfg(feature = "std")]
93impl FileAuditLogPersistence {
94 pub fn new(base_path: impl AsRef<Path>) -> Result<Self, Error> {
96 let base_path = base_path.as_ref();
97 if base_path.as_os_str().is_empty() {
98 return Err(Error::OutOfRange(
99 "AuditLog persistence base path must not be empty".into(),
100 ));
101 }
102 let base = base_path.as_os_str();
103 let mut slot0 = base.to_os_string();
104 slot0.push(".slot0");
105 let mut slot1 = base.to_os_string();
106 slot1.push(".slot1");
107 Ok(Self {
108 slot_paths: [PathBuf::from(slot0), PathBuf::from(slot1)],
109 })
110 }
111
112 pub fn slot_paths(&self) -> [PathBuf; 2] {
114 self.slot_paths.clone()
115 }
116}
117
118#[cfg(feature = "std")]
119enum SlotState {
120 Missing,
121 Recoverable(Error),
122 Fatal(Error),
123 Valid(AuditLogSnapshot),
124}
125
126#[cfg(feature = "std")]
127enum SlotReadFailure {
128 Recoverable(Error),
129 Fatal(Error),
130}
131
132#[cfg(feature = "std")]
133impl AuditLogPersistence for FileAuditLogPersistence {
134 fn load(&self, expected_object: ObjectIdentifier) -> Result<Option<AuditLogSnapshot>, Error> {
135 let slots = [
136 read_slot(&self.slot_paths[0], expected_object),
137 read_slot(&self.slot_paths[1], expected_object),
138 ];
139 select_newest_slot(slots)
140 }
141
142 fn commit(&self, snapshot: &AuditLogSnapshot) -> Result<(), Error> {
143 let bytes = encode_snapshot(snapshot)?;
144 let path = &self.slot_paths[(snapshot.generation % 2) as usize];
145 if let Some(parent) = path
146 .parent()
147 .filter(|parent| !parent.as_os_str().is_empty())
148 {
149 std::fs::create_dir_all(parent)?;
150 }
151
152 let (mut file, created) = match OpenOptions::new().write(true).create_new(true).open(path) {
153 Ok(file) => (file, true),
154 Err(error) if error.kind() == ErrorKind::AlreadyExists => (
155 OpenOptions::new().write(true).truncate(true).open(path)?,
156 false,
157 ),
158 Err(error) => return Err(error.into()),
159 };
160 if let Err(error) = file.write_all(&bytes) {
161 invalidate_failed_slot(&file);
162 return Err(error.into());
163 }
164 if let Err(error) = file.sync_all() {
165 invalidate_failed_slot(&file);
166 return Err(error.into());
167 }
168 if created {
172 crate::durable::sync_parent_dir(path);
173 }
174 Ok(())
175 }
176}
177
178#[cfg(feature = "std")]
179fn select_newest_slot(slots: [SlotState; 2]) -> Result<Option<AuditLogSnapshot>, Error> {
180 let mut newest: Option<AuditLogSnapshot> = None;
181 let mut present = false;
182 let mut last_error = None;
183
184 for slot in slots {
185 match slot {
186 SlotState::Missing => {}
187 SlotState::Recoverable(error) => {
188 present = true;
189 last_error = Some(error);
190 }
191 SlotState::Fatal(error) => return Err(error),
192 SlotState::Valid(snapshot) => {
193 present = true;
194 match newest.as_ref() {
195 None => newest = Some(snapshot),
196 Some(current) if snapshot.generation > current.generation => {
197 newest = Some(snapshot);
198 }
199 Some(current) if snapshot.generation == current.generation => {
200 if encode_snapshot(&snapshot)? != encode_snapshot(current)? {
201 return Err(Error::Encoding(
202 "AuditLog slots contain divergent snapshots at the same generation"
203 .into(),
204 ));
205 }
206 }
207 Some(_) => {}
208 }
209 }
210 }
211 }
212
213 if newest.is_some() {
214 Ok(newest)
215 } else if present {
216 Err(last_error.unwrap_or_else(|| {
217 Error::Encoding("AuditLog persistence has no valid compatible slot".into())
218 }))
219 } else {
220 Ok(None)
221 }
222}
223
224#[cfg(feature = "std")]
225fn invalidate_failed_slot(file: &File) {
226 let _ = file.set_len(0);
229 let _ = file.sync_all();
230}
231
232#[cfg(feature = "std")]
233fn read_slot(path: &Path, expected_object: ObjectIdentifier) -> SlotState {
234 match read_slot_inner(path, expected_object) {
235 Ok(Some(snapshot)) => SlotState::Valid(snapshot),
236 Ok(None) => SlotState::Missing,
237 Err(SlotReadFailure::Recoverable(error)) => SlotState::Recoverable(error),
238 Err(SlotReadFailure::Fatal(error)) => SlotState::Fatal(error),
239 }
240}
241
242#[cfg(feature = "std")]
243fn read_slot_inner(
244 path: &Path,
245 expected_object: ObjectIdentifier,
246) -> Result<Option<AuditLogSnapshot>, SlotReadFailure> {
247 let file = match File::open(path) {
248 Ok(file) => file,
249 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
250 Err(error) => return Err(SlotReadFailure::Fatal(error.into())),
251 };
252 let metadata = file
253 .metadata()
254 .map_err(|error| SlotReadFailure::Fatal(error.into()))?;
255 let length = usize::try_from(metadata.len()).map_err(|_| {
256 SlotReadFailure::Recoverable(Error::OutOfRange(
257 "AuditLog snapshot length exceeds usize".into(),
258 ))
259 })?;
260 if length > MAX_SNAPSHOT_BYTES {
261 return Err(SlotReadFailure::Recoverable(Error::OutOfRange(format!(
262 "AuditLog snapshot length {length} exceeds {MAX_SNAPSHOT_BYTES}"
263 ))));
264 }
265 let mut data = Vec::with_capacity(length.min(MAX_SNAPSHOT_BYTES + 1));
266 file.take((MAX_SNAPSHOT_BYTES + 1) as u64)
267 .read_to_end(&mut data)
268 .map_err(|error| SlotReadFailure::Fatal(error.into()))?;
269 if data.len() > MAX_SNAPSHOT_BYTES {
270 return Err(SlotReadFailure::Recoverable(Error::OutOfRange(format!(
271 "AuditLog snapshot length exceeds {MAX_SNAPSHOT_BYTES}"
272 ))));
273 }
274 if data.len() != length {
275 return Err(SlotReadFailure::Fatal(Error::Encoding(
276 "AuditLog snapshot changed while being read".into(),
277 )));
278 }
279
280 verify_snapshot_integrity(&data).map_err(SlotReadFailure::Recoverable)?;
281 let version = u16::from_be_bytes(data[8..10].try_into().unwrap());
282 if !matches!(
283 version,
284 SCHEMA_VERSION_V1 | SCHEMA_VERSION_V2 | SCHEMA_VERSION
285 ) {
286 return Err(SlotReadFailure::Fatal(Error::Encoding(format!(
287 "AuditLog snapshot schema version {version} is unsupported"
288 ))));
289 }
290 let object_identifier =
291 ObjectIdentifier::decode(&data[10..14]).map_err(SlotReadFailure::Recoverable)?;
292 if object_identifier != expected_object {
293 return Err(SlotReadFailure::Fatal(Error::Encoding(
294 "AuditLog snapshot object identity does not match".into(),
295 )));
296 }
297 decode_verified_snapshot(&data, object_identifier, version)
298 .map(Some)
299 .map_err(SlotReadFailure::Recoverable)
300}
301
302fn verify_snapshot_integrity(data: &[u8]) -> Result<(), Error> {
303 if data.len() > MAX_SNAPSHOT_BYTES {
304 return Err(Error::OutOfRange(format!(
305 "AuditLog snapshot length {} exceeds {MAX_SNAPSHOT_BYTES}",
306 data.len()
307 )));
308 }
309 if data.len() < HEADER_LEN + TRAILER_LEN {
310 return Err(Error::Encoding("AuditLog snapshot is truncated".into()));
311 }
312 if &data[..8] != MAGIC {
313 return Err(Error::Encoding("AuditLog snapshot magic is invalid".into()));
314 }
315 let payload_len = u32::from_be_bytes(data[22..26].try_into().unwrap()) as usize;
316 let expected_len = HEADER_LEN
317 .checked_add(payload_len)
318 .and_then(|length| length.checked_add(TRAILER_LEN))
319 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
320 if expected_len != data.len() {
321 return Err(Error::Encoding(
322 "AuditLog snapshot declared length does not match file length".into(),
323 ));
324 }
325 let checksum_start = data.len() - TRAILER_LEN;
326 let stored_checksum = u32::from_be_bytes(data[checksum_start..].try_into().unwrap());
327 if crc32(&data[..checksum_start]) != stored_checksum {
328 return Err(Error::Encoding(
329 "AuditLog snapshot checksum is invalid".into(),
330 ));
331 }
332 Ok(())
333}
334
335fn encode_snapshot(snapshot: &AuditLogSnapshot) -> Result<Vec<u8>, Error> {
336 encode_snapshot_for_version(snapshot, SCHEMA_VERSION)
337}
338
339#[cfg(all(test, feature = "std"))]
340pub(super) fn encode_snapshot_v1(snapshot: &AuditLogSnapshot) -> Result<Vec<u8>, Error> {
341 if !snapshot.completed_receipts.is_empty() {
342 return Err(Error::Encoding(
343 "AuditLog schema-v1 snapshot cannot contain completed receipts".into(),
344 ));
345 }
346 encode_snapshot_for_version(snapshot, SCHEMA_VERSION_V1)
347}
348
349#[cfg(all(test, feature = "std"))]
353pub(super) fn encode_snapshot_v2(snapshot: &AuditLogSnapshot) -> Result<Vec<u8>, Error> {
354 encode_snapshot_for_version(snapshot, SCHEMA_VERSION_V2)
355}
356
357fn encode_snapshot_for_version(
358 snapshot: &AuditLogSnapshot,
359 version: u16,
360) -> Result<Vec<u8>, Error> {
361 validate_snapshot(snapshot)?;
362 let mut payload = Vec::new();
363 payload.extend_from_slice(&snapshot.capacity.to_be_bytes());
364 payload.push(u8::from(snapshot.log_enable));
365 payload.extend_from_slice(&snapshot.total_record_count.to_be_bytes());
366 payload.extend_from_slice(&(snapshot.records.len() as u32).to_be_bytes());
367 for result in &snapshot.records {
368 validate_record_dynamic_size(&result.record)?;
369 let mut record = BytesMut::new();
370 encode_audit_log_record(&result.record, &mut record)?;
371 if record.len() > MAX_RECORD_BYTES {
372 return Err(Error::OutOfRange(format!(
373 "AuditLog record length {} exceeds {MAX_RECORD_BYTES}",
374 record.len()
375 )));
376 }
377 let projected = HEADER_LEN
378 .checked_add(payload.len())
379 .and_then(|length| length.checked_add(8 + 4 + record.len()))
380 .and_then(|length| length.checked_add(TRAILER_LEN))
381 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
382 if projected > MAX_SNAPSHOT_BYTES {
383 return Err(Error::OutOfRange(format!(
384 "AuditLog snapshot length exceeds {MAX_SNAPSHOT_BYTES}"
385 )));
386 }
387 payload.extend_from_slice(&result.sequence_number.to_be_bytes());
388 payload.extend_from_slice(&(record.len() as u32).to_be_bytes());
389 payload.extend_from_slice(&record);
390 }
391 if version != SCHEMA_VERSION_V1 {
392 receipt_codec::encode(&snapshot.completed_receipts, &mut payload)?;
393 }
394 let total_len = HEADER_LEN
395 .checked_add(payload.len())
396 .and_then(|length| length.checked_add(TRAILER_LEN))
397 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
398 if total_len > MAX_SNAPSHOT_BYTES {
399 return Err(Error::OutOfRange(format!(
400 "AuditLog snapshot length {total_len} exceeds {MAX_SNAPSHOT_BYTES}"
401 )));
402 }
403
404 let mut data = Vec::with_capacity(total_len);
405 data.extend_from_slice(MAGIC);
406 data.extend_from_slice(&version.to_be_bytes());
407 data.extend_from_slice(&snapshot.object_identifier.encode());
408 data.extend_from_slice(&snapshot.generation.to_be_bytes());
409 data.extend_from_slice(&(payload.len() as u32).to_be_bytes());
410 data.extend_from_slice(&payload);
411 let checksum = crc32(&data);
412 data.extend_from_slice(&checksum.to_be_bytes());
413 Ok(data)
414}
415
416fn decode_verified_snapshot(
417 data: &[u8],
418 object_identifier: ObjectIdentifier,
419 version: u16,
420) -> Result<AuditLogSnapshot, Error> {
421 let generation = u64::from_be_bytes(data[14..22].try_into().unwrap());
422 if generation == 0 {
423 return Err(Error::Encoding(
424 "AuditLog snapshot generation must be nonzero".into(),
425 ));
426 }
427 let checksum_start = data.len() - TRAILER_LEN;
428 let payload = &data[HEADER_LEN..checksum_start];
429 let mut offset = 0;
430 let capacity = take_u32(payload, &mut offset, "capacity")?;
431 if capacity > MAX_AUDIT_RECORDS {
432 return Err(Error::OutOfRange(format!(
433 "AuditLog persisted capacity {capacity} exceeds {MAX_AUDIT_RECORDS}"
434 )));
435 }
436 let log_enable = match take(payload, &mut offset, 1, "log-enable")?[0] {
437 0 => false,
438 1 => true,
439 _ => {
440 return Err(Error::Encoding(
441 "AuditLog persisted log-enable is not Boolean".into(),
442 ));
443 }
444 };
445 let total_record_count = take_u64(payload, &mut offset, "total-record-count")?;
446 let record_count = take_u32(payload, &mut offset, "record-count")?;
447 if record_count > capacity || record_count > MAX_AUDIT_RECORDS {
448 return Err(Error::OutOfRange(format!(
449 "AuditLog persisted record count {record_count} exceeds capacity {capacity}"
450 )));
451 }
452 let mut records = Vec::with_capacity(record_count as usize);
453 for _ in 0..record_count {
454 let sequence_number = take_u64(payload, &mut offset, "sequence-number")?;
455 let record_len = take_u32(payload, &mut offset, "record-length")? as usize;
456 if record_len > MAX_RECORD_BYTES {
457 return Err(Error::OutOfRange(format!(
458 "AuditLog persisted record length {record_len} exceeds {MAX_RECORD_BYTES}"
459 )));
460 }
461 let record_data = take(payload, &mut offset, record_len, "record")?;
462 let mut record = decode_audit_log_record(record_data)?;
463 if version != SCHEMA_VERSION {
464 restore_pre_v3_log_status(&mut record);
465 }
466 records.push(BACnetAuditLogRecordResult {
467 sequence_number,
468 record,
469 });
470 }
471 let completed_receipts = if version == SCHEMA_VERSION_V1 {
472 Vec::new()
473 } else {
474 receipt_codec::decode(payload, &mut offset)?
475 };
476 if offset != payload.len() {
477 return Err(Error::Encoding(
478 "AuditLog snapshot payload has trailing bytes".into(),
479 ));
480 }
481
482 let snapshot = AuditLogSnapshot {
483 object_identifier,
484 generation,
485 capacity,
486 log_enable,
487 total_record_count,
488 records,
489 completed_receipts,
490 };
491 validate_snapshot(&snapshot)?;
492 Ok(snapshot)
493}
494
495fn restore_pre_v3_log_status(record: &mut BACnetAuditLogRecord) {
504 if let BACnetAuditLogDatum::LogStatus(status) = &mut record.datum {
505 *status = LogStatus::from_bits_truncate(status.bits().reverse_bits() >> 5);
506 }
507}
508
509pub(super) fn validate_snapshot(snapshot: &AuditLogSnapshot) -> Result<(), Error> {
510 if snapshot.generation == 0 {
511 return Err(Error::OutOfRange(
512 "AuditLog snapshot generation must be nonzero".into(),
513 ));
514 }
515 if snapshot.capacity > MAX_AUDIT_RECORDS {
516 return Err(Error::OutOfRange(format!(
517 "AuditLog capacity {} exceeds {MAX_AUDIT_RECORDS}",
518 snapshot.capacity
519 )));
520 }
521 if snapshot.records.len() > snapshot.capacity as usize {
522 return Err(Error::OutOfRange(format!(
523 "AuditLog record count {} exceeds capacity {}",
524 snapshot.records.len(),
525 snapshot.capacity
526 )));
527 }
528 receipt::validate_receipts(&snapshot.completed_receipts)?;
529 if (snapshot.total_record_count == 0 && !snapshot.records.is_empty())
532 || snapshot
533 .records
534 .last()
535 .is_some_and(|last| last.sequence_number != snapshot.total_record_count)
536 {
537 return Err(Error::Encoding(
538 "AuditLog snapshot record identities do not match Total_Record_Count".into(),
539 ));
540 }
541 for pair in snapshot.records.windows(2) {
542 let expected = if pair[0].sequence_number == u64::MAX {
543 1
544 } else {
545 pair[0].sequence_number + 1
546 };
547 if pair[0].sequence_number == 0 || pair[1].sequence_number != expected {
548 return Err(Error::Encoding(
549 "AuditLog snapshot sequence identities are not contiguous".into(),
550 ));
551 }
552 }
553 if snapshot
554 .records
555 .first()
556 .is_some_and(|first| first.sequence_number == 0)
557 {
558 return Err(Error::Encoding(
559 "AuditLog snapshot sequence identities must be nonzero".into(),
560 ));
561 }
562
563 let mut projected_len = HEADER_LEN
564 .checked_add(4 + 1 + 8 + 4)
565 .and_then(|length| length.checked_add(TRAILER_LEN))
566 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
567 for result in &snapshot.records {
568 let encoded_len = validate_record(&result.record)?;
569 projected_len = projected_len
570 .checked_add(8 + 4 + encoded_len)
571 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
572 if projected_len > MAX_SNAPSHOT_BYTES {
573 return Err(Error::OutOfRange(format!(
574 "AuditLog snapshot length exceeds {MAX_SNAPSHOT_BYTES}"
575 )));
576 }
577 }
578 let receipt_len = if snapshot.completed_receipts.is_empty() {
582 0
583 } else {
584 receipt_codec::encoded_len(&snapshot.completed_receipts)?
585 };
586 projected_len = projected_len
587 .checked_add(receipt_len)
588 .ok_or_else(|| Error::OutOfRange("AuditLog snapshot length overflow".into()))?;
589 if projected_len > MAX_SNAPSHOT_BYTES {
590 return Err(Error::OutOfRange(format!(
591 "AuditLog snapshot length exceeds {MAX_SNAPSHOT_BYTES}"
592 )));
593 }
594 Ok(())
595}
596
597pub(super) fn validate_record(
598 record: &bacnet_types::constructed::BACnetAuditLogRecord,
599) -> Result<usize, Error> {
600 validate_record_dynamic_size(record)?;
601 let mut encoded = BytesMut::new();
602 encode_audit_log_record(record, &mut encoded)?;
603 if encoded.len() > MAX_RECORD_BYTES {
604 return Err(Error::OutOfRange(format!(
605 "AuditLog record length {} exceeds {MAX_RECORD_BYTES}",
606 encoded.len()
607 )));
608 }
609 Ok(encoded.len())
610}
611
612fn validate_record_dynamic_size(
613 record: &bacnet_types::constructed::BACnetAuditLogRecord,
614) -> Result<(), Error> {
615 let bacnet_types::constructed::BACnetAuditLogDatum::AuditNotification(notification) =
616 &record.datum
617 else {
618 return Ok(());
619 };
620 let recipient_size = |recipient: &bacnet_types::constructed::BACnetRecipient| match recipient {
621 bacnet_types::constructed::BACnetRecipient::Device(_) => 4,
622 bacnet_types::constructed::BACnetRecipient::Address(address) => address.mac_address.len(),
623 };
624 let lengths = [
625 recipient_size(¬ification.source_device),
626 recipient_size(¬ification.target_device),
627 notification.source_comment.as_ref().map_or(0, String::len),
628 notification.target_comment.as_ref().map_or(0, String::len),
629 notification.target_value.as_ref().map_or(0, Vec::len),
630 notification.current_value.as_ref().map_or(0, Vec::len),
631 ];
632 let dynamic = lengths.into_iter().try_fold(0usize, |sum, length| {
633 sum.checked_add(length)
634 .ok_or_else(|| Error::OutOfRange("AuditLog record length overflow".into()))
635 })?;
636 if dynamic > MAX_RECORD_BYTES {
637 return Err(Error::OutOfRange(format!(
638 "AuditLog record dynamic length {dynamic} exceeds {MAX_RECORD_BYTES}"
639 )));
640 }
641 Ok(())
642}
643
644fn take<'a>(
645 data: &'a [u8],
646 offset: &mut usize,
647 length: usize,
648 field: &str,
649) -> Result<&'a [u8], Error> {
650 let end = offset
651 .checked_add(length)
652 .ok_or_else(|| Error::OutOfRange(format!("AuditLog {field} length overflow")))?;
653 if end > data.len() {
654 return Err(Error::Encoding(format!(
655 "AuditLog snapshot {field} is truncated"
656 )));
657 }
658 let value = &data[*offset..end];
659 *offset = end;
660 Ok(value)
661}
662
663fn take_u32(data: &[u8], offset: &mut usize, field: &str) -> Result<u32, Error> {
664 Ok(u32::from_be_bytes(
665 take(data, offset, 4, field)?.try_into().unwrap(),
666 ))
667}
668
669fn take_u64(data: &[u8], offset: &mut usize, field: &str) -> Result<u64, Error> {
670 Ok(u64::from_be_bytes(
671 take(data, offset, 8, field)?.try_into().unwrap(),
672 ))
673}
674
675#[cfg(all(test, feature = "std"))]
676mod tests {
677 use super::*;
678 use bacnet_types::enums::ObjectType;
679
680 fn snapshot() -> AuditLogSnapshot {
681 AuditLogSnapshot {
682 object_identifier: ObjectIdentifier::new(ObjectType::AUDIT_LOG, 1).unwrap(),
683 generation: 1,
684 capacity: 1,
685 log_enable: true,
686 total_record_count: 0,
687 records: Vec::new(),
688 completed_receipts: Vec::new(),
689 }
690 }
691
692 fn io_failure() -> SlotState {
693 SlotState::Fatal(Error::Transport(std::io::Error::other(
694 "injected slot read failure",
695 )))
696 }
697
698 #[test]
699 fn audit_slot_selection_never_masks_io_failure_with_valid_peer() {
700 assert!(matches!(
701 select_newest_slot([io_failure(), SlotState::Valid(snapshot())]),
702 Err(Error::Transport(_))
703 ));
704 assert!(matches!(
705 select_newest_slot([SlotState::Valid(snapshot()), io_failure()]),
706 Err(Error::Transport(_))
707 ));
708 }
709
710 #[test]
711 fn audit_slot_selection_requires_identical_same_generation_snapshots() {
712 let expected = snapshot();
713 let selected = select_newest_slot([
714 SlotState::Valid(expected.clone()),
715 SlotState::Valid(expected.clone()),
716 ])
717 .unwrap()
718 .unwrap();
719 assert_eq!(selected, expected);
720
721 let mut divergent = snapshot();
722 divergent.log_enable = false;
723 assert!(matches!(
724 select_newest_slot([
725 SlotState::Valid(snapshot()),
726 SlotState::Valid(divergent),
727 ]),
728 Err(Error::Encoding(message)) if message.contains("same generation")
729 ));
730 }
731}