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
7//! cache: the payload bytes stored per page. A _physical page_ is what a page occupies on disk:
8//! the logical page followed by a 12-byte _CRC record_ containing:
9//!
10//! ```text
11//! | len1 (2 bytes) |  crc1 (4 bytes) | len2 (2 bytes) | crc2 (4 bytes) |
12//! ```
13//!
14//! Throughout this module, an unqualified page size always denotes the logical size (matching
15//! the configured value); only physical sizes carry a qualified `physical_page_size` name.
16//!
17//! # Blob-page alignment
18//!
19//! Physical page `p` begins at blob offset `p * physical_page_size`, and a blob created with the
20//! default layout ([crate::DEFAULT_BLOB_LAYOUT]) begins its data on a blob-page boundary (see
21//! [crate::BLOB_PAGE_SIZE]). Choosing a logical page size such that the physical page size is a
22//! power of two (see [page_size]) therefore makes every physical page either fit within a single
23//! blob page or start on a blob-page boundary and span whole blob pages. Blobs with the unaligned
24//! [crate::BlobLayout::V0] layout begin their data at offset 8 and never align, regardless of the
25//! page size chosen.
26//!
27//! Alignment is a performance property, not a correctness requirement: any page size works, but
28//! physical pages that straddle blob-page boundaries amplify cold random reads.
29//!
30//! Two checksums are stored so that re-writing a partial page cannot destroy the valid checksum
31//! for its last durable contents. Each rewrite covers the whole physical page: the new checksum
32//! lands in the slot not protecting the durable contents, while the durable prefix and its
33//! protected checksum are resubmitted byte-identically, leaving their durable bytes unchanged
34//! even if the write tears. A checksum over a page is computed over the first [0,len) bytes in
35//! the page, with all other bytes in the page ignored. Ordinary partial-page payload writes
36//! 0-pad the range [len, page_size), but recovery does not depend on bytes outside [0,len). A
37//! checksum with length 0 is never considered valid. If both checksums are valid for the page,
38//! the one with the larger `len` is considered authoritative. Partial-page shrink first makes
39//! the shorter checksum durable in the alternate slot, then invalidates the old longer checksum.
40//!
41//! A _full_ page is one whose crc stores a len equal to the logical page size. Otherwise the page
42//! is called _partial_. All pages in a blob are full except for the very last page, which can be
43//! full or partial. A partial page's durable prefix remains recoverable while it is rewritten.
44
45use 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
65/// Size in bytes of the checksum record appended to each logical page.
66pub 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
71/// The logical page size whose physical page occupies exactly `physical_page_size` bytes on disk
72/// (see the module docs on blob-page alignment).
73///
74/// This selects a page size for a store. It is not a migration path: a store that already holds
75/// data cannot be reopened under a different page size, as the mismatched pages fail their
76/// integrity check and reopening for writing silently truncates them. Changing page size is a
77/// destructive format migration.
78///
79/// # Panics
80///
81/// Panics if `physical_page_size` is not a power of two, does not exceed the CRC record size,
82/// or yields a logical size that does not fit a `u16`, so misconfiguration is caught at
83/// construction (or compile time, in const contexts).
84pub 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/// Validate a physical page's CRC record, exposing [Checksum::validate_page] to tests elsewhere
105/// in the crate.
106#[cfg(test)]
107pub(crate) fn validate_page_for_tests(page: &[u8]) -> bool {
108    Checksum::validate_page(page).is_some()
109}
110
111/// Select a physical page's authoritative checksum slot, falling back to the other slot if a
112/// write tore, and return the CRC-validated logical length (or `None` when neither slot
113/// verifies).
114///
115/// `page` is one raw physical page: `logical_page_size` bytes followed by the checksum record.
116/// This deliberately re-derives the slot arbitration instead of calling the production
117/// validator so fuzz oracles built on it do not trust the reader they are checking.
118#[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/// Flip one byte inside physical page `page` of the blob at `name`, leaving every other page
145/// valid. Models a torn interior page: a crash during an in-flight fsync can lose an interior
146/// page while later pages persist. Physical pages are the logical page plus the checksum record.
147#[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    // Every valid checksum slot covers byte zero, including a shorter fallback slot.
156    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    // A complete physical page must follow the target: a trailing truncated physical page
161    // can never validate, so a target followed only by one would be the last validatable
162    // page.
163    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
184/// Ensure every requested range lies within the blob's size.
185///
186/// # Panics
187///
188/// Panics if `buf` does not hold one slot per range totaling its length, or if ranges are not
189/// sorted and non-overlapping.
190fn 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
222/// Return the blob offset and length of physical page `page_num` for `page_size`-byte logical
223/// pages.
224fn 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
235/// Validate a physical page read from a blob and return its logical bytes and validated checksum.
236fn 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
243/// Read the designated page and return both its logical bytes and validated checksum.
244async 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/// One of a page footer's two CRC slots, laid out back to back after the page data.
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257enum Slot {
258    First,
259    Second,
260}
261
262impl Slot {
263    /// Byte offset of this slot within the page's CRC footer.
264    const fn offset(self) -> usize {
265        match self {
266            Self::First => 0,
267            Self::Second => CHECKSUM_SLOT_SIZE,
268        }
269    }
270
271    /// The other slot.
272    const fn other(self) -> Self {
273        match self {
274            Self::First => Self::Second,
275            Self::Second => Self::First,
276        }
277    }
278}
279
280/// The checksum covering a page's logical bytes and the footer slot that holds it.
281#[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
294/// Describes a CRC record stored at the end of a page.
295///
296/// The CRC with the larger length is authoritative. Two slots let a partial-page rewrite preserve
297/// the checksum covering the previously committed bytes while writing the new checksum elsewhere.
298struct Checksum {
299    len1: u16,
300    crc1: u32,
301    len2: u16,
302    crc2: u32,
303}
304
305impl Checksum {
306    /// Create a new CRC record with the given length and CRC.
307    /// The new CRC is stored in the first slot (len1/crc1), with the second slot zeroed.
308    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    /// The slot holding the authoritative (longer) CRC; the first slot wins ties.
318    const fn authoritative(&self) -> Slot {
319        if self.len1 >= self.len2 {
320            Slot::First
321        } else {
322            Slot::Second
323        }
324    }
325
326    /// Return the active checksum if the page is valid. The provided slice is assumed to be exactly
327    /// the size of a physical page.
328    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        // Decode the CRC record from the page footer. The size guard above guarantees all of its
340        // bytes are present, and every bit pattern decodes, so the read cannot fail.
341        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        // Prefer the authoritative slot: when both slots are valid, it covers the most recently
346        // committed contents of the page.
347        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        // An interrupted write can corrupt only the slot it was rewriting. The other slot still
353        // covers the page's previously committed contents.
354        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    /// Validate one slot independently of the footer's authority ordering.
363    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        // A zero length marks an inactive checksum slot (committed pages are never empty).
373        // This also rejects zero-filled physical pages from unwritten storage.
374        if len_usize == 0 {
375            return None;
376        }
377
378        // The checksum must cover only logical page bytes, not the checksum footer itself.
379        if len_usize > crc_start_idx {
380            return None;
381        }
382
383        // The recorded checksum must match the claimed logical prefix.
384        if Crc32::checksum(&buf[..len_usize]) != crc {
385            return None;
386        }
387        Some(ActiveChecksum::new(slot, len, crc))
388    }
389
390    /// Return one checksum slot without considering authority.
391    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    /// Returns the CRC record in its storage representation.
399    fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
400        self.encode_fixed()
401    }
402
403    /// Encode a whole checksum slot (`[len: u16][crc: u32]`) in its storage representation.
404    ///
405    /// A page footer holds two slots; recovery treats the one with the larger `len` as
406    /// authoritative. A `len` of 0 is never authoritative.
407    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    /// Encode just a slot's leading `len` field (the first [`CHECKSUM_SLOT_LEN_SIZE`] bytes of
416    /// [`Self::slot_bytes`]).
417    ///
418    /// Because `len` decides which slot is authoritative, rewriting only this field commits a
419    /// previously staged slot without disturbing its already-durable CRC. Retiring a slot must
420    /// instead zero it entirely with [`Self::slot_bytes`]: a zero length with a durable CRC left
421    /// behind could be reassembled into the retired checksum by a later torn rewrite.
422    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        // Verify big-endian encoding
571        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        // The first slot wins ties.
606        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        // Write some data
623        let data = b"hello world";
624        page[..data.len()].copy_from_slice(data);
625
626        // Compute CRC of the data portion
627        let crc = Crc32::checksum(&page[..data.len()]);
628        let record = Checksum::new(data.len() as u16, crc);
629
630        // Write the CRC record at the end
631        let crc_start = physical_page_size - Checksum::SIZE;
632        page[crc_start..].copy_from_slice(&record.to_bytes());
633
634        // Validate - should return the active checksum
635        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        // Write some data
647        let data = b"hello world";
648        page[..data.len()].copy_from_slice(data);
649
650        // Write a record with wrong CRC
651        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        // Should fail validation (return None)
658        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        // Write some data and compute correct CRC
669        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        // Corrupt the data
678        page[0] = 0xFF;
679
680        // Should fail validation (return None)
681        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        // Write data and compute CRC for the larger portion
692        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        // Create a record where len2 has the valid CRC for longer data
697        let record = Checksum {
698            len1: 5,
699            crc1: 0xDEADBEEF, // Invalid CRC for shorter data
700            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        // Should validate using len2/crc2 since len2 > len1
708        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        // Write data
720        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        // Create a record where:
726        // len1 is larger (primary) but INVALID
727        // len2 is smaller (fallback) but VALID
728        let record = Checksum {
729            len1: valid_len + 10, // Larger, so it's primary
730            crc1: 0xBAD1DEA,      // Invalid CRC
731            len2: valid_len,      // Smaller, so it's fallback
732            crc2: valid_crc,      // Valid CRC
733        };
734
735        let crc_start = physical_page_size - Checksum::SIZE;
736        page[crc_start..].copy_from_slice(&record.to_bytes());
737
738        // Should validate using the fallback (len2)
739        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        // Write some data
756        let data = b"some data";
757        page[..data.len()].copy_from_slice(data);
758
759        // Create a record where:
760        // len1 > 0 (primary) but with INVALID CRC
761        // len2 = 0 (no fallback available)
762        let record = Checksum {
763            len1: data.len() as u16,
764            crc1: 0xBAD1DEA, // Invalid CRC
765            len2: 0,         // No fallback
766            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        // Should fail validation since primary is invalid and no fallback exists
773        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}