1use crate::{Blob, BufMut, Error, IoBuf, IoBufMut, ReadOptions};
46#[cfg(any(test, feature = "test-utils"))]
47use crate::{Storage, WriteOptions};
48use commonware_codec::{Buf, Copying, EncodeFixed, FixedSize, Read as CodecRead, ReadExt, Write};
49use commonware_cryptography::{Crc32, crc32};
50use std::num::NonZeroU16;
51
52mod cache;
53mod read;
54mod sealed;
55mod tip;
56mod view;
57mod writer;
58
59pub use cache::CacheRef;
60pub use read::Replay;
61pub use sealed::Sealed;
62use tracing::{debug, error};
63pub use writer::{Append, Recovering, Recovery, Writer};
64
65pub const CHECKSUM_SIZE: u64 = Checksum::SIZE as u64;
67
68const CHECKSUM_SLOT_LEN_SIZE: usize = u16::SIZE;
69const CHECKSUM_SLOT_SIZE: usize = CHECKSUM_SLOT_LEN_SIZE + crc32::Digest::SIZE;
70
71pub const fn page_size(physical_page_size: u32) -> NonZeroU16 {
85 assert!(
86 physical_page_size.is_power_of_two(),
87 "physical page size must be a power of two"
88 );
89 assert!(
90 physical_page_size as u64 > CHECKSUM_SIZE,
91 "physical page size must exceed the CRC record size"
92 );
93 let logical = physical_page_size as u64 - CHECKSUM_SIZE;
94 assert!(
95 logical <= u16::MAX as u64,
96 "logical page size must fit in a u16"
97 );
98 match NonZeroU16::new(logical as u16) {
99 Some(size) => size,
100 None => unreachable!(),
101 }
102}
103
104#[cfg(test)]
107pub(crate) fn validate_page_for_tests(page: &[u8]) -> bool {
108 Checksum::validate_page(page).is_some()
109}
110
111#[cfg(any(test, feature = "test-utils"))]
119pub fn page_len(page: &[u8], logical_page_size: usize) -> Option<usize> {
120 let footer = page.get(logical_page_size..)?;
121 if footer.len() != CHECKSUM_SIZE as usize {
122 return None;
123 }
124 let slots = [
125 (
126 u16::from_be_bytes(footer[0..2].try_into().unwrap()) as usize,
127 u32::from_be_bytes(footer[2..6].try_into().unwrap()),
128 ),
129 (
130 u16::from_be_bytes(footer[6..8].try_into().unwrap()) as usize,
131 u32::from_be_bytes(footer[8..12].try_into().unwrap()),
132 ),
133 ];
134 let authoritative = usize::from(slots[1].0 > slots[0].0);
135 for slot in [authoritative, authoritative ^ 1] {
136 let (len, checksum) = slots[slot];
137 if len > 0 && len <= logical_page_size && Crc32::checksum(&page[..len]) == checksum {
138 return Some(len);
139 }
140 }
141 None
142}
143
144#[cfg(any(test, feature = "test-utils"))]
148pub async fn corrupt_page(
149 storage: &impl Storage,
150 partition: &str,
151 name: &[u8],
152 page: u64,
153 logical_page_size: u64,
154) {
155 let physical_page_size = logical_page_size + CHECKSUM_SIZE;
157 let offset = page * physical_page_size;
158 let (blob, size) = storage.open(partition, name).await.unwrap();
159
160 assert!(
164 offset
165 .checked_add(physical_page_size * 2)
166 .is_some_and(|end| end <= size),
167 "corruption target must be an interior page"
168 );
169 let byte = blob
170 .read_at(offset, 1, ReadOptions::default())
171 .await
172 .unwrap()
173 .coalesce();
174 blob.write_at(
175 offset,
176 vec![byte.as_ref()[0] ^ 0xFF],
177 WriteOptions::default(),
178 )
179 .await
180 .unwrap();
181 blob.sync().await.unwrap();
182}
183
184fn validate_read_ranges(
191 buf_len: usize,
192 ranges: impl Iterator<Item = (u64, usize)>,
193 size: u64,
194) -> Result<(), Error> {
195 let mut expected_len = 0usize;
196 let mut previous_end = None;
197 for (offset, len) in ranges {
198 expected_len = expected_len
199 .checked_add(len)
200 .expect("buf must hold one slot per range totaling its length");
201 let end = offset
202 .checked_add(len as u64)
203 .ok_or(Error::OffsetOverflow)?;
204 if let Some(previous_end) = previous_end {
205 assert!(
206 offset >= previous_end,
207 "ranges must be sorted and non-overlapping"
208 );
209 }
210 if end > size {
211 return Err(Error::BlobInsufficientLength);
212 }
213 previous_end = Some(end);
214 }
215 assert_eq!(
216 buf_len, expected_len,
217 "buf must hold one slot per range totaling its length"
218 );
219 Ok(())
220}
221
222fn physical_page(page_num: u64, page_size: u64) -> Result<(u64, usize), Error> {
225 let physical_page_size = page_size
226 .checked_add(CHECKSUM_SIZE)
227 .ok_or(Error::OffsetOverflow)?;
228 let physical_page_start = page_num
229 .checked_mul(physical_page_size)
230 .ok_or(Error::OffsetOverflow)?;
231 let len = usize::try_from(physical_page_size).map_err(|_| Error::OffsetOverflow)?;
232 Ok((physical_page_start, len))
233}
234
235fn validate_physical_page(page: IoBufMut) -> Result<(IoBuf, ActiveChecksum), Error> {
237 let Some(checksum) = Checksum::validate_page(page.as_ref()) else {
238 return Err(Error::InvalidChecksum);
239 };
240 Ok((page.freeze().slice(..usize::from(checksum.len)), checksum))
241}
242
243async fn get_page_with_checksum_from_blob(
245 blob: &impl Blob,
246 page_num: u64,
247 page_size: u64,
248 read_options: ReadOptions,
249) -> Result<(IoBuf, ActiveChecksum), Error> {
250 let (start, len) = physical_page(page_num, page_size)?;
251 let page = blob.read_at(start, len, read_options).await?.coalesce();
252 validate_physical_page(page)
253}
254
255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257enum Slot {
258 First,
259 Second,
260}
261
262impl Slot {
263 const fn offset(self) -> usize {
265 match self {
266 Self::First => 0,
267 Self::Second => CHECKSUM_SLOT_SIZE,
268 }
269 }
270
271 const fn other(self) -> Self {
273 match self {
274 Self::First => Self::Second,
275 Self::Second => Self::First,
276 }
277 }
278}
279
280#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282struct ActiveChecksum {
283 slot: Slot,
284 len: u16,
285 crc: u32,
286}
287
288impl ActiveChecksum {
289 const fn new(slot: Slot, len: u16, crc: u32) -> Self {
290 Self { slot, len, crc }
291 }
292}
293
294struct Checksum {
299 len1: u16,
300 crc1: u32,
301 len2: u16,
302 crc2: u32,
303}
304
305impl Checksum {
306 const fn new(len: u16, crc: u32) -> Self {
309 Self {
310 len1: len,
311 crc1: crc,
312 len2: 0,
313 crc2: 0,
314 }
315 }
316
317 const fn authoritative(&self) -> Slot {
319 if self.len1 >= self.len2 {
320 Slot::First
321 } else {
322 Slot::Second
323 }
324 }
325
326 fn validate_page(buf: &[u8]) -> Option<ActiveChecksum> {
329 let physical_page_size = buf.len() as u64;
330 if physical_page_size < CHECKSUM_SIZE {
331 error!(
332 physical_page_size,
333 required = CHECKSUM_SIZE,
334 "read page smaller than CRC record"
335 );
336 return None;
337 }
338
339 let crc_start_idx = (physical_page_size - CHECKSUM_SIZE) as usize;
342 let mut crc_bytes = Copying(&buf[crc_start_idx..]);
343 let crc_record = Self::read(&mut crc_bytes).expect("CRC record read should not fail");
344
345 let authoritative = crc_record.authoritative();
348 if let Some(checksum) = crc_record.validate_slot(authoritative, buf, crc_start_idx) {
349 return Some(checksum);
350 }
351
352 debug!("Invalid authoritative CRC, using fallback CRC");
355 let checksum = crc_record.validate_slot(authoritative.other(), buf, crc_start_idx);
356 if checksum.is_none() {
357 debug!("Invalid fallback CRC");
358 }
359 checksum
360 }
361
362 fn validate_slot(
364 &self,
365 slot: Slot,
366 buf: &[u8],
367 crc_start_idx: usize,
368 ) -> Option<ActiveChecksum> {
369 let (len, crc) = self.get_slot(slot);
370 let len_usize = len as usize;
371
372 if len_usize == 0 {
375 return None;
376 }
377
378 if len_usize > crc_start_idx {
380 return None;
381 }
382
383 if Crc32::checksum(&buf[..len_usize]) != crc {
385 return None;
386 }
387 Some(ActiveChecksum::new(slot, len, crc))
388 }
389
390 const fn get_slot(&self, slot: Slot) -> (u16, u32) {
392 match slot {
393 Slot::First => (self.len1, self.crc1),
394 Slot::Second => (self.len2, self.crc2),
395 }
396 }
397
398 fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
400 self.encode_fixed()
401 }
402
403 fn slot_bytes(len: u16, crc: u32) -> [u8; CHECKSUM_SLOT_SIZE] {
408 let mut bytes = [0; CHECKSUM_SLOT_SIZE];
409 let mut buf = bytes.as_mut_slice();
410 len.write(&mut buf);
411 crc.write(&mut buf);
412 bytes
413 }
414
415 fn slot_len_bytes(len: u16) -> [u8; CHECKSUM_SLOT_LEN_SIZE] {
423 let mut bytes = [0; CHECKSUM_SLOT_LEN_SIZE];
424 let mut buf = bytes.as_mut_slice();
425 len.write(&mut buf);
426 bytes
427 }
428}
429
430impl Write for Checksum {
431 fn write(&self, buf: &mut impl BufMut) {
432 self.len1.write(buf);
433 self.crc1.write(buf);
434 self.len2.write(buf);
435 self.crc2.write(buf);
436 }
437}
438
439impl CodecRead for Checksum {
440 type Cfg = ();
441
442 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
443 Ok(Self {
444 len1: u16::read(buf)?,
445 crc1: u32::read(buf)?,
446 len2: u16::read(buf)?,
447 crc2: u32::read(buf)?,
448 })
449 }
450}
451
452impl FixedSize for Checksum {
453 const SIZE: usize = 2 * u16::SIZE + 2 * crc32::Digest::SIZE;
454}
455
456#[cfg(feature = "arbitrary")]
457impl arbitrary::Arbitrary<'_> for Checksum {
458 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
459 Ok(Self {
460 len1: u.arbitrary()?,
461 crc1: u.arbitrary()?,
462 len2: u.arbitrary()?,
463 crc2: u.arbitrary()?,
464 })
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use rstest::rstest;
472
473 #[test]
474 #[should_panic(expected = "corruption target must be an interior page")]
475 fn test_corrupt_page_rejects_short_blob() {
476 use crate::Runner as _;
477 crate::deterministic::Runner::default().start(|context| async move {
478 corrupt_page(&context, "short-blob", b"blob", 0, 64).await;
479 });
480 }
481
482 enum ValidationExpectation {
483 Ok,
484 OffsetOverflow,
485 BlobInsufficientLength,
486 }
487
488 #[rstest]
489 #[case::ok(12, vec![(0, 4), (4, 8)], 16, ValidationExpectation::Ok)]
490 #[case::empty_ranges_are_a_noop(0, vec![], 0, ValidationExpectation::Ok)]
491 #[case::zero_length_range(4, vec![(0, 0), (0, 4)], 16, ValidationExpectation::Ok)]
492 #[case::offset_overflow(4, vec![(u64::MAX, 4)], 16, ValidationExpectation::OffsetOverflow)]
493 #[case::insufficient_length(4, vec![(14, 4)], 16, ValidationExpectation::BlobInsufficientLength)]
494 #[case::range_may_end_exactly_at_logical_size(4, vec![(12, 4)], 16, ValidationExpectation::Ok)]
495 fn test_validate_read_ranges(
496 #[case] buf_len: usize,
497 #[case] ranges: Vec<(u64, usize)>,
498 #[case] size: u64,
499 #[case] expected: ValidationExpectation,
500 ) {
501 let result = validate_read_ranges(buf_len, ranges.iter().copied(), size);
502
503 match expected {
504 ValidationExpectation::Ok => assert!(result.is_ok()),
505 ValidationExpectation::OffsetOverflow => {
506 assert!(matches!(result, Err(Error::OffsetOverflow)))
507 }
508 ValidationExpectation::BlobInsufficientLength => {
509 assert!(matches!(result, Err(Error::BlobInsufficientLength)))
510 }
511 }
512 }
513
514 #[test]
515 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
516 fn test_validate_read_ranges_rejects_buffer_len_mismatch() {
517 let _ = validate_read_ranges(7, [(0, 4), (4, 4)].into_iter(), 16);
518 }
519
520 #[test]
521 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
522 fn test_validate_read_ranges_rejects_overlapping_ranges() {
523 let _ = validate_read_ranges(8, [(0, 4), (2, 4)].into_iter(), 16);
524 }
525
526 #[test]
527 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
528 fn test_validate_read_ranges_rejects_unsorted_ranges() {
529 let _ = validate_read_ranges(8, [(8, 4), (4, 4)].into_iter(), 16);
530 }
531
532 #[test]
533 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
534 fn test_validate_read_ranges_rejects_length_overflow() {
535 let _ = validate_read_ranges(
536 usize::MAX,
537 [(0, usize::MAX), (u64::MAX, 1)].into_iter(),
538 u64::MAX,
539 );
540 }
541
542 #[test]
543 fn test_crc_record_encode_read_roundtrip() {
544 let record = Checksum {
545 len1: 0x1234,
546 crc1: 0xAABBCCDD,
547 len2: 0x5678,
548 crc2: 0x11223344,
549 };
550
551 let bytes = record.to_bytes();
552 let restored = Checksum::read(&mut Copying(&bytes)).unwrap();
553
554 assert_eq!(restored.len1, 0x1234);
555 assert_eq!(restored.crc1, 0xAABBCCDD);
556 assert_eq!(restored.len2, 0x5678);
557 assert_eq!(restored.crc2, 0x11223344);
558 }
559
560 #[test]
561 fn test_crc_record_encoding() {
562 let record = Checksum {
563 len1: 0x0102,
564 crc1: 0x03040506,
565 len2: 0x0708,
566 crc2: 0x090A0B0C,
567 };
568
569 let bytes = record.to_bytes();
570 assert_eq!(
572 bytes,
573 [
574 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C
575 ]
576 );
577 }
578
579 #[test]
580 fn test_crc_record_authoritative_len1_larger() {
581 let record = Checksum {
582 len1: 200,
583 crc1: 0xAAAAAAAA,
584 len2: 100,
585 crc2: 0xBBBBBBBB,
586 };
587
588 assert_eq!(record.authoritative(), Slot::First);
589 }
590
591 #[test]
592 fn test_crc_record_authoritative_len2_larger() {
593 let record = Checksum {
594 len1: 100,
595 crc1: 0xAAAAAAAA,
596 len2: 200,
597 crc2: 0xBBBBBBBB,
598 };
599
600 assert_eq!(record.authoritative(), Slot::Second);
601 }
602
603 #[test]
604 fn test_crc_record_authoritative_equal_lengths() {
605 let record = Checksum {
607 len1: 100,
608 crc1: 0xAAAAAAAA,
609 len2: 100,
610 crc2: 0xBBBBBBBB,
611 };
612
613 assert_eq!(record.authoritative(), Slot::First);
614 }
615
616 #[test]
617 fn test_validate_page_valid() {
618 let page_size = 64usize;
619 let physical_page_size = page_size + Checksum::SIZE;
620 let mut page = vec![0u8; physical_page_size];
621
622 let data = b"hello world";
624 page[..data.len()].copy_from_slice(data);
625
626 let crc = Crc32::checksum(&page[..data.len()]);
628 let record = Checksum::new(data.len() as u16, crc);
629
630 let crc_start = physical_page_size - Checksum::SIZE;
632 page[crc_start..].copy_from_slice(&record.to_bytes());
633
634 let validated = Checksum::validate_page(&page);
636 assert!(validated.is_some());
637 assert_eq!(validated.unwrap().len as usize, data.len());
638 }
639
640 #[test]
641 fn test_validate_page_invalid_crc() {
642 let page_size = 64usize;
643 let physical_page_size = page_size + Checksum::SIZE;
644 let mut page = vec![0u8; physical_page_size];
645
646 let data = b"hello world";
648 page[..data.len()].copy_from_slice(data);
649
650 let wrong_crc = 0xBADBADBA;
652 let record = Checksum::new(data.len() as u16, wrong_crc);
653
654 let crc_start = physical_page_size - Checksum::SIZE;
655 page[crc_start..].copy_from_slice(&record.to_bytes());
656
657 let validated = Checksum::validate_page(&page);
659 assert!(validated.is_none());
660 }
661
662 #[test]
663 fn test_validate_page_corrupted_data() {
664 let page_size = 64usize;
665 let physical_page_size = page_size + Checksum::SIZE;
666 let mut page = vec![0u8; physical_page_size];
667
668 let data = b"hello world";
670 page[..data.len()].copy_from_slice(data);
671 let crc = Crc32::checksum(&page[..data.len()]);
672 let record = Checksum::new(data.len() as u16, crc);
673
674 let crc_start = physical_page_size - Checksum::SIZE;
675 page[crc_start..].copy_from_slice(&record.to_bytes());
676
677 page[0] = 0xFF;
679
680 let validated = Checksum::validate_page(&page);
682 assert!(validated.is_none());
683 }
684
685 #[test]
686 fn test_validate_page_uses_larger_len() {
687 let page_size = 64usize;
688 let physical_page_size = page_size + Checksum::SIZE;
689 let mut page = vec![0u8; physical_page_size];
690
691 let data = b"hello world, this is longer";
693 page[..data.len()].copy_from_slice(data);
694 let crc = Crc32::checksum(&page[..data.len()]);
695
696 let record = Checksum {
698 len1: 5,
699 crc1: 0xDEADBEEF, len2: data.len() as u16,
701 crc2: crc,
702 };
703
704 let crc_start = physical_page_size - Checksum::SIZE;
705 page[crc_start..].copy_from_slice(&record.to_bytes());
706
707 let validated = Checksum::validate_page(&page);
709 assert!(validated.is_some());
710 assert_eq!(validated.unwrap().len as usize, data.len());
711 }
712
713 #[test]
714 fn test_validate_page_uses_fallback() {
715 let page_size = 64usize;
716 let physical_page_size = page_size + Checksum::SIZE;
717 let mut page = vec![0u8; physical_page_size];
718
719 let data = b"fallback data";
721 page[..data.len()].copy_from_slice(data);
722 let valid_crc = Crc32::checksum(&page[..data.len()]);
723 let valid_len = data.len() as u16;
724
725 let record = Checksum {
729 len1: valid_len + 10, crc1: 0xBAD1DEA, len2: valid_len, crc2: valid_crc, };
734
735 let crc_start = physical_page_size - Checksum::SIZE;
736 page[crc_start..].copy_from_slice(&record.to_bytes());
737
738 let validated = Checksum::validate_page(&page);
740
741 assert!(validated.is_some(), "Should have validated using fallback");
742 let validated = validated.unwrap();
743 assert_eq!(
744 validated,
745 ActiveChecksum::new(Slot::Second, valid_len, valid_crc)
746 );
747 }
748
749 #[test]
750 fn test_validate_page_no_fallback_available() {
751 let page_size = 64usize;
752 let physical_page_size = page_size + Checksum::SIZE;
753 let mut page = vec![0u8; physical_page_size];
754
755 let data = b"some data";
757 page[..data.len()].copy_from_slice(data);
758
759 let record = Checksum {
763 len1: data.len() as u16,
764 crc1: 0xBAD1DEA, len2: 0, crc2: 0,
767 };
768
769 let crc_start = physical_page_size - Checksum::SIZE;
770 page[crc_start..].copy_from_slice(&record.to_bytes());
771
772 let validated = Checksum::validate_page(&page);
774 assert!(
775 validated.is_none(),
776 "Should fail when primary is invalid and fallback has len=0"
777 );
778 }
779
780 #[cfg(feature = "arbitrary")]
781 mod conformance {
782 use super::*;
783 use commonware_codec::conformance::CodecConformance;
784
785 commonware_conformance::conformance_tests! {
786 CodecConformance<Checksum>,
787 }
788 }
789}