1use crate::{Blob, Buf, BufMut, Error, IoBuf};
27use commonware_codec::{EncodeFixed, FixedSize, Read as CodecRead, ReadExt, Write};
28use commonware_cryptography::{crc32, Crc32};
29
30mod cache;
31mod read;
32mod sealed;
33mod view;
34mod writer;
35
36pub use cache::CacheRef;
37pub use read::Replay;
38pub use sealed::Sealed;
39use tracing::{debug, error};
40pub use writer::Writer;
41
42const CHECKSUM_SIZE: u64 = Checksum::SIZE as u64;
44const CHECKSUM_SLOT_LEN_SIZE: usize = u16::SIZE;
45const CHECKSUM_SLOT_SIZE: usize = CHECKSUM_SLOT_LEN_SIZE + crc32::Digest::SIZE;
46
47fn validate_read_ranges(
54 buf_len: usize,
55 ranges: impl Iterator<Item = (u64, usize)>,
56 size: u64,
57) -> Result<(), Error> {
58 let mut expected_len = 0usize;
59 let mut previous_end = None;
60 for (offset, len) in ranges {
61 expected_len = expected_len
62 .checked_add(len)
63 .expect("buf must hold one slot per range totaling its length");
64 let end = offset
65 .checked_add(len as u64)
66 .ok_or(Error::OffsetOverflow)?;
67 if let Some(previous_end) = previous_end {
68 assert!(
69 offset >= previous_end,
70 "ranges must be sorted and non-overlapping"
71 );
72 }
73 if end > size {
74 return Err(Error::BlobInsufficientLength);
75 }
76 previous_end = Some(end);
77 }
78 assert_eq!(
79 buf_len, expected_len,
80 "buf must hold one slot per range totaling its length"
81 );
82 Ok(())
83}
84
85fn split_read_ranges<'a>(
95 mut buf: &'a mut [u8],
96 ranges: impl ExactSizeIterator<Item = (u64, usize)>,
97 tail_offset: u64,
98 tail: &[u8],
99) -> Vec<(&'a mut [u8], u64)> {
100 let mut cache_ranges = Vec::with_capacity(ranges.len());
101 for (offset, len) in ranges {
102 let (slot, rest) = buf.split_at_mut(len);
103 buf = rest;
104 if len == 0 {
105 continue;
106 }
107 let end = offset + len as u64;
108 if end <= tail_offset {
109 cache_ranges.push((slot, offset));
111 } else if offset >= tail_offset {
112 let src = (offset - tail_offset) as usize;
114 slot.copy_from_slice(&tail[src..src + len]);
115 } else {
116 let prefix_len = (tail_offset - offset) as usize;
119 let (prefix, suffix) = slot.split_at_mut(prefix_len);
120 suffix.copy_from_slice(&tail[..len - prefix_len]);
121 cache_ranges.push((prefix, offset));
122 }
123 }
124 cache_ranges
125}
126
127async fn get_page_from_blob(
131 blob: &impl Blob,
132 page_num: u64,
133 logical_page_size: u64,
134) -> Result<IoBuf, Error> {
135 let (page, _) = get_page_with_checksum_from_blob(blob, page_num, logical_page_size).await?;
136 Ok(page)
137}
138
139async fn get_page_with_checksum_from_blob(
141 blob: &impl Blob,
142 page_num: u64,
143 logical_page_size: u64,
144) -> Result<(IoBuf, Checksum), Error> {
145 let physical_page_size = logical_page_size
146 .checked_add(CHECKSUM_SIZE)
147 .ok_or(Error::OffsetOverflow)?;
148 let physical_page_start = page_num
149 .checked_mul(physical_page_size)
150 .ok_or(Error::OffsetOverflow)?;
151
152 let page = blob
153 .read_at(physical_page_start, physical_page_size as usize)
154 .await?
155 .coalesce();
156
157 let Some(record) = Checksum::validate_page(page.as_ref()) else {
158 return Err(Error::InvalidChecksum);
159 };
160 let (len, _) = record.get_crc();
161
162 Ok((page.freeze().slice(..len as usize), record))
163}
164
165#[derive(Clone, Copy)]
167enum Slot {
168 First,
169 Second,
170}
171
172impl Slot {
173 const fn offset(self) -> usize {
175 match self {
176 Self::First => 0,
177 Self::Second => CHECKSUM_SLOT_SIZE,
178 }
179 }
180
181 const fn other(self) -> Self {
183 match self {
184 Self::First => Self::Second,
185 Self::Second => Self::First,
186 }
187 }
188}
189
190#[derive(Clone)]
196struct Checksum {
197 len1: u16,
198 crc1: u32,
199 len2: u16,
200 crc2: u32,
201}
202
203impl Checksum {
204 const fn new(len: u16, crc: u32) -> Self {
207 Self::in_slot(Slot::First, len, crc)
208 }
209
210 const fn in_slot(slot: Slot, len: u16, crc: u32) -> Self {
212 match slot {
213 Slot::First => Self {
214 len1: len,
215 crc1: crc,
216 len2: 0,
217 crc2: 0,
218 },
219 Slot::Second => Self {
220 len1: 0,
221 crc1: 0,
222 len2: len,
223 crc2: crc,
224 },
225 }
226 }
227
228 const fn authoritative(&self) -> Slot {
230 if self.len1 >= self.len2 {
231 Slot::First
232 } else {
233 Slot::Second
234 }
235 }
236
237 fn validate_page(buf: &[u8]) -> Option<Self> {
242 let page_size = buf.len() as u64;
243 if page_size < CHECKSUM_SIZE {
244 error!(
245 page_size,
246 required = CHECKSUM_SIZE,
247 "read page smaller than CRC record"
248 );
249 return None;
250 }
251
252 let crc_start_idx = (page_size - CHECKSUM_SIZE) as usize;
253 let mut crc_bytes = &buf[crc_start_idx..];
254 let mut crc_record = Self::read(&mut crc_bytes).expect("CRC record read should not fail");
255 let (len, crc) = crc_record.get_crc();
256
257 let len_usize = len as usize;
260 if len_usize == 0 {
261 debug!("Invalid CRC: len==0");
263 return None;
264 }
265
266 if len_usize > crc_start_idx {
267 debug!("Invalid CRC: len too long. Using fallback CRC");
269 if crc_record.validate_fallback(buf, crc_start_idx) {
270 return Some(crc_record);
271 }
272 return None;
273 }
274
275 let computed_crc = Crc32::checksum(&buf[..len_usize]);
276 if computed_crc != crc {
277 debug!("Invalid CRC: doesn't match page contents. Using fallback CRC");
278 if crc_record.validate_fallback(buf, crc_start_idx) {
279 return Some(crc_record);
280 }
281 return None;
282 }
283
284 Some(crc_record)
285 }
286
287 fn validate_fallback(&mut self, buf: &[u8], crc_start_idx: usize) -> bool {
291 let (len, crc) = self.get_fallback_crc();
292 if len == 0 {
293 debug!("Invalid fallback CRC: len==0");
295 return false;
296 }
297
298 let len_usize = len as usize;
299
300 if len_usize > crc_start_idx {
301 debug!("Invalid fallback CRC: len too long.");
303 return false;
304 }
305
306 let computed_crc = Crc32::checksum(&buf[..len_usize]);
307 if computed_crc != crc {
308 debug!("Invalid fallback CRC: doesn't match page contents.");
309 return false;
310 }
311
312 true
313 }
314
315 const fn get_crc(&self) -> (u16, u32) {
319 match self.authoritative() {
320 Slot::First => (self.len1, self.crc1),
321 Slot::Second => (self.len2, self.crc2),
322 }
323 }
324
325 const fn get_fallback_crc(&mut self) -> (u16, u32) {
329 match self.authoritative() {
330 Slot::First => {
331 self.len1 = 0;
333 self.crc1 = 0;
334 (self.len2, self.crc2)
335 }
336 Slot::Second => {
337 self.len2 = 0;
339 self.crc2 = 0;
340 (self.len1, self.crc1)
341 }
342 }
343 }
344
345 fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
347 self.encode_fixed()
348 }
349
350 fn slot_bytes(len: u16, crc: u32) -> [u8; CHECKSUM_SLOT_SIZE] {
355 let mut bytes = [0; CHECKSUM_SLOT_SIZE];
356 let mut buf = bytes.as_mut_slice();
357 len.write(&mut buf);
358 crc.write(&mut buf);
359 bytes
360 }
361
362 fn slot_len_bytes(len: u16) -> [u8; CHECKSUM_SLOT_LEN_SIZE] {
369 let mut bytes = [0; CHECKSUM_SLOT_LEN_SIZE];
370 let mut buf = bytes.as_mut_slice();
371 len.write(&mut buf);
372 bytes
373 }
374}
375
376impl Write for Checksum {
377 fn write(&self, buf: &mut impl BufMut) {
378 self.len1.write(buf);
379 self.crc1.write(buf);
380 self.len2.write(buf);
381 self.crc2.write(buf);
382 }
383}
384
385impl CodecRead for Checksum {
386 type Cfg = ();
387
388 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
389 Ok(Self {
390 len1: u16::read(buf)?,
391 crc1: u32::read(buf)?,
392 len2: u16::read(buf)?,
393 crc2: u32::read(buf)?,
394 })
395 }
396}
397
398impl FixedSize for Checksum {
399 const SIZE: usize = 2 * u16::SIZE + 2 * crc32::Digest::SIZE;
400}
401
402#[cfg(feature = "arbitrary")]
403impl arbitrary::Arbitrary<'_> for Checksum {
404 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
405 Ok(Self {
406 len1: u.arbitrary()?,
407 crc1: u.arbitrary()?,
408 len2: u.arbitrary()?,
409 crc2: u.arbitrary()?,
410 })
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use rstest::rstest;
418
419 enum ValidationExpectation {
420 Ok,
421 OffsetOverflow,
422 BlobInsufficientLength,
423 }
424
425 #[rstest]
426 #[case::ok(12, vec![(0, 4), (4, 8)], 16, ValidationExpectation::Ok)]
427 #[case::empty_ranges_are_a_noop(0, vec![], 0, ValidationExpectation::Ok)]
428 #[case::zero_length_range(4, vec![(0, 0), (0, 4)], 16, ValidationExpectation::Ok)]
429 #[case::offset_overflow(4, vec![(u64::MAX, 4)], 16, ValidationExpectation::OffsetOverflow)]
430 #[case::insufficient_length(4, vec![(14, 4)], 16, ValidationExpectation::BlobInsufficientLength)]
431 #[case::range_may_end_exactly_at_logical_size(4, vec![(12, 4)], 16, ValidationExpectation::Ok)]
432 fn test_validate_read_ranges(
433 #[case] buf_len: usize,
434 #[case] ranges: Vec<(u64, usize)>,
435 #[case] size: u64,
436 #[case] expected: ValidationExpectation,
437 ) {
438 let result = validate_read_ranges(buf_len, ranges.iter().copied(), size);
439
440 match expected {
441 ValidationExpectation::Ok => assert!(result.is_ok()),
442 ValidationExpectation::OffsetOverflow => {
443 assert!(matches!(result, Err(Error::OffsetOverflow)))
444 }
445 ValidationExpectation::BlobInsufficientLength => {
446 assert!(matches!(result, Err(Error::BlobInsufficientLength)))
447 }
448 }
449 }
450
451 #[test]
452 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
453 fn test_validate_read_ranges_rejects_buffer_len_mismatch() {
454 let _ = validate_read_ranges(7, [(0, 4), (4, 4)].into_iter(), 16);
455 }
456
457 #[test]
458 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
459 fn test_validate_read_ranges_rejects_overlapping_ranges() {
460 let _ = validate_read_ranges(8, [(0, 4), (2, 4)].into_iter(), 16);
461 }
462
463 #[test]
464 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
465 fn test_validate_read_ranges_rejects_unsorted_ranges() {
466 let _ = validate_read_ranges(8, [(8, 4), (4, 4)].into_iter(), 16);
467 }
468
469 #[test]
470 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
471 fn test_validate_read_ranges_rejects_length_overflow() {
472 let _ = validate_read_ranges(
473 usize::MAX,
474 [(0, usize::MAX), (u64::MAX, 1)].into_iter(),
475 u64::MAX,
476 );
477 }
478
479 #[test]
480 fn test_crc_record_encode_read_roundtrip() {
481 let record = Checksum {
482 len1: 0x1234,
483 crc1: 0xAABBCCDD,
484 len2: 0x5678,
485 crc2: 0x11223344,
486 };
487
488 let bytes = record.to_bytes();
489 let restored = Checksum::read(&mut &bytes[..]).unwrap();
490
491 assert_eq!(restored.len1, 0x1234);
492 assert_eq!(restored.crc1, 0xAABBCCDD);
493 assert_eq!(restored.len2, 0x5678);
494 assert_eq!(restored.crc2, 0x11223344);
495 }
496
497 #[test]
498 fn test_crc_record_encoding() {
499 let record = Checksum {
500 len1: 0x0102,
501 crc1: 0x03040506,
502 len2: 0x0708,
503 crc2: 0x090A0B0C,
504 };
505
506 let bytes = record.to_bytes();
507 assert_eq!(
509 bytes,
510 [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C]
511 );
512 }
513
514 #[test]
515 fn test_crc_record_get_crc_len1_larger() {
516 let record = Checksum {
517 len1: 200,
518 crc1: 0xAAAAAAAA,
519 len2: 100,
520 crc2: 0xBBBBBBBB,
521 };
522
523 let (len, crc) = record.get_crc();
524 assert_eq!(len, 200);
525 assert_eq!(crc, 0xAAAAAAAA);
526 }
527
528 #[test]
529 fn test_crc_record_get_crc_len2_larger() {
530 let record = Checksum {
531 len1: 100,
532 crc1: 0xAAAAAAAA,
533 len2: 200,
534 crc2: 0xBBBBBBBB,
535 };
536
537 let (len, crc) = record.get_crc();
538 assert_eq!(len, 200);
539 assert_eq!(crc, 0xBBBBBBBB);
540 }
541
542 #[test]
543 fn test_crc_record_get_crc_equal_lengths() {
544 let record = Checksum {
546 len1: 100,
547 crc1: 0xAAAAAAAA,
548 len2: 100,
549 crc2: 0xBBBBBBBB,
550 };
551
552 let (len, crc) = record.get_crc();
553 assert_eq!(len, 100);
554 assert_eq!(crc, 0xAAAAAAAA);
555 }
556
557 #[test]
558 fn test_validate_page_valid() {
559 let logical_page_size = 64usize;
560 let physical_page_size = logical_page_size + Checksum::SIZE;
561 let mut page = vec![0u8; physical_page_size];
562
563 let data = b"hello world";
565 page[..data.len()].copy_from_slice(data);
566
567 let crc = Crc32::checksum(&page[..data.len()]);
569 let record = Checksum::new(data.len() as u16, crc);
570
571 let crc_start = physical_page_size - Checksum::SIZE;
573 page[crc_start..].copy_from_slice(&record.to_bytes());
574
575 let validated = Checksum::validate_page(&page);
577 assert!(validated.is_some());
578 let (len, _) = validated.unwrap().get_crc();
579 assert_eq!(len as usize, data.len());
580 }
581
582 #[test]
583 fn test_validate_page_invalid_crc() {
584 let logical_page_size = 64usize;
585 let physical_page_size = logical_page_size + Checksum::SIZE;
586 let mut page = vec![0u8; physical_page_size];
587
588 let data = b"hello world";
590 page[..data.len()].copy_from_slice(data);
591
592 let wrong_crc = 0xBADBADBA;
594 let record = Checksum::new(data.len() as u16, wrong_crc);
595
596 let crc_start = physical_page_size - Checksum::SIZE;
597 page[crc_start..].copy_from_slice(&record.to_bytes());
598
599 let validated = Checksum::validate_page(&page);
601 assert!(validated.is_none());
602 }
603
604 #[test]
605 fn test_validate_page_corrupted_data() {
606 let logical_page_size = 64usize;
607 let physical_page_size = logical_page_size + Checksum::SIZE;
608 let mut page = vec![0u8; physical_page_size];
609
610 let data = b"hello world";
612 page[..data.len()].copy_from_slice(data);
613 let crc = Crc32::checksum(&page[..data.len()]);
614 let record = Checksum::new(data.len() as u16, crc);
615
616 let crc_start = physical_page_size - Checksum::SIZE;
617 page[crc_start..].copy_from_slice(&record.to_bytes());
618
619 page[0] = 0xFF;
621
622 let validated = Checksum::validate_page(&page);
624 assert!(validated.is_none());
625 }
626
627 #[test]
628 fn test_validate_page_uses_larger_len() {
629 let logical_page_size = 64usize;
630 let physical_page_size = logical_page_size + Checksum::SIZE;
631 let mut page = vec![0u8; physical_page_size];
632
633 let data = b"hello world, this is longer";
635 page[..data.len()].copy_from_slice(data);
636 let crc = Crc32::checksum(&page[..data.len()]);
637
638 let record = Checksum {
640 len1: 5,
641 crc1: 0xDEADBEEF, len2: data.len() as u16,
643 crc2: crc,
644 };
645
646 let crc_start = physical_page_size - Checksum::SIZE;
647 page[crc_start..].copy_from_slice(&record.to_bytes());
648
649 let validated = Checksum::validate_page(&page);
651 assert!(validated.is_some());
652 let (len, _) = validated.unwrap().get_crc();
653 assert_eq!(len as usize, data.len());
654 }
655
656 #[test]
657 fn test_validate_page_uses_fallback() {
658 let logical_page_size = 64usize;
659 let physical_page_size = logical_page_size + Checksum::SIZE;
660 let mut page = vec![0u8; physical_page_size];
661
662 let data = b"fallback data";
664 page[..data.len()].copy_from_slice(data);
665 let valid_crc = Crc32::checksum(&page[..data.len()]);
666 let valid_len = data.len() as u16;
667
668 let record = Checksum {
672 len1: valid_len + 10, crc1: 0xBAD1DEA, len2: valid_len, crc2: valid_crc, };
677
678 let crc_start = physical_page_size - Checksum::SIZE;
679 page[crc_start..].copy_from_slice(&record.to_bytes());
680
681 let validated = Checksum::validate_page(&page);
683
684 assert!(validated.is_some(), "Should have validated using fallback");
685 let validated = validated.unwrap();
686 let (len, crc) = validated.get_crc();
687 assert_eq!(len, valid_len);
688 assert_eq!(crc, valid_crc);
689
690 assert_eq!(validated.len1, 0);
692 assert_eq!(validated.crc1, 0);
693 }
694
695 #[test]
696 fn test_validate_page_no_fallback_available() {
697 let logical_page_size = 64usize;
698 let physical_page_size = logical_page_size + Checksum::SIZE;
699 let mut page = vec![0u8; physical_page_size];
700
701 let data = b"some data";
703 page[..data.len()].copy_from_slice(data);
704
705 let record = Checksum {
709 len1: data.len() as u16,
710 crc1: 0xBAD1DEA, len2: 0, crc2: 0,
713 };
714
715 let crc_start = physical_page_size - Checksum::SIZE;
716 page[crc_start..].copy_from_slice(&record.to_bytes());
717
718 let validated = Checksum::validate_page(&page);
720 assert!(
721 validated.is_none(),
722 "Should fail when primary is invalid and fallback has len=0"
723 );
724 }
725
726 #[cfg(feature = "arbitrary")]
727 mod conformance {
728 use super::*;
729 use commonware_codec::conformance::CodecConformance;
730
731 commonware_conformance::conformance_tests! {
732 CodecConformance<Checksum>,
733 }
734 }
735}