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//! # Storage-page alignment
18//!
19//! Physical page `p` begins at blob offset `p * physical_page_size`, and a blob created with
20//! the default layout ([crate::DEFAULT_BLOB_LAYOUT]) begins its data on a 4096-byte boundary.
21//! Choosing a logical page size such that the physical page size is a power of two (see
22//! [page_size]) therefore makes every physical page either fit within a single 4096-byte
23//! storage page or start on a 4096-byte boundary and span whole storage pages. Blobs with the
24//! unaligned [crate::BlobLayout::V0] layout begin their data at offset 8 and never align,
25//! regardless of the page size chosen.
26//!
27//! Alignment is a performance property, not a correctness requirement: any page size works, but
28//! physical pages that straddle storage-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, 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
64/// Size in bytes of the checksum record appended to each logical page.
65pub const CHECKSUM_SIZE: u64 = Checksum::SIZE as u64;
66
67/// The storage-page granularity physical pages should align to (see the module docs).
68pub(crate) const STORAGE_PAGE_SIZE: u64 = 4096;
69
70// The alignment reasoning above assumes blobs created with the default layout place their
71// data on a storage-page boundary.
72const _: () = 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
81/// The logical page size whose physical page occupies exactly `physical_page_size` bytes on disk
82/// (see the module docs on storage-page alignment).
83///
84/// This selects a page size for a store. It is not a migration path: a store that already holds
85/// data cannot be reopened under a different page size, as the mismatched pages fail their
86/// integrity check and reopening for writing silently truncates them. Changing page size is a
87/// destructive format migration.
88///
89/// # Panics
90///
91/// Panics if `physical_page_size` is not a power of two, does not exceed the CRC record size,
92/// or yields a logical size that does not fit a `u16`, so misconfiguration is caught at
93/// construction (or compile time, in const contexts).
94pub 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/// Validate a physical page's CRC record, exposing [Checksum::validate_page] to tests elsewhere
115/// in the crate.
116#[cfg(test)]
117pub(crate) fn validate_page_for_tests(page: &[u8]) -> bool {
118    Checksum::validate_page(page).is_some()
119}
120
121/// Select a physical page's authoritative checksum slot, falling back to the other slot if a
122/// write tore, and return the CRC-validated logical length (or `None` when neither slot
123/// verifies).
124///
125/// `page` is one raw physical page: `logical_page_size` bytes followed by the checksum record.
126/// This deliberately re-derives the slot arbitration instead of calling the production
127/// validator so fuzz oracles built on it do not trust the reader they are checking.
128#[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/// Flip one byte inside physical page `page` of the blob at `name`, leaving every other page
155/// valid. Models a torn interior page: a crash during an in-flight fsync can lose an interior
156/// page while later pages persist. Physical pages are the logical page plus the checksum record.
157#[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    // Every valid checksum slot covers byte zero, including a shorter fallback slot.
166    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    // A complete physical page must follow the target: a trailing truncated physical page
171    // can never validate, so a target followed only by one would be the last validatable
172    // page.
173    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
194/// Ensure every requested range lies within the blob's size.
195///
196/// # Panics
197///
198/// Panics if `buf` does not hold one slot per range totaling its length, or if ranges are not
199/// sorted and non-overlapping.
200fn 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
232/// Partition a batch of variable-length range reads into bytes copied from the in-memory tail
233/// and ranges that need cache/blob reads.
234///
235/// `buf` holds one slot per range, back to back (validated by [validate_read_ranges]). `tail`
236/// holds the logical bytes at `[tail_offset, tail_offset + tail.len())`; for [Writer] this is the
237/// tip buffer, for [Sealed] the partial last page. Ranges entirely within `tail` are copied into
238/// place. Ranges fully or partially below `tail_offset` are returned as `(dest_slice, offset)`
239/// pairs for the caller to read from the page cache or blob. `split_at_mut` yields disjoint
240/// per-range slots, so returned slices never alias.
241fn 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            // Entirely below the tail bytes, so this needs a cache/blob read.
257            cache_ranges.push((slot, offset));
258        } else if offset >= tail_offset {
259            // Entirely within the tail bytes.
260            let src = (offset - tail_offset) as usize;
261            slot.copy_from_slice(&tail[src..src + len]);
262        } else {
263            // Straddles the boundary: copy the suffix from the tail bytes, record the prefix
264            // for a cache/blob read.
265            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
274/// Read the designated page from the underlying blob and return its logical bytes as a vector if it
275/// passes the integrity check, returning error otherwise. Safely handles partial pages. Caller can
276/// check the length of the returned vector to determine if the page was partial vs full.
277async 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
288/// Read the designated page and return both its logical bytes and validated checksum.
289async 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/// One of a page footer's two CRC slots, laid out back to back after the page data.
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320enum Slot {
321    First,
322    Second,
323}
324
325impl Slot {
326    /// Byte offset of this slot within the page's CRC footer.
327    const fn offset(self) -> usize {
328        match self {
329            Self::First => 0,
330            Self::Second => CHECKSUM_SLOT_SIZE,
331        }
332    }
333
334    /// The other slot.
335    const fn other(self) -> Self {
336        match self {
337            Self::First => Self::Second,
338            Self::Second => Self::First,
339        }
340    }
341}
342
343/// The checksum covering a page's logical bytes and the footer slot that holds it.
344#[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
357/// Describes a CRC record stored at the end of a page.
358///
359/// The CRC with the larger length is authoritative. Two slots let a partial-page rewrite preserve
360/// the checksum covering the previously committed bytes while writing the new checksum elsewhere.
361struct Checksum {
362    len1: u16,
363    crc1: u32,
364    len2: u16,
365    crc2: u32,
366}
367
368impl Checksum {
369    /// Create a new CRC record with the given length and CRC.
370    /// The new CRC is stored in the first slot (len1/crc1), with the second slot zeroed.
371    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    /// The slot holding the authoritative (longer) CRC; the first slot wins ties.
381    const fn authoritative(&self) -> Slot {
382        if self.len1 >= self.len2 {
383            Slot::First
384        } else {
385            Slot::Second
386        }
387    }
388
389    /// Return the active checksum if the page is valid. The provided slice is assumed to be exactly
390    /// the size of a physical page.
391    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        // Decode the CRC record from the page footer. The size guard above guarantees all of its
403        // bytes are present, and every bit pattern decodes, so the read cannot fail.
404        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        // Prefer the authoritative slot: when both slots are valid, it covers the most recently
409        // committed contents of the page.
410        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        // An interrupted write can corrupt only the slot it was rewriting. The other slot still
416        // covers the page's previously committed contents.
417        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    /// Validate one slot independently of the footer's authority ordering.
426    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        // A zero length marks an inactive checksum slot (committed pages are never empty).
436        // This also rejects zero-filled physical pages from unwritten storage.
437        if len_usize == 0 {
438            return None;
439        }
440
441        // The checksum must cover only logical page bytes, not the checksum footer itself.
442        if len_usize > crc_start_idx {
443            return None;
444        }
445
446        // The recorded checksum must match the claimed logical prefix.
447        if Crc32::checksum(&buf[..len_usize]) != crc {
448            return None;
449        }
450        Some(ActiveChecksum::new(slot, len, crc))
451    }
452
453    /// Return one checksum slot without considering authority.
454    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    /// Returns the CRC record in its storage representation.
462    fn to_bytes(&self) -> [u8; CHECKSUM_SIZE as usize] {
463        self.encode_fixed()
464    }
465
466    /// Encode a whole checksum slot (`[len: u16][crc: u32]`) in its storage representation.
467    ///
468    /// A page footer holds two slots; recovery treats the one with the larger `len` as
469    /// authoritative. A `len` of 0 is never authoritative.
470    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    /// Encode just a slot's leading `len` field (the first [`CHECKSUM_SLOT_LEN_SIZE`] bytes of
479    /// [`Self::slot_bytes`]).
480    ///
481    /// Because `len` decides which slot is authoritative, rewriting only this field commits a
482    /// previously staged slot without disturbing its already-durable CRC. Retiring a slot must
483    /// instead zero it entirely with [`Self::slot_bytes`]: a zero length with a durable CRC left
484    /// behind could be reassembled into the retired checksum by a later torn rewrite.
485    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        // Verify big-endian encoding
634        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        // The first slot wins ties.
669        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        // Write some data
686        let data = b"hello world";
687        page[..data.len()].copy_from_slice(data);
688
689        // Compute CRC of the data portion
690        let crc = Crc32::checksum(&page[..data.len()]);
691        let record = Checksum::new(data.len() as u16, crc);
692
693        // Write the CRC record at the end
694        let crc_start = physical_page_size - Checksum::SIZE;
695        page[crc_start..].copy_from_slice(&record.to_bytes());
696
697        // Validate - should return the active checksum
698        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        // Write some data
710        let data = b"hello world";
711        page[..data.len()].copy_from_slice(data);
712
713        // Write a record with wrong CRC
714        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        // Should fail validation (return None)
721        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        // Write some data and compute correct CRC
732        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        // Corrupt the data
741        page[0] = 0xFF;
742
743        // Should fail validation (return None)
744        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        // Write data and compute CRC for the larger portion
755        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        // Create a record where len2 has the valid CRC for longer data
760        let record = Checksum {
761            len1: 5,
762            crc1: 0xDEADBEEF, // Invalid CRC for shorter data
763            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        // Should validate using len2/crc2 since len2 > len1
771        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        // Write data
783        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        // Create a record where:
789        // len1 is larger (primary) but INVALID
790        // len2 is smaller (fallback) but VALID
791        let record = Checksum {
792            len1: valid_len + 10, // Larger, so it's primary
793            crc1: 0xBAD1DEA,      // Invalid CRC
794            len2: valid_len,      // Smaller, so it's fallback
795            crc2: valid_crc,      // Valid CRC
796        };
797
798        let crc_start = physical_page_size - Checksum::SIZE;
799        page[crc_start..].copy_from_slice(&record.to_bytes());
800
801        // Should validate using the fallback (len2)
802        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        // Write some data
819        let data = b"some data";
820        page[..data.len()].copy_from_slice(data);
821
822        // Create a record where:
823        // len1 > 0 (primary) but with INVALID CRC
824        // len2 = 0 (no fallback available)
825        let record = Checksum {
826            len1: data.len() as u16,
827            crc1: 0xBAD1DEA, // Invalid CRC
828            len2: 0,         // No fallback
829            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        // Should fail validation since primary is invalid and no fallback exists
836        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}