Skip to main content

commonware_runtime/utils/buffer/paged/
read.rs

1use super::Checksum;
2use crate::{Blob, Buf, Error, IoBuf};
3use commonware_codec::FixedSize;
4use std::{collections::VecDeque, num::NonZeroU16};
5use tracing::error;
6
7/// State for a single buffer of pages read from the blob.
8///
9/// Each fill produces one `BufferState` containing all pages read in that batch.
10/// Navigation skips CRCs by computing offsets rather than creating separate
11/// `Bytes` slices per page.
12pub(super) struct BufferState {
13    /// The raw physical buffer containing pages with interleaved CRCs.
14    buffer: IoBuf,
15    /// Number of pages in this buffer.
16    num_pages: usize,
17    /// Logical length of the last page (may be partial).
18    last_page_len: usize,
19}
20
21/// Async I/O component that prefetches pages and validates CRCs.
22///
23/// This handles reading batches of pages from the blob, validating their
24/// checksums, and producing `BufferState` for the sync buffering layer.
25pub(super) struct PageReader<B: Blob> {
26    /// The underlying blob to read from.
27    blob: B,
28    /// Physical page size (logical_page_size + CHECKSUM_SIZE).
29    page_size: usize,
30    /// Logical page size (data bytes per page, not including CRC).
31    logical_page_size: usize,
32    /// The physical size of the blob.
33    physical_blob_size: u64,
34    /// The size of the blob.
35    logical_blob_size: u64,
36    /// Next page index to read from the blob.
37    blob_page: u64,
38    /// Number of pages to prefetch at once.
39    prefetch_count: usize,
40}
41
42impl<B: Blob> PageReader<B> {
43    /// Creates a new PageReader.
44    ///
45    /// The `physical_blob_size` must already exclude any trailing invalid data
46    /// (e.g., junk pages from an interrupted write). Each physical page is the same
47    /// size on disk, but the CRC record indicates how much logical data it contains.
48    /// The last page may be logically partial (CRC length < logical page size), but
49    /// all preceding pages must be logically full. A logically partial non-last page
50    /// indicates corruption and will cause an `Error::InvalidChecksum`.
51    pub(super) fn new(
52        blob: B,
53        physical_blob_size: u64,
54        logical_blob_size: u64,
55        prefetch_count: usize,
56        logical_page_size: NonZeroU16,
57    ) -> Self {
58        let logical_page_size = logical_page_size.get() as usize;
59        let page_size = logical_page_size + Checksum::SIZE;
60        let physical_pages = physical_blob_size / page_size as u64;
61        let logical_pages = if logical_blob_size == 0 {
62            0
63        } else {
64            ((logical_blob_size - 1) / logical_page_size as u64) + 1
65        };
66        assert_eq!(physical_blob_size % page_size as u64, 0);
67        assert_eq!(physical_pages, logical_pages);
68
69        Self {
70            blob,
71            page_size,
72            logical_page_size,
73            physical_blob_size,
74            logical_blob_size,
75            blob_page: 0,
76            prefetch_count,
77        }
78    }
79
80    /// Returns the size of the blob.
81    pub(super) const fn blob_size(&self) -> u64 {
82        self.logical_blob_size
83    }
84
85    /// Returns the physical page size.
86    pub(super) const fn page_size(&self) -> usize {
87        self.page_size
88    }
89
90    /// Returns the logical page size.
91    pub(super) const fn logical_page_size(&self) -> usize {
92        self.logical_page_size
93    }
94
95    /// Fills a buffer with the next batch of pages.
96    ///
97    /// Returns `Some((BufferState, logical_bytes))` if data was loaded,
98    /// `None` if no more data available.
99    pub(super) async fn fill(&mut self) -> Result<Option<(BufferState, usize)>, Error> {
100        // Calculate physical read offset
101        let start_offset = match self.blob_page.checked_mul(self.page_size as u64) {
102            Some(o) => o,
103            None => return Err(Error::OffsetOverflow),
104        };
105        if start_offset >= self.physical_blob_size {
106            return Ok(None); // No more data
107        }
108
109        // Calculate how many pages to read
110        let remaining_physical = (self.physical_blob_size - start_offset) as usize;
111        let max_pages = remaining_physical / self.page_size;
112        let pages_to_read = max_pages.min(self.prefetch_count);
113        if pages_to_read == 0 {
114            return Ok(None);
115        }
116        let bytes_to_read = pages_to_read * self.page_size;
117
118        // Read physical data
119        let physical_buf = self
120            .blob
121            .read_at(start_offset, bytes_to_read)
122            .await?
123            .coalesce()
124            .freeze();
125
126        // Validate CRCs and compute total logical bytes
127        let mut total_logical = 0usize;
128        let mut last_len = 0usize;
129        let is_final_batch = pages_to_read == max_pages;
130        for page_idx in 0..pages_to_read {
131            let page_start = page_idx * self.page_size;
132            let page_slice = &physical_buf.as_ref()[page_start..page_start + self.page_size];
133            let Some(record) = Checksum::validate_page(page_slice) else {
134                error!(page = self.blob_page + page_idx as u64, "CRC mismatch");
135                return Err(Error::InvalidChecksum);
136            };
137            let (len, _) = record.get_crc();
138            let len = len as usize;
139
140            // Only the final page in the blob may have partial length
141            let is_last_page_in_blob = is_final_batch && page_idx + 1 == pages_to_read;
142            if !is_last_page_in_blob && len != self.logical_page_size {
143                error!(
144                    page = self.blob_page + page_idx as u64,
145                    expected = self.logical_page_size,
146                    actual = len,
147                    "non-last page has partial length"
148                );
149                return Err(Error::InvalidChecksum);
150            }
151
152            let logical_start = (self.blob_page + page_idx as u64)
153                .checked_mul(self.logical_page_size as u64)
154                .ok_or(Error::OffsetOverflow)?;
155            let logical_remaining = self.logical_blob_size.saturating_sub(logical_start);
156            let logical_remaining_in_page =
157                logical_remaining.min(self.logical_page_size as u64) as usize;
158            let exposed_len = len.min(logical_remaining_in_page);
159
160            total_logical += exposed_len;
161            last_len = exposed_len;
162        }
163        self.blob_page += pages_to_read as u64;
164
165        let state = BufferState {
166            buffer: physical_buf,
167            num_pages: pages_to_read,
168            last_page_len: last_len,
169        };
170
171        Ok(Some((state, total_logical)))
172    }
173}
174
175/// Sync buffering component that implements the `Buf` trait.
176///
177/// This accumulates `BufferState` from multiple fills and provides navigation
178/// across pages while skipping CRCs. Consumed buffers are cleaned up in
179/// `advance()`.
180struct ReplayBuf {
181    /// Physical page size (logical_page_size + CHECKSUM_SIZE).
182    page_size: usize,
183    /// Logical page size (data bytes per page, not including CRC).
184    logical_page_size: usize,
185    /// Accumulated buffers from fills.
186    buffers: VecDeque<BufferState>,
187    /// Current page index within the front buffer.
188    current_page: usize,
189    /// Current offset within the current page's logical data.
190    offset_in_page: usize,
191    /// Total remaining logical bytes across all buffers.
192    remaining: usize,
193}
194
195impl ReplayBuf {
196    /// Creates a new ReplayBuf.
197    const fn new(page_size: usize, logical_page_size: usize) -> Self {
198        Self {
199            page_size,
200            logical_page_size,
201            buffers: VecDeque::new(),
202            current_page: 0,
203            offset_in_page: 0,
204            remaining: 0,
205        }
206    }
207
208    /// Clears the buffer and resets the read offset to 0.
209    fn clear(&mut self) {
210        self.buffers.clear();
211        self.current_page = 0;
212        self.offset_in_page = 0;
213        self.remaining = 0;
214    }
215
216    /// Adds a buffer from a fill operation.
217    fn push(&mut self, state: BufferState, logical_bytes: usize) {
218        // If buffers is empty, this is the first fill after a seek.
219        // Skip bytes before the seek offset (offset_in_page).
220        let skip = if self.buffers.is_empty() {
221            self.offset_in_page
222        } else {
223            0
224        };
225        self.buffers.push_back(state);
226        self.remaining += logical_bytes.saturating_sub(skip);
227    }
228
229    /// Returns the logical length of the given page in the given buffer.
230    const fn page_len(buf: &BufferState, page_idx: usize, logical_page_size: usize) -> usize {
231        if page_idx + 1 == buf.num_pages {
232            buf.last_page_len
233        } else {
234            logical_page_size
235        }
236    }
237}
238
239impl Buf for ReplayBuf {
240    fn remaining(&self) -> usize {
241        self.remaining
242    }
243
244    fn chunk(&self) -> &[u8] {
245        let Some(buf) = self.buffers.front() else {
246            return &[];
247        };
248        if self.current_page >= buf.num_pages {
249            return &[];
250        }
251        let page_len = Self::page_len(buf, self.current_page, self.logical_page_size);
252        let physical_start = self.current_page * self.page_size + self.offset_in_page;
253        let physical_end = self.current_page * self.page_size + page_len;
254        &buf.buffer.as_ref()[physical_start..physical_end]
255    }
256
257    fn advance(&mut self, mut cnt: usize) {
258        self.remaining = self.remaining.saturating_sub(cnt);
259
260        while cnt > 0 {
261            let Some(buf) = self.buffers.front() else {
262                break;
263            };
264
265            // Advance within current buffer
266            while cnt > 0 && self.current_page < buf.num_pages {
267                let page_len = Self::page_len(buf, self.current_page, self.logical_page_size);
268                let available = page_len - self.offset_in_page;
269                if cnt < available {
270                    self.offset_in_page += cnt;
271                    return;
272                }
273                cnt -= available;
274                self.current_page += 1;
275                self.offset_in_page = 0;
276            }
277
278            // Current buffer exhausted, move to next
279            if self.current_page >= buf.num_pages {
280                self.buffers.pop_front();
281                self.current_page = 0;
282                self.offset_in_page = 0;
283            }
284        }
285    }
286}
287
288/// Replays logical data from a blob containing pages with interleaved CRCs.
289///
290/// This combines async I/O (`PageReader`) with sync buffering (`ReplayBuf`)
291/// to provide an `ensure(n)` + `Buf` interface for codec decoding.
292pub struct Replay<B: Blob> {
293    /// Async I/O component.
294    reader: PageReader<B>,
295    /// Sync buffering component.
296    buffer: ReplayBuf,
297    /// Whether the blob has been fully read.
298    exhausted: bool,
299}
300
301impl<B: Blob> Replay<B> {
302    /// Creates a new Replay from a PageReader.
303    pub(super) const fn new(reader: PageReader<B>) -> Self {
304        let page_size = reader.page_size();
305        let logical_page_size = reader.logical_page_size();
306        Self {
307            reader,
308            buffer: ReplayBuf::new(page_size, logical_page_size),
309            exhausted: false,
310        }
311    }
312
313    /// Returns the size of the blob.
314    pub const fn blob_size(&self) -> u64 {
315        self.reader.blob_size()
316    }
317
318    /// Returns true if the reader has been exhausted (no more pages to read).
319    ///
320    /// When exhausted, the buffer may still contain data that hasn't been consumed.
321    /// Callers should check `remaining()` to see if there's data left to process.
322    pub const fn is_exhausted(&self) -> bool {
323        self.exhausted
324    }
325
326    /// Ensures at least `n` bytes are available in the buffer.
327    ///
328    /// This method fills the buffer from the blob until either:
329    /// - At least `n` bytes are available (returns `Ok(true)`)
330    /// - The blob is exhausted with fewer than `n` bytes (returns `Ok(false)`)
331    /// - A read error occurs (returns `Err`)
332    ///
333    /// When `Ok(false)` is returned, callers should still attempt to process
334    /// the remaining bytes in the buffer (check `remaining()`), as they may
335    /// contain valid data that doesn't require the full `n` bytes.
336    pub async fn ensure(&mut self, n: usize) -> Result<bool, Error> {
337        while self.buffer.remaining < n && !self.exhausted {
338            match self.reader.fill().await? {
339                Some((state, logical_bytes)) => {
340                    self.buffer.push(state, logical_bytes);
341                }
342                None => {
343                    self.exhausted = true;
344                }
345            }
346        }
347        Ok(self.buffer.remaining >= n)
348    }
349
350    /// Seeks to `offset` in the blob, returning `Err(BlobInsufficientLength)` if `offset` exceeds
351    /// the blob size.
352    pub fn seek_to(&mut self, offset: u64) -> Result<(), Error> {
353        if offset > self.reader.blob_size() {
354            return Err(Error::BlobInsufficientLength);
355        }
356
357        self.buffer.clear();
358        self.exhausted = false;
359
360        let page_size = self.reader.logical_page_size as u64;
361        self.reader.blob_page = offset / page_size;
362        self.buffer.current_page = 0;
363        self.buffer.offset_in_page = (offset % page_size) as usize;
364
365        Ok(())
366    }
367}
368
369impl<B: Blob> Buf for Replay<B> {
370    fn remaining(&self) -> usize {
371        self.buffer.remaining()
372    }
373
374    fn chunk(&self) -> &[u8] {
375        self.buffer.chunk()
376    }
377
378    fn advance(&mut self, cnt: usize) {
379        self.buffer.advance(cnt);
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::{super::writer::Writer, *};
386    use crate::{deterministic, Runner as _, Storage as _};
387    use commonware_macros::test_traced;
388    use commonware_utils::{NZUsize, NZU16};
389
390    const PAGE_SIZE: NonZeroU16 = NZU16!(103);
391    const BUFFER_PAGES: usize = 2;
392
393    #[test_traced("DEBUG")]
394    fn test_replay_basic() {
395        let executor = deterministic::Runner::default();
396        executor.start(|context: deterministic::Context| async move {
397            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
398            assert_eq!(blob_size, 0);
399
400            let cache_ref =
401                super::super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_PAGES));
402            let mut append = Writer::new(blob.clone(), blob_size, BUFFER_PAGES * 115, cache_ref)
403                .await
404                .unwrap();
405
406            // Write data spanning multiple pages
407            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
408            append.append(&data).await.unwrap();
409            append.sync().await.unwrap();
410
411            // Create Replay
412            let mut replay = append.replay(NZUsize!(BUFFER_PAGES)).await.unwrap();
413
414            // Ensure all data is available
415            replay.ensure(300).await.unwrap();
416
417            // Verify we got all the data
418            assert_eq!(replay.remaining(), 300);
419
420            // Read all data via Buf interface
421            let mut collected = Vec::new();
422            while replay.remaining() > 0 {
423                let chunk = replay.chunk();
424                collected.extend_from_slice(chunk);
425                let len = chunk.len();
426                replay.advance(len);
427            }
428            assert_eq!(collected, data);
429        });
430    }
431
432    #[test_traced("DEBUG")]
433    fn test_replay_partial_page() {
434        let executor = deterministic::Runner::default();
435        executor.start(|context: deterministic::Context| async move {
436            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
437
438            let cache_ref =
439                super::super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_PAGES));
440            let mut append = Writer::new(blob.clone(), blob_size, BUFFER_PAGES * 115, cache_ref)
441                .await
442                .unwrap();
443
444            // Write data that doesn't fill the last page
445            let data: Vec<u8> = (1u8..=(PAGE_SIZE.get() + 10) as u8).collect();
446            append.append(&data).await.unwrap();
447            append.sync().await.unwrap();
448
449            let mut replay = append.replay(NZUsize!(BUFFER_PAGES)).await.unwrap();
450
451            // Ensure all data is available
452            replay.ensure(data.len()).await.unwrap();
453
454            assert_eq!(replay.remaining(), data.len());
455        });
456    }
457
458    #[test_traced("DEBUG")]
459    fn test_replay_cross_buffer_boundary() {
460        // Use prefetch_count=1 to force separate BufferStates per page.
461        // This tests navigation across multiple BufferStates in the VecDeque.
462        let executor = deterministic::Runner::default();
463        executor.start(|context: deterministic::Context| async move {
464            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
465            assert_eq!(blob_size, 0);
466
467            let cache_ref =
468                super::super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_PAGES));
469            let mut append = Writer::new(blob.clone(), blob_size, BUFFER_PAGES * 115, cache_ref)
470                .await
471                .unwrap();
472
473            // Write data spanning 4 pages (4 * 103 = 412 bytes, with last page partial)
474            let data: Vec<u8> = (0u8..=255).cycle().take(400).collect();
475            append.append(&data).await.unwrap();
476            append.sync().await.unwrap();
477
478            // Create Replay with buffer size that results in prefetch_count=1.
479            // Physical page size = 103 + 12 = 115 bytes.
480            // Buffer size of 115 gives prefetch_pages = 115/115 = 1.
481            let mut replay = append.replay(NZUsize!(115)).await.unwrap();
482
483            // Ensure all data - this requires 4 separate fill() calls (one per page).
484            // Each fill() creates a new BufferState, so we'll have 4 BufferStates.
485            assert!(replay.ensure(400).await.unwrap());
486            assert_eq!(replay.remaining(), 400);
487
488            // Read all data via Buf interface, verifying navigation across BufferStates.
489            let mut collected = Vec::new();
490            let mut chunks_read = 0;
491            while replay.remaining() > 0 {
492                let chunk = replay.chunk();
493                assert!(
494                    !chunk.is_empty(),
495                    "chunk() returned empty but remaining > 0"
496                );
497                collected.extend_from_slice(chunk);
498                let len = chunk.len();
499                replay.advance(len);
500                chunks_read += 1;
501            }
502
503            assert_eq!(collected, data);
504            // With prefetch_count=1 and 4 pages, we expect at least 4 chunks
505            // (one per page, though partial reads could result in more).
506            assert!(
507                chunks_read >= 4,
508                "Expected at least 4 chunks for 4 pages, got {}",
509                chunks_read
510            );
511        });
512    }
513
514    #[test_traced("DEBUG")]
515    fn test_replay_empty_blob() {
516        // Test that replaying an empty blob works correctly.
517        // ensure() should return Ok(false) when no data is available.
518        let executor = deterministic::Runner::default();
519        executor.start(|context: deterministic::Context| async move {
520            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
521            assert_eq!(blob_size, 0);
522
523            let cache_ref =
524                super::super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_PAGES));
525            let mut append = Writer::new(blob.clone(), blob_size, BUFFER_PAGES * 115, cache_ref)
526                .await
527                .unwrap();
528
529            // Don't write any data - blob remains empty
530            assert_eq!(append.size(), 0);
531
532            // Create Replay on empty blob
533            let mut replay = append.replay(NZUsize!(BUFFER_PAGES)).await.unwrap();
534
535            // Verify initial state - remaining is 0, but not yet marked exhausted
536            // (exhausted is set after first fill attempt)
537            assert_eq!(replay.remaining(), 0);
538
539            // ensure(0) should succeed (we have >= 0 bytes)
540            assert!(replay.ensure(0).await.unwrap());
541
542            // ensure(1) should return Ok(false) - not enough data, and marks exhausted
543            assert!(!replay.ensure(1).await.unwrap());
544
545            // Now should be marked as exhausted after the fill attempt
546            assert!(replay.is_exhausted());
547
548            // chunk() should return empty slice
549            assert!(replay.chunk().is_empty());
550
551            // remaining should still be 0
552            assert_eq!(replay.remaining(), 0);
553        });
554    }
555
556    #[test_traced("DEBUG")]
557    fn test_replay_seek_to() {
558        let executor = deterministic::Runner::default();
559        executor.start(|context: deterministic::Context| async move {
560            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
561
562            let cache_ref =
563                super::super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_PAGES));
564            let mut append = Writer::new(blob.clone(), blob_size, BUFFER_PAGES * 115, cache_ref)
565                .await
566                .unwrap();
567
568            // Write data spanning multiple pages
569            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
570            append.append(&data).await.unwrap();
571            append.sync().await.unwrap();
572
573            let mut replay = append.replay(NZUsize!(BUFFER_PAGES)).await.unwrap();
574
575            // Seek forward, read, then seek backward
576            replay.seek_to(150).unwrap();
577            replay.ensure(50).await.unwrap();
578            assert_eq!(replay.get_u8(), data[150]);
579
580            // Seek back to start
581            replay.seek_to(0).unwrap();
582            replay.ensure(1).await.unwrap();
583            assert_eq!(replay.get_u8(), data[0]);
584
585            // Seek beyond blob size should error
586            assert!(replay.seek_to(data.len() as u64 + 1).is_err());
587
588            // Test that remaining() is correct after seek by reading all data.
589            let seek_offset = 150usize;
590            replay.seek_to(seek_offset as u64).unwrap();
591            let expected_remaining = data.len() - seek_offset;
592            // Read all bytes and verify content
593            let mut collected = Vec::new();
594            loop {
595                // Load more data if needed
596                if !replay.ensure(1).await.unwrap() {
597                    break; // No more data available
598                }
599                let chunk = replay.chunk();
600                if chunk.is_empty() {
601                    break;
602                }
603                collected.extend_from_slice(chunk);
604                let len = chunk.len();
605                replay.advance(len);
606            }
607            assert_eq!(
608                collected.len(),
609                expected_remaining,
610                "After seeking to {}, should read {} bytes but got {}",
611                seek_offset,
612                expected_remaining,
613                collected.len()
614            );
615            assert_eq!(collected, &data[seek_offset..]);
616        });
617    }
618}