1use crate::{Blob, Buf, BufMut, Error, IoBuf, ReadOptions};
46#[cfg(any(test, feature = "test-utils"))]
47use crate::{Storage, WriteOptions};
48use commonware_codec::{EncodeFixed, FixedSize, Read as CodecRead, ReadExt, Write};
49use commonware_cryptography::{Crc32, crc32};
50use std::num::NonZeroU16;
51
52mod cache;
53mod read;
54mod sealed;
55mod view;
56mod writer;
57
58pub use cache::CacheRef;
59pub use read::Replay;
60pub use sealed::Sealed;
61use tracing::{debug, error};
62pub use writer::Writer;
63
64pub const CHECKSUM_SIZE: u64 = Checksum::SIZE as u64;
66
67pub(crate) const STORAGE_PAGE_SIZE: u64 = 4096;
69
70const _: () = assert!(
73 crate::DEFAULT_BLOB_LAYOUT
74 .data_offset()
75 .is_multiple_of(STORAGE_PAGE_SIZE)
76);
77
78const CHECKSUM_SLOT_LEN_SIZE: usize = u16::SIZE;
79const CHECKSUM_SLOT_SIZE: usize = CHECKSUM_SLOT_LEN_SIZE + crc32::Digest::SIZE;
80
81pub const fn page_size(physical_page_size: u32) -> NonZeroU16 {
95 assert!(
96 physical_page_size.is_power_of_two(),
97 "physical page size must be a power of two"
98 );
99 assert!(
100 physical_page_size as u64 > CHECKSUM_SIZE,
101 "physical page size must exceed the CRC record size"
102 );
103 let logical = physical_page_size as u64 - CHECKSUM_SIZE;
104 assert!(
105 logical <= u16::MAX as u64,
106 "logical page size must fit in a u16"
107 );
108 match NonZeroU16::new(logical as u16) {
109 Some(size) => size,
110 None => unreachable!(),
111 }
112}
113
114#[cfg(test)]
117pub(crate) fn validate_page_for_tests(page: &[u8]) -> bool {
118 Checksum::validate_page(page).is_some()
119}
120
121#[cfg(any(test, feature = "test-utils"))]
129pub fn page_len(page: &[u8], logical_page_size: usize) -> Option<usize> {
130 let footer = page.get(logical_page_size..)?;
131 if footer.len() != CHECKSUM_SIZE as usize {
132 return None;
133 }
134 let slots = [
135 (
136 u16::from_be_bytes(footer[0..2].try_into().unwrap()) as usize,
137 u32::from_be_bytes(footer[2..6].try_into().unwrap()),
138 ),
139 (
140 u16::from_be_bytes(footer[6..8].try_into().unwrap()) as usize,
141 u32::from_be_bytes(footer[8..12].try_into().unwrap()),
142 ),
143 ];
144 let authoritative = usize::from(slots[1].0 > slots[0].0);
145 for slot in [authoritative, authoritative ^ 1] {
146 let (len, checksum) = slots[slot];
147 if len > 0 && len <= logical_page_size && Crc32::checksum(&page[..len]) == checksum {
148 return Some(len);
149 }
150 }
151 None
152}
153
154#[cfg(any(test, feature = "test-utils"))]
158pub async fn corrupt_page(
159 storage: &impl Storage,
160 partition: &str,
161 name: &[u8],
162 page: u64,
163 logical_page_size: u64,
164) {
165 let physical_page_size = logical_page_size + CHECKSUM_SIZE;
167 let offset = page * physical_page_size;
168 let (blob, size) = storage.open(partition, name).await.unwrap();
169
170 assert!(
174 offset
175 .checked_add(physical_page_size * 2)
176 .is_some_and(|end| end <= size),
177 "corruption target must be an interior page"
178 );
179 let byte = blob
180 .read_at(offset, 1, ReadOptions::default())
181 .await
182 .unwrap()
183 .coalesce();
184 blob.write_at(
185 offset,
186 vec![byte.as_ref()[0] ^ 0xFF],
187 WriteOptions::default(),
188 )
189 .await
190 .unwrap();
191 blob.sync().await.unwrap();
192}
193
194fn validate_read_ranges(
201 buf_len: usize,
202 ranges: impl Iterator<Item = (u64, usize)>,
203 size: u64,
204) -> Result<(), Error> {
205 let mut expected_len = 0usize;
206 let mut previous_end = None;
207 for (offset, len) in ranges {
208 expected_len = expected_len
209 .checked_add(len)
210 .expect("buf must hold one slot per range totaling its length");
211 let end = offset
212 .checked_add(len as u64)
213 .ok_or(Error::OffsetOverflow)?;
214 if let Some(previous_end) = previous_end {
215 assert!(
216 offset >= previous_end,
217 "ranges must be sorted and non-overlapping"
218 );
219 }
220 if end > size {
221 return Err(Error::BlobInsufficientLength);
222 }
223 previous_end = Some(end);
224 }
225 assert_eq!(
226 buf_len, expected_len,
227 "buf must hold one slot per range totaling its length"
228 );
229 Ok(())
230}
231
232fn split_read_ranges<'a>(
242 mut buf: &'a mut [u8],
243 ranges: impl ExactSizeIterator<Item = (u64, usize)>,
244 tail_offset: u64,
245 tail: &[u8],
246) -> Vec<(&'a mut [u8], u64)> {
247 let mut cache_ranges = Vec::with_capacity(ranges.len());
248 for (offset, len) in ranges {
249 let (slot, rest) = buf.split_at_mut(len);
250 buf = rest;
251 if len == 0 {
252 continue;
253 }
254 let end = offset + len as u64;
255 if end <= tail_offset {
256 cache_ranges.push((slot, offset));
258 } else if offset >= tail_offset {
259 let src = (offset - tail_offset) as usize;
261 slot.copy_from_slice(&tail[src..src + len]);
262 } else {
263 let prefix_len = (tail_offset - offset) as usize;
266 let (prefix, suffix) = slot.split_at_mut(prefix_len);
267 suffix.copy_from_slice(&tail[..len - prefix_len]);
268 cache_ranges.push((prefix, offset));
269 }
270 }
271 cache_ranges
272}
273
274async fn get_page_from_blob(
278 blob: &impl Blob,
279 page_num: u64,
280 page_size: u64,
281 read_options: ReadOptions,
282) -> Result<IoBuf, Error> {
283 let (page, _) =
284 get_page_with_checksum_from_blob(blob, page_num, page_size, read_options).await?;
285 Ok(page)
286}
287
288async fn get_page_with_checksum_from_blob(
290 blob: &impl Blob,
291 page_num: u64,
292 page_size: u64,
293 read_options: ReadOptions,
294) -> Result<(IoBuf, ActiveChecksum), Error> {
295 let physical_page_size = page_size
296 .checked_add(CHECKSUM_SIZE)
297 .ok_or(Error::OffsetOverflow)?;
298 let physical_page_start = page_num
299 .checked_mul(physical_page_size)
300 .ok_or(Error::OffsetOverflow)?;
301
302 let page = blob
303 .read_at(
304 physical_page_start,
305 physical_page_size as usize,
306 read_options,
307 )
308 .await?
309 .coalesce();
310
311 let Some(checksum) = Checksum::validate_page(page.as_ref()) else {
312 return Err(Error::InvalidChecksum);
313 };
314
315 Ok((page.freeze().slice(..checksum.len as usize), checksum))
316}
317
318#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320enum Slot {
321 First,
322 Second,
323}
324
325impl Slot {
326 const fn offset(self) -> usize {
328 match self {
329 Self::First => 0,
330 Self::Second => CHECKSUM_SLOT_SIZE,
331 }
332 }
333
334 const fn other(self) -> Self {
336 match self {
337 Self::First => Self::Second,
338 Self::Second => Self::First,
339 }
340 }
341}
342
343#[derive(Clone, Copy, Debug, Eq, PartialEq)]
345struct ActiveChecksum {
346 slot: Slot,
347 len: u16,
348 crc: u32,
349}
350
351impl ActiveChecksum {
352 const fn new(slot: Slot, len: u16, crc: u32) -> Self {
353 Self { slot, len, crc }
354 }
355}
356
357struct Checksum {
362 len1: u16,
363 crc1: u32,
364 len2: u16,
365 crc2: u32,
366}
367
368impl Checksum {
369 const fn new(len: u16, crc: u32) -> Self {
372 Self {
373 len1: len,
374 crc1: crc,
375 len2: 0,
376 crc2: 0,
377 }
378 }
379
380 const fn authoritative(&self) -> Slot {
382 if self.len1 >= self.len2 {
383 Slot::First
384 } else {
385 Slot::Second
386 }
387 }
388
389 fn validate_page(buf: &[u8]) -> Option<ActiveChecksum> {
392 let physical_page_size = buf.len() as u64;
393 if physical_page_size < CHECKSUM_SIZE {
394 error!(
395 physical_page_size,
396 required = CHECKSUM_SIZE,
397 "read page smaller than CRC record"
398 );
399 return None;
400 }
401
402 let crc_start_idx = (physical_page_size - CHECKSUM_SIZE) as usize;
405 let mut crc_bytes = &buf[crc_start_idx..];
406 let crc_record = Self::read(&mut crc_bytes).expect("CRC record read should not fail");
407
408 let authoritative = crc_record.authoritative();
411 if let Some(checksum) = crc_record.validate_slot(authoritative, buf, crc_start_idx) {
412 return Some(checksum);
413 }
414
415 debug!("Invalid authoritative CRC, using fallback CRC");
418 let checksum = crc_record.validate_slot(authoritative.other(), buf, crc_start_idx);
419 if checksum.is_none() {
420 debug!("Invalid fallback CRC");
421 }
422 checksum
423 }
424
425 fn validate_slot(
427 &self,
428 slot: Slot,
429 buf: &[u8],
430 crc_start_idx: usize,
431 ) -> Option<ActiveChecksum> {
432 let (len, crc) = self.get_slot(slot);
433 let len_usize = len as usize;
434
435 if len_usize == 0 {
438 return None;
439 }
440
441 if len_usize > crc_start_idx {
443 return None;
444 }
445
446 if Crc32::checksum(&buf[..len_usize]) != crc {
448 return None;
449 }
450 Some(ActiveChecksum::new(slot, len, crc))
451 }
452
453 const fn get_slot(&self, slot: Slot) -> (u16, u32) {
455 match slot {
456 Slot::First => (self.len1, self.crc1),
457 Slot::Second => (self.len2, self.crc2),
458 }
459 }
460
461 fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
463 self.encode_fixed()
464 }
465
466 fn slot_bytes(len: u16, crc: u32) -> [u8; CHECKSUM_SLOT_SIZE] {
471 let mut bytes = [0; CHECKSUM_SLOT_SIZE];
472 let mut buf = bytes.as_mut_slice();
473 len.write(&mut buf);
474 crc.write(&mut buf);
475 bytes
476 }
477
478 fn slot_len_bytes(len: u16) -> [u8; CHECKSUM_SLOT_LEN_SIZE] {
486 let mut bytes = [0; CHECKSUM_SLOT_LEN_SIZE];
487 let mut buf = bytes.as_mut_slice();
488 len.write(&mut buf);
489 bytes
490 }
491}
492
493impl Write for Checksum {
494 fn write(&self, buf: &mut impl BufMut) {
495 self.len1.write(buf);
496 self.crc1.write(buf);
497 self.len2.write(buf);
498 self.crc2.write(buf);
499 }
500}
501
502impl CodecRead for Checksum {
503 type Cfg = ();
504
505 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
506 Ok(Self {
507 len1: u16::read(buf)?,
508 crc1: u32::read(buf)?,
509 len2: u16::read(buf)?,
510 crc2: u32::read(buf)?,
511 })
512 }
513}
514
515impl FixedSize for Checksum {
516 const SIZE: usize = 2 * u16::SIZE + 2 * crc32::Digest::SIZE;
517}
518
519#[cfg(feature = "arbitrary")]
520impl arbitrary::Arbitrary<'_> for Checksum {
521 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
522 Ok(Self {
523 len1: u.arbitrary()?,
524 crc1: u.arbitrary()?,
525 len2: u.arbitrary()?,
526 crc2: u.arbitrary()?,
527 })
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use rstest::rstest;
535
536 #[test]
537 #[should_panic(expected = "corruption target must be an interior page")]
538 fn test_corrupt_page_rejects_short_blob() {
539 use crate::Runner as _;
540 crate::deterministic::Runner::default().start(|context| async move {
541 corrupt_page(&context, "short-blob", b"blob", 0, 64).await;
542 });
543 }
544
545 enum ValidationExpectation {
546 Ok,
547 OffsetOverflow,
548 BlobInsufficientLength,
549 }
550
551 #[rstest]
552 #[case::ok(12, vec![(0, 4), (4, 8)], 16, ValidationExpectation::Ok)]
553 #[case::empty_ranges_are_a_noop(0, vec![], 0, ValidationExpectation::Ok)]
554 #[case::zero_length_range(4, vec![(0, 0), (0, 4)], 16, ValidationExpectation::Ok)]
555 #[case::offset_overflow(4, vec![(u64::MAX, 4)], 16, ValidationExpectation::OffsetOverflow)]
556 #[case::insufficient_length(4, vec![(14, 4)], 16, ValidationExpectation::BlobInsufficientLength)]
557 #[case::range_may_end_exactly_at_logical_size(4, vec![(12, 4)], 16, ValidationExpectation::Ok)]
558 fn test_validate_read_ranges(
559 #[case] buf_len: usize,
560 #[case] ranges: Vec<(u64, usize)>,
561 #[case] size: u64,
562 #[case] expected: ValidationExpectation,
563 ) {
564 let result = validate_read_ranges(buf_len, ranges.iter().copied(), size);
565
566 match expected {
567 ValidationExpectation::Ok => assert!(result.is_ok()),
568 ValidationExpectation::OffsetOverflow => {
569 assert!(matches!(result, Err(Error::OffsetOverflow)))
570 }
571 ValidationExpectation::BlobInsufficientLength => {
572 assert!(matches!(result, Err(Error::BlobInsufficientLength)))
573 }
574 }
575 }
576
577 #[test]
578 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
579 fn test_validate_read_ranges_rejects_buffer_len_mismatch() {
580 let _ = validate_read_ranges(7, [(0, 4), (4, 4)].into_iter(), 16);
581 }
582
583 #[test]
584 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
585 fn test_validate_read_ranges_rejects_overlapping_ranges() {
586 let _ = validate_read_ranges(8, [(0, 4), (2, 4)].into_iter(), 16);
587 }
588
589 #[test]
590 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
591 fn test_validate_read_ranges_rejects_unsorted_ranges() {
592 let _ = validate_read_ranges(8, [(8, 4), (4, 4)].into_iter(), 16);
593 }
594
595 #[test]
596 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
597 fn test_validate_read_ranges_rejects_length_overflow() {
598 let _ = validate_read_ranges(
599 usize::MAX,
600 [(0, usize::MAX), (u64::MAX, 1)].into_iter(),
601 u64::MAX,
602 );
603 }
604
605 #[test]
606 fn test_crc_record_encode_read_roundtrip() {
607 let record = Checksum {
608 len1: 0x1234,
609 crc1: 0xAABBCCDD,
610 len2: 0x5678,
611 crc2: 0x11223344,
612 };
613
614 let bytes = record.to_bytes();
615 let restored = Checksum::read(&mut &bytes[..]).unwrap();
616
617 assert_eq!(restored.len1, 0x1234);
618 assert_eq!(restored.crc1, 0xAABBCCDD);
619 assert_eq!(restored.len2, 0x5678);
620 assert_eq!(restored.crc2, 0x11223344);
621 }
622
623 #[test]
624 fn test_crc_record_encoding() {
625 let record = Checksum {
626 len1: 0x0102,
627 crc1: 0x03040506,
628 len2: 0x0708,
629 crc2: 0x090A0B0C,
630 };
631
632 let bytes = record.to_bytes();
633 assert_eq!(
635 bytes,
636 [
637 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C
638 ]
639 );
640 }
641
642 #[test]
643 fn test_crc_record_authoritative_len1_larger() {
644 let record = Checksum {
645 len1: 200,
646 crc1: 0xAAAAAAAA,
647 len2: 100,
648 crc2: 0xBBBBBBBB,
649 };
650
651 assert_eq!(record.authoritative(), Slot::First);
652 }
653
654 #[test]
655 fn test_crc_record_authoritative_len2_larger() {
656 let record = Checksum {
657 len1: 100,
658 crc1: 0xAAAAAAAA,
659 len2: 200,
660 crc2: 0xBBBBBBBB,
661 };
662
663 assert_eq!(record.authoritative(), Slot::Second);
664 }
665
666 #[test]
667 fn test_crc_record_authoritative_equal_lengths() {
668 let record = Checksum {
670 len1: 100,
671 crc1: 0xAAAAAAAA,
672 len2: 100,
673 crc2: 0xBBBBBBBB,
674 };
675
676 assert_eq!(record.authoritative(), Slot::First);
677 }
678
679 #[test]
680 fn test_validate_page_valid() {
681 let page_size = 64usize;
682 let physical_page_size = page_size + Checksum::SIZE;
683 let mut page = vec![0u8; physical_page_size];
684
685 let data = b"hello world";
687 page[..data.len()].copy_from_slice(data);
688
689 let crc = Crc32::checksum(&page[..data.len()]);
691 let record = Checksum::new(data.len() as u16, crc);
692
693 let crc_start = physical_page_size - Checksum::SIZE;
695 page[crc_start..].copy_from_slice(&record.to_bytes());
696
697 let validated = Checksum::validate_page(&page);
699 assert!(validated.is_some());
700 assert_eq!(validated.unwrap().len as usize, data.len());
701 }
702
703 #[test]
704 fn test_validate_page_invalid_crc() {
705 let page_size = 64usize;
706 let physical_page_size = page_size + Checksum::SIZE;
707 let mut page = vec![0u8; physical_page_size];
708
709 let data = b"hello world";
711 page[..data.len()].copy_from_slice(data);
712
713 let wrong_crc = 0xBADBADBA;
715 let record = Checksum::new(data.len() as u16, wrong_crc);
716
717 let crc_start = physical_page_size - Checksum::SIZE;
718 page[crc_start..].copy_from_slice(&record.to_bytes());
719
720 let validated = Checksum::validate_page(&page);
722 assert!(validated.is_none());
723 }
724
725 #[test]
726 fn test_validate_page_corrupted_data() {
727 let page_size = 64usize;
728 let physical_page_size = page_size + Checksum::SIZE;
729 let mut page = vec![0u8; physical_page_size];
730
731 let data = b"hello world";
733 page[..data.len()].copy_from_slice(data);
734 let crc = Crc32::checksum(&page[..data.len()]);
735 let record = Checksum::new(data.len() as u16, crc);
736
737 let crc_start = physical_page_size - Checksum::SIZE;
738 page[crc_start..].copy_from_slice(&record.to_bytes());
739
740 page[0] = 0xFF;
742
743 let validated = Checksum::validate_page(&page);
745 assert!(validated.is_none());
746 }
747
748 #[test]
749 fn test_validate_page_uses_larger_len() {
750 let page_size = 64usize;
751 let physical_page_size = page_size + Checksum::SIZE;
752 let mut page = vec![0u8; physical_page_size];
753
754 let data = b"hello world, this is longer";
756 page[..data.len()].copy_from_slice(data);
757 let crc = Crc32::checksum(&page[..data.len()]);
758
759 let record = Checksum {
761 len1: 5,
762 crc1: 0xDEADBEEF, len2: data.len() as u16,
764 crc2: crc,
765 };
766
767 let crc_start = physical_page_size - Checksum::SIZE;
768 page[crc_start..].copy_from_slice(&record.to_bytes());
769
770 let validated = Checksum::validate_page(&page);
772 assert!(validated.is_some());
773 assert_eq!(validated.unwrap().len as usize, data.len());
774 }
775
776 #[test]
777 fn test_validate_page_uses_fallback() {
778 let page_size = 64usize;
779 let physical_page_size = page_size + Checksum::SIZE;
780 let mut page = vec![0u8; physical_page_size];
781
782 let data = b"fallback data";
784 page[..data.len()].copy_from_slice(data);
785 let valid_crc = Crc32::checksum(&page[..data.len()]);
786 let valid_len = data.len() as u16;
787
788 let record = Checksum {
792 len1: valid_len + 10, crc1: 0xBAD1DEA, len2: valid_len, crc2: valid_crc, };
797
798 let crc_start = physical_page_size - Checksum::SIZE;
799 page[crc_start..].copy_from_slice(&record.to_bytes());
800
801 let validated = Checksum::validate_page(&page);
803
804 assert!(validated.is_some(), "Should have validated using fallback");
805 let validated = validated.unwrap();
806 assert_eq!(
807 validated,
808 ActiveChecksum::new(Slot::Second, valid_len, valid_crc)
809 );
810 }
811
812 #[test]
813 fn test_validate_page_no_fallback_available() {
814 let page_size = 64usize;
815 let physical_page_size = page_size + Checksum::SIZE;
816 let mut page = vec![0u8; physical_page_size];
817
818 let data = b"some data";
820 page[..data.len()].copy_from_slice(data);
821
822 let record = Checksum {
826 len1: data.len() as u16,
827 crc1: 0xBAD1DEA, len2: 0, crc2: 0,
830 };
831
832 let crc_start = physical_page_size - Checksum::SIZE;
833 page[crc_start..].copy_from_slice(&record.to_bytes());
834
835 let validated = Checksum::validate_page(&page);
837 assert!(
838 validated.is_none(),
839 "Should fail when primary is invalid and fallback has len=0"
840 );
841 }
842
843 #[cfg(feature = "arbitrary")]
844 mod conformance {
845 use super::*;
846 use commonware_codec::conformance::CodecConformance;
847
848 commonware_conformance::conformance_tests! {
849 CodecConformance<Checksum>,
850 }
851 }
852}