Skip to main content

commonware_runtime/utils/buffer/paged/
mod.rs

1//! Blob wrappers for reading and writing data with integrity guarantees, plus a page cache that
2//! manages read caching over the data.
3//!
4//! # Page-oriented structure
5//!
6//! Blob data is stored in _pages_ having a logical `page_size` dictated by the managing page cache.
7//! A _physical page_ consists of `page_size` bytes of data followed by a 12-byte _CRC
8//! record_ containing:
9//!
10//! ```text
11//! | len1 (2 bytes) |  crc1 (4 bytes) | len2 (2 bytes) | crc2 (4 bytes) |
12//! ```
13//!
14//! Two checksums are stored so that partial pages can be re-written without overwriting a valid
15//! checksum for its previously committed contents. A checksum over a page is computed over the
16//! first [0,len) bytes in the page, with all other bytes in the page ignored. Ordinary partial-page
17//! payload writes 0-pad the range [len, page_size), but recovery does not depend on bytes outside
18//! [0,len). A checksum with length 0 is never considered valid. If both checksums are valid for the
19//! page, the one with the larger `len` is considered authoritative. Partial-page shrink first makes
20//! the shorter checksum durable in the alternate slot, then invalidates the old longer checksum.
21//!
22//! A _full_ page is one whose crc stores a len equal to the logical page size. Otherwise the page
23//! is called _partial_. All pages in a blob are full except for the very last page, which can be
24//! full or partial. A partial page's committed prefix remains recoverable while it is rewritten.
25
26use 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
42// A checksum record contains two slots. Each slot stores one u16 length and one CRC.
43const 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
47/// Ensure every requested range lies within the blob's size.
48///
49/// # Panics
50///
51/// Panics if `buf` does not hold one slot per range totaling its length, or if ranges are not
52/// sorted and non-overlapping.
53fn 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
85/// Partition a batch of variable-length range reads into bytes copied from the in-memory tail
86/// and ranges that need cache/blob reads.
87///
88/// `buf` holds one slot per range, back to back (validated by [validate_read_ranges]). `tail`
89/// holds the logical bytes at `[tail_offset, tail_offset + tail.len())`; for [Writer] this is the
90/// tip buffer, for [Sealed] the partial last page. Ranges entirely within `tail` are copied into
91/// place. Ranges fully or partially below `tail_offset` are returned as `(dest_slice, offset)`
92/// pairs for the caller to read from the page cache or blob. `split_at_mut` yields disjoint
93/// per-range slots, so returned slices never alias.
94fn 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            // Entirely below the tail bytes, so this needs a cache/blob read.
110            cache_ranges.push((slot, offset));
111        } else if offset >= tail_offset {
112            // Entirely within the tail bytes.
113            let src = (offset - tail_offset) as usize;
114            slot.copy_from_slice(&tail[src..src + len]);
115        } else {
116            // Straddles the boundary: copy the suffix from the tail bytes, record the prefix
117            // for a cache/blob read.
118            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
127/// Read the designated page from the underlying blob and return its logical bytes as a vector if it
128/// passes the integrity check, returning error otherwise. Safely handles partial pages. Caller can
129/// check the length of the returned vector to determine if the page was partial vs full.
130async 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
139/// Read the designated page and return both its logical bytes and validated checksum record.
140async 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/// One of a page footer's two CRC slots, laid out back to back after the page data.
166#[derive(Clone, Copy)]
167enum Slot {
168    First,
169    Second,
170}
171
172impl Slot {
173    /// Byte offset of this slot within the page's CRC footer.
174    const fn offset(self) -> usize {
175        match self {
176            Self::First => 0,
177            Self::Second => CHECKSUM_SLOT_SIZE,
178        }
179    }
180
181    /// The other slot.
182    const fn other(self) -> Self {
183        match self {
184            Self::First => Self::Second,
185            Self::Second => Self::First,
186        }
187    }
188}
189
190/// Describes a CRC record stored at the end of a page.
191///
192/// The CRC accompanied by the larger length is the one that should be treated as authoritative for
193/// the page. Two checksums are stored so that partial pages can be written without overwriting a
194/// valid checksum for a previously committed partial page.
195#[derive(Clone)]
196struct Checksum {
197    len1: u16,
198    crc1: u32,
199    len2: u16,
200    crc2: u32,
201}
202
203impl Checksum {
204    /// Create a new CRC record with the given length and CRC.
205    /// The new CRC is stored in the first slot (len1/crc1), with the second slot zeroed.
206    const fn new(len: u16, crc: u32) -> Self {
207        Self::in_slot(Slot::First, len, crc)
208    }
209
210    /// A record carrying `(len, crc)` in `slot`, with the other slot zeroed.
211    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    /// The slot holding the authoritative (longer) CRC; the first slot wins ties.
229    const fn authoritative(&self) -> Slot {
230        if self.len1 >= self.len2 {
231            Slot::First
232        } else {
233            Slot::Second
234        }
235    }
236
237    /// Return the CRC record for the page if it is valid. The provided slice is assumed to be
238    /// exactly the size of a physical page. The record may not precisely reflect the bytes written
239    /// if what should have been the most recent CRC doesn't validate, in which case it will be
240    /// zeroed and the other CRC used as a fallback.
241    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        // Validate that len is in the valid range [1, logical_page_size].
258        // A page with len=0 is invalid (e.g., all-zero pages from unwritten data).
259        let len_usize = len as usize;
260        if len_usize == 0 {
261            // Both CRCs have 0 length, so there is no fallback possible.
262            debug!("Invalid CRC: len==0");
263            return None;
264        }
265
266        if len_usize > crc_start_idx {
267            // len is too large so this CRC isn't valid. Fall back to the other CRC.
268            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    /// Attempts to validate a CRC record based on its fallback CRC because the primary CRC failed
288    /// validation. The primary CRC is zeroed in the process. Returns false if the fallback CRC
289    /// fails validation.
290    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            // No fallback available (only one CRC was ever written to this page).
294            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            // len is too large so this CRC isn't valid.
302            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    /// Returns the CRC record with the longer (authoritative) length, without performing any
316    /// validation. If they both have the same length (which should only happen due to data
317    /// corruption) return the first.
318    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    /// Zeroes the primary CRC (because we assumed it failed validation) and returns the other. This
326    /// should only be called if the primary CRC failed validation. After this returns, get_crc will
327    /// no longer return the invalid primary CRC.
328    const fn get_fallback_crc(&mut self) -> (u16, u32) {
329        match self.authoritative() {
330            Slot::First => {
331                // First CRC was primary, and must have been invalid. Zero it and return the second.
332                self.len1 = 0;
333                self.crc1 = 0;
334                (self.len2, self.crc2)
335            }
336            Slot::Second => {
337                // Second CRC was primary, and must have been invalid. Zero it and return the first.
338                self.len2 = 0;
339                self.crc2 = 0;
340                (self.len1, self.crc1)
341            }
342        }
343    }
344
345    /// Returns the CRC record in its storage representation.
346    fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
347        self.encode_fixed()
348    }
349
350    /// Encode a whole checksum slot (`[len: u16][crc: u32]`) in its storage representation.
351    ///
352    /// A page footer holds two slots; recovery treats the one with the larger `len` as
353    /// authoritative (see [`Self::get_crc`]). A `len` of 0 is never authoritative.
354    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    /// Encode just a slot's leading `len` field (the first [`CHECKSUM_SLOT_LEN_SIZE`] bytes of
363    /// [`Self::slot_bytes`]).
364    ///
365    /// Because `len` decides which slot is authoritative, rewriting only this field flips a slot's
366    /// authority without disturbing its already-durable CRC: writing a non-zero `len` commits a
367    /// previously staged slot, while writing 0 retires one.
368    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        // Verify big-endian encoding
508        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        // When lengths are equal, len1/crc1 is returned (first slot wins ties).
545        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        // Write some data
564        let data = b"hello world";
565        page[..data.len()].copy_from_slice(data);
566
567        // Compute CRC of the data portion
568        let crc = Crc32::checksum(&page[..data.len()]);
569        let record = Checksum::new(data.len() as u16, crc);
570
571        // Write the CRC record at the end
572        let crc_start = physical_page_size - Checksum::SIZE;
573        page[crc_start..].copy_from_slice(&record.to_bytes());
574
575        // Validate - should return Some with the Checksum
576        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        // Write some data
589        let data = b"hello world";
590        page[..data.len()].copy_from_slice(data);
591
592        // Write a record with wrong CRC
593        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        // Should fail validation (return None)
600        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        // Write some data and compute correct CRC
611        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        // Corrupt the data
620        page[0] = 0xFF;
621
622        // Should fail validation (return None)
623        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        // Write data and compute CRC for the larger portion
634        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        // Create a record where len2 has the valid CRC for longer data
639        let record = Checksum {
640            len1: 5,
641            crc1: 0xDEADBEEF, // Invalid CRC for shorter data
642            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        // Should validate using len2/crc2 since len2 > len1
650        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        // Write data
663        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        // Create a record where:
669        // len1 is larger (primary) but INVALID
670        // len2 is smaller (fallback) but VALID
671        let record = Checksum {
672            len1: valid_len + 10, // Larger, so it's primary
673            crc1: 0xBAD1DEA,      // Invalid CRC
674            len2: valid_len,      // Smaller, so it's fallback
675            crc2: valid_crc,      // Valid CRC
676        };
677
678        let crc_start = physical_page_size - Checksum::SIZE;
679        page[crc_start..].copy_from_slice(&record.to_bytes());
680
681        // Should validate using the fallback (len2)
682        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        // Verify that the invalid primary was zeroed out
691        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        // Write some data
702        let data = b"some data";
703        page[..data.len()].copy_from_slice(data);
704
705        // Create a record where:
706        // len1 > 0 (primary) but with INVALID CRC
707        // len2 = 0 (no fallback available)
708        let record = Checksum {
709            len1: data.len() as u16,
710            crc1: 0xBAD1DEA, // Invalid CRC
711            len2: 0,         // No fallback
712            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        // Should fail validation since primary is invalid and no fallback exists
719        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}