Skip to main content

commonware_runtime/utils/buffer/paged/
writer.rs

1//! Single-owner, append-only access to a [Blob].
2//!
3//! A [Writer] exclusively owns its blob and cannot be cloned. Appended bytes can be read back
4//! immediately but are not durable until a sync completes ([Writer::sync], or the handles
5//! returned by [Writer::start_sync] and [Writer::seal]).
6//!
7//! # Snapshot
8//!
9//! [Writer::snapshot] returns the writer with a logical read view.
10//!
11//! # Seal
12//!
13//! [Writer::seal] ends writing, returning an immutable [super::Sealed] view plus a completion
14//! handle for the sync it starts.
15//!
16//! # Paging
17//!
18//! Callers append and read logical bytes; the blob stores physical pages in the format described
19//! in [`super`]. Appends accumulate in a write buffer and reach the blob in pages. Buffered bytes
20//! are readable immediately but durable only after `sync`. Full pages read from the blob are
21//! cached in a shared page cache, so reads are served from the write buffer, the page cache, or
22//! the blob itself. Large appends bypass the write buffer and write whole pages directly to the
23//! blob.
24//!
25//! # Checksums
26//!
27//! Each physical page ends in a two-slot CRC record. The slots let a partial page be rewritten
28//! without clobbering its last durable contents, so an interrupted write loses at most unsynced
29//! bytes. [Writer::new] backs up over any trailing bytes not covered by a valid checksum,
30//! treating them as an incomplete write.
31//!
32//! # Recovery
33//!
34//! [Recovery::open] trims an invalid tail and [Recovery::truncate] makes the selected prefix
35//! durable before returning. Conversion into an append-only [Writer] gives up permission to
36//! shorten the blob.
37//!
38//! # Raw [Blob] handles
39//!
40//! The [Writer] owns the page layout, page cache entries, and durability bookkeeping of its
41//! [Blob]. Raw handles shared before the writer existed see physical bytes, including CRC
42//! records, and do not observe buffered bytes until they are flushed. They must not mutate the
43//! blob while a [Writer] exists: such writes bypass the write buffer and page cache and can
44//! invalidate checksum recovery.
45
46use super::{
47    Sealed,
48    read::{PageReader, Replay},
49    tip::Buffer,
50    view::{Tail, View},
51};
52use crate::{
53    Blob, Error, Handle, IoBuf, IoBufMut, IoBufs, ReadOptions, WriteOptions,
54    buffer::{
55        SyncState,
56        paged::{ActiveChecksum, CHECKSUM_SIZE, CacheRef, Checksum, Slot},
57    },
58};
59use bytes::BufMut;
60use commonware_codec::{EncodeSize, Write};
61use commonware_cryptography::Crc32;
62use commonware_utils::Widen;
63use std::{
64    marker::PhantomData,
65    num::{NonZeroU16, NonZeroUsize},
66    sync::Arc,
67};
68use tracing::warn;
69
70/// Adjusts a requested write-buffer `capacity` upward to the value the buffer actually uses,
71/// applying two upward adjustments:
72///
73/// - Rounds up to a whole multiple of `page_size`, so the buffer always holds an exact number of
74///   pages. Callers can then drain and bulk-cache full pages without re-rounding the capacity.
75/// - Raises the result to a floor of two pages, so the buffer can hold at least one full page of
76///   new data even while caching a nearly-full page of already written data.
77fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
78    let page_size = page_size as usize;
79    let rounded = capacity.next_multiple_of(page_size);
80    let floor = page_size * 2;
81    if rounded < floor {
82        warn!(
83            floor,
84            "requested buffer capacity is too low, increasing it to floor"
85        );
86    }
87    rounded.max(floor)
88}
89
90/// Returns whether appending `append_len` bytes should bypass the write buffer and write whole
91/// pages directly: the append would overflow capacity, and at least one whole page remains to
92/// write after filling the current page up to a boundary.
93///
94/// Larger appends bypass the buffer, so a buffered append exceeds `capacity` by less than one
95/// page (given `capacity` is a whole number of pages; see [adjusted_capacity]). The write
96/// buffer's peak size therefore stays under `capacity + page_size`.
97const fn too_big_for_buffer(
98    buffer_len: usize,
99    buffer_capacity: usize,
100    append_len: usize,
101    page_size: usize,
102) -> bool {
103    let fill = buffer_len.next_multiple_of(page_size) - buffer_len;
104    let overflows_capacity = buffer_len + append_len > buffer_capacity;
105    let has_full_page_after_fill = append_len >= fill + page_size;
106
107    overflows_capacity && has_full_page_after_fill
108}
109
110/// An initialized, append-only paged blob.
111pub struct Append;
112
113/// A paged blob whose retained prefix is still being selected.
114pub struct Recovering;
115
116/// Exclusive initialization owner for selecting and durably publishing a retained prefix.
117///
118/// All previous writers and disk-backed readers must close before recovery opens.
119/// Only recovery may shorten the retained logical prefix.
120pub type Recovery<B> = Writer<B, Recovering>;
121
122/// Unique writer to a cache-wrapped [Blob].
123///
124/// Storage-mutating functions consume the writer and return it only on success: an error (or a
125/// dropped future) destroys the handle.
126pub struct Writer<B: Blob, Phase = Append> {
127    /// Distinguishes the recovery owner from the append-only writer.
128    phase: PhantomData<Phase>,
129
130    /// The underlying blob being wrapped.
131    blob: Arc<B>,
132
133    /// The page where the next appended byte will be written to.
134    current_page: u64,
135
136    /// The active checksum of the partial page in the blob, if any.
137    partial_page_state: Option<ActiveChecksum>,
138
139    /// The durable checksum of the page a partial-page flush would rewrite, if any.
140    ///
141    /// Rewrites preserve this slot byte-identically so a torn rewrite can never lose the page's
142    /// last durable contents. It trails [Self::partial_page_state] until a completed sync proves
143    /// the flushed state durable: an unsynced flush ([Self::replay], [Self::start_sync]) must not
144    /// advance it, or a later rewrite would evict the durable checksum and a crash cutting both
145    /// writes could leave no slot covering the synced prefix.
146    durable_page_state: Option<ActiveChecksum>,
147
148    /// Durability state for plain writes, resizes, and range-sync writes.
149    sync_state: SyncState,
150
151    /// Unique id assigned to this blob by the page cache.
152    id: u64,
153
154    /// A reference to the page cache that manages read caching for this blob.
155    cache_ref: CacheRef,
156
157    /// The write buffer containing any logical bytes following the last full page boundary in the
158    /// underlying blob.
159    buffer: Buffer,
160}
161
162impl<B: Blob> Recovery<B> {
163    /// Open `blob` for initialization repair.
164    ///
165    /// `blob` must already hold `original_blob_size` physical bytes. Reads are cached through
166    /// `cache_ref` and appends stage in a write buffer of capacity `capacity`. Trims any invalid
167    /// tail so the blob ends at a checksum-validated page. Earlier pages are not scanned.
168    ///
169    /// Before appending, the tail-page contents must be durable: either pass a blob with no writes
170    /// since it was opened or call [Self::sync]. Until then, recovery may read or truncate the
171    /// blob. The discovered checksum slot seeds durable-slot tracking, so appending over a
172    /// still-volatile tail can overwrite the only durable slot.
173    pub async fn open(
174        blob: B,
175        original_blob_size: u64,
176        capacity: usize,
177        cache_ref: CacheRef,
178    ) -> Result<Self, Error> {
179        let blob = Arc::new(blob);
180        let page_size: u64 = cache_ref.page_size().widen();
181        let (partial_page_state, pages, invalid_data_found) =
182            Writer::<B>::read_last_valid_page(&blob, original_blob_size, page_size).await?;
183        if invalid_data_found {
184            // Invalid data was detected, trim it from the blob.
185            let new_blob_size = pages * (page_size + CHECKSUM_SIZE);
186            warn!(
187                original_blob_size,
188                new_blob_size, "truncating blob to remove invalid data"
189            );
190            blob.resize(new_blob_size).await?;
191            blob.sync().await?;
192        }
193        let capacity = adjusted_capacity(capacity, page_size);
194
195        // A valid tail may still include unsynced writes from the wrapped blob handle.
196        let needs_sync = !invalid_data_found;
197
198        // Retain a partial page in the tip buffer; complete pages remain backed by the blob.
199        let (current_page, partial_page_state, partial_data) = match partial_page_state {
200            Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
201            None => (pages, None, None),
202        };
203
204        let buffer = Buffer::from(
205            current_page * page_size,
206            partial_data.unwrap_or_default().as_ref(),
207            capacity,
208            page_size as usize,
209        );
210
211        Ok(Self {
212            phase: PhantomData,
213            blob,
214            current_page,
215            partial_page_state,
216            durable_page_state: partial_page_state,
217            sync_state: if needs_sync {
218                SyncState::Dirty
219            } else {
220                SyncState::Clean
221            },
222            id: cache_ref.next_id(),
223            cache_ref,
224            buffer,
225        })
226    }
227
228    /// Durably rewrite a committed page to a shorter partial length.
229    async fn sync_shrunk_page(
230        &mut self,
231        page: u64,
232        page_size: u64,
233        new_len: u16,
234        new_crc: u32,
235        old_checksum: &ActiveChecksum,
236    ) -> Result<ActiveChecksum, Error> {
237        // Recovery chooses the valid slot with the larger length. While shrinking, the new
238        // checksum must be made durable without becoming authoritative until the old longer slot
239        // can be disabled. The sequence below therefore lets recovery observe either the old page
240        // or the new shorter page, but not a footer where both slots were damaged by
241        // one torn write.
242        let physical_page_size = page_size
243            .checked_add(CHECKSUM_SIZE)
244            .ok_or(Error::OffsetOverflow)?;
245        let crc_start = page
246            .checked_mul(physical_page_size)
247            .and_then(|start| start.checked_add(page_size))
248            .ok_or(Error::OffsetOverflow)?;
249        let old_slot = old_checksum.slot;
250        let new_slot = old_slot.other();
251
252        // Stage the new slot with a 0 length and the shrunken page CRC. A crash here leaves the
253        // old slot as the only non-zero valid slot.
254        let new_slot_offset = crc_start
255            .checked_add(new_slot.offset() as u64)
256            .ok_or(Error::OffsetOverflow)?;
257        let staged_slot = Checksum::slot_bytes(0, new_crc);
258        self.sync_state
259            .write_at(
260                &self.blob,
261                new_slot_offset,
262                staged_slot.to_vec(),
263                WriteOptions::SYNC | WriteOptions::DONT_CACHE,
264            )
265            .await?;
266
267        // Publish the new shrunken length. If a crash happens before the old slot is invalidated,
268        // both slots may be valid, but recovery still chooses the old longer length.
269        let published_len = Checksum::slot_len_bytes(new_len);
270        self.sync_state
271            .write_at(
272                &self.blob,
273                new_slot_offset,
274                published_len.to_vec(),
275                WriteOptions::SYNC | WriteOptions::DONT_CACHE,
276            )
277            .await?;
278
279        // Clear the old slot entirely. The write stays within the old slot, so it cannot damage
280        // the already-durable shorter checksum, and zeroing the CRC alongside the length keeps a
281        // later torn rewrite of this page from reassembling the retired longer checksum over the
282        // pre-shrink bytes still on the page. Once this lands, the shrunken slot wins.
283        let old_slot_offset = crc_start
284            .checked_add(old_slot.offset() as u64)
285            .ok_or(Error::OffsetOverflow)?;
286        self.sync_state
287            .write_at(
288                &self.blob,
289                old_slot_offset,
290                Checksum::slot_bytes(0, 0).to_vec(),
291                WriteOptions::SYNC | WriteOptions::DONT_CACHE,
292            )
293            .await?;
294
295        Ok(ActiveChecksum::new(new_slot, new_len, new_crc))
296    }
297
298    /// Shrink the retained logical prefix to `target_size`. A page-boundary shrink leaves its
299    /// resize for the caller's sync.
300    async fn shrink(mut self, target_size: u64) -> Result<Self, Error> {
301        let page_size: u64 = self.cache_ref.page_size().widen();
302        let physical_page_size = page_size
303            .checked_add(CHECKSUM_SIZE)
304            .ok_or(Error::OffsetOverflow)?;
305
306        // Partial-page truncation requires a durable source page.
307        self = self.sync().await?;
308
309        // Calculate the physical size needed for the new size.
310        let full_pages = target_size / page_size;
311        let partial_bytes = target_size % page_size;
312        let physical_pages = full_pages
313            .checked_add(u64::from(partial_bytes > 0))
314            .ok_or(Error::OffsetOverflow)?;
315        let new_physical_size = physical_pages
316            .checked_mul(physical_page_size)
317            .ok_or(Error::OffsetOverflow)?;
318        let tail_offset = full_pages
319            .checked_mul(page_size)
320            .ok_or(Error::OffsetOverflow)?;
321        let current_physical_size = if self.partial_page_state.is_some() {
322            self.current_page
323                .checked_add(1)
324                .and_then(|pages| pages.checked_mul(physical_page_size))
325                .ok_or(Error::OffsetOverflow)?
326        } else {
327            self.current_page
328                .checked_mul(physical_page_size)
329                .ok_or(Error::OffsetOverflow)?
330        };
331
332        // A logical shrink can leave the physical page count unchanged. Only real physical
333        // resizes need to create a pending sync.
334        if new_physical_size != current_physical_size {
335            self.sync_state
336                .resize(&self.blob, new_physical_size)
337                .await?;
338        }
339        if partial_bytes > 0 {
340            self.shrink_to_partial(full_pages, partial_bytes, page_size, tail_offset)
341                .await?;
342            return Ok(self);
343        }
344
345        // Shrink the blob to a page boundary, which requires no CRC-slot rewrite.
346        self.partial_page_state = None;
347        self.durable_page_state = None;
348        self.current_page = full_pages;
349        self.buffer.replace(tail_offset, &[]);
350
351        Ok(self)
352    }
353
354    /// Perform a shrink to a partial page tip and make the shorter CRC slot authoritative.
355    async fn shrink_to_partial(
356        &mut self,
357        full_pages: u64,
358        partial_bytes: u64,
359        page_size: u64,
360        tail_offset: u64,
361    ) -> Result<(), Error> {
362        // The truncated page becomes the blob's tip. Its data is validated before the shorter
363        // checksum record is published.
364        self.current_page = full_pages;
365
366        // The retained prefix becomes the authoritative tip buffer, so this
367        // page need not remain in the OS page cache.
368        let (page_data, old_checksum) = super::get_page_with_checksum_from_blob(
369            &self.blob,
370            full_pages,
371            page_size,
372            ReadOptions::DONT_CACHE,
373        )
374        .await?;
375
376        // Ensure the validated data covers what we need.
377        if (page_data.len() as u64) < partial_bytes {
378            return Err(Error::InvalidChecksum);
379        }
380
381        let new_data = &page_data.as_ref()[..partial_bytes as usize];
382        self.buffer.replace(tail_offset, new_data);
383
384        let final_record = self
385            .sync_shrunk_page(
386                full_pages,
387                page_size,
388                partial_bytes as u16,
389                Crc32::checksum(new_data),
390                &old_checksum,
391            )
392            .await?;
393
394        // Both active and durable state now identify the published shorter record.
395        self.partial_page_state = Some(final_record);
396        self.durable_page_state = Some(final_record);
397
398        Ok(())
399    }
400
401    /// Durably retain at most `size` logical bytes.
402    ///
403    /// A size above the current length leaves the length unchanged. Pending writes are synchronized
404    /// even when the length does not change.
405    ///
406    /// Outstanding replays that cover the shortened or rewritten suffix are invalidated and must
407    /// not be consumed afterward.
408    pub async fn truncate(mut self, size: u64) -> Result<Self, Error> {
409        if size < self.size() {
410            self = self.shrink(size).await?;
411        }
412        self.sync().await
413    }
414}
415
416impl<B: Blob> From<Recovery<B>> for Writer<B> {
417    /// Convert recovery into a live writer without flushing or syncing.
418    ///
419    /// Truncation is already durable. Bytes appended since the last completed sync remain unsynced.
420    fn from(recovery: Recovery<B>) -> Self {
421        Self {
422            phase: PhantomData,
423            blob: recovery.blob,
424            current_page: recovery.current_page,
425            partial_page_state: recovery.partial_page_state,
426            durable_page_state: recovery.durable_page_state,
427            sync_state: recovery.sync_state,
428            id: recovery.id,
429            cache_ref: recovery.cache_ref,
430            buffer: recovery.buffer,
431        }
432    }
433}
434
435impl<B: Blob> Writer<B> {
436    /// Open a paged blob, repairing incomplete tail pages before publishing its writer.
437    /// The existing tail must be durable, and no previous writer or disk-backed reader may
438    /// access this storage incarnation during initialization.
439    pub async fn new(
440        blob: B,
441        size: u64,
442        capacity: usize,
443        cache_ref: CacheRef,
444    ) -> Result<Self, Error> {
445        Ok(Recovery::open(blob, size, capacity, cache_ref)
446            .await?
447            .into())
448    }
449
450    /// Scans backwards from the end of the blob, stopping when it finds a valid page.
451    ///
452    /// # Returns
453    ///
454    /// A tuple of `(partial_page, page_count, invalid_data_found)`:
455    ///
456    /// - `partial_page`: If the last valid page is partial (contains fewer than `page_size` logical
457    ///   bytes), returns `Some((data, checksum))` containing the logical data and its active
458    ///   checksum.
459    ///   Returns `None` if the last valid page is full or if no valid pages exist.
460    ///
461    /// - `page_count`: The number of pages in the blob up to and including the last valid page
462    ///   found (whether or not it's partial). Note that it's possible earlier pages may be invalid
463    ///   since this function stops scanning when it finds one valid page.
464    ///
465    /// - `invalid_data_found`: `true` if there are any bytes in the blob that follow the last valid
466    ///   page. Typically the blob should be resized to eliminate them since their integrity cannot
467    ///   be guaranteed.
468    async fn read_last_valid_page(
469        blob: &B,
470        blob_size: u64,
471        page_size: u64,
472    ) -> Result<(Option<(IoBuf, ActiveChecksum)>, u64, bool), Error> {
473        let physical_page_size = page_size + CHECKSUM_SIZE;
474        let partial_bytes = blob_size % physical_page_size;
475        let mut last_page_end = blob_size - partial_bytes;
476
477        // If the last physical page in the blob is truncated, it can't have a valid CRC record and
478        // must be invalid.
479        let mut invalid_data_found = partial_bytes != 0;
480
481        while last_page_end != 0 {
482            // Read the last page and parse its CRC record.
483            // A valid full page remains disk-authoritative and may be read again after startup.
484            let page_start = last_page_end - physical_page_size;
485            let buf = blob
486                .read_at(
487                    page_start,
488                    physical_page_size as usize,
489                    ReadOptions::default(),
490                )
491                .await?
492                .coalesce()
493                .freeze();
494
495            match Checksum::validate_page(buf.as_ref()) {
496                Some(checksum) => {
497                    // Found a valid page.
498                    let len = checksum.len as u64;
499                    if len != page_size {
500                        // The page is partial (logical data doesn't fill the page).
501                        let logical_bytes = buf.slice(..len as usize);
502                        return Ok((
503                            Some((logical_bytes, checksum)),
504                            last_page_end / physical_page_size,
505                            invalid_data_found,
506                        ));
507                    }
508                    // The page is full.
509                    return Ok((None, last_page_end / physical_page_size, invalid_data_found));
510                }
511                None => {
512                    // The page is invalid.
513                    last_page_end = page_start;
514                    invalid_data_found = true;
515                }
516            }
517        }
518
519        // No valid page exists in the blob.
520        Ok((None, 0, invalid_data_found))
521    }
522
523    /// Capture an immutable [`Sealed`] view of the logical bytes.
524    ///
525    /// Buffered full pages are written to the blob, and the partial page is copied into the view.
526    /// Neither is made durable. Call [`Self::sync`] if the returned handle's bytes must survive a
527    /// crash.
528    ///
529    /// Later appends preserve this view, including its frozen partial page.
530    pub async fn snapshot(mut self) -> Result<(Self, Sealed<B>), Error> {
531        // Write complete pages before capturing the view, which owns a copy of the partial tail.
532        self.flush_internal(false, false).await?;
533        let sealed = self.sealed_handle(self.id);
534        Ok((self, sealed))
535    }
536
537    /// Capture an immutable [`Sealed`] view of the logical bytes without writing to the blob.
538    ///
539    /// This is [`Self::snapshot`] without its flush. It returns the same view when every full
540    /// page is already in the blob, and `None` while the write buffer holds a full page that has
541    /// not been written.
542    pub fn try_snapshot(&self) -> Option<Sealed<B>> {
543        if self.has_flush_work(false) {
544            return None;
545        }
546        Some(self.sealed_handle(self.id))
547    }
548
549    /// Read and validate one page, returning its logical bytes and the range they cover.
550    async fn read_page(
551        blob: &B,
552        page: u64,
553        page_size: u64,
554        read_options: ReadOptions,
555    ) -> Result<(IoBuf, u64, u64), Error> {
556        let (logical, _) =
557            super::get_page_with_checksum_from_blob(blob, page, page_size, read_options).await?;
558        let start = page.checked_mul(page_size).ok_or(Error::OffsetOverflow)?;
559        let len: u64 = Widen::widen(logical.len());
560        let end = start.checked_add(len).ok_or(Error::OffsetOverflow)?;
561        Ok((logical, start, end))
562    }
563
564    /// Read a logical range directly from a raw paged blob, validating every page it spans.
565    ///
566    /// Returns [Error::BlobInsufficientLength] when valid page contents do not cover the whole
567    /// range, and [Error::OffsetOverflow] when its end or page offsets overflow.
568    pub async fn read_range(
569        blob: &B,
570        logical_page_size: NonZeroU16,
571        offset: u64,
572        len: usize,
573        read_options: ReadOptions,
574    ) -> Result<IoBufs, Error> {
575        // Resolve the requested range before allocating or reading any pages.
576        let len_u64: u64 = Widen::widen(len);
577        let end = offset.checked_add(len_u64).ok_or(Error::OffsetOverflow)?;
578        if len == 0 {
579            return Ok(IoBufs::default());
580        }
581
582        // Identify the inclusive logical page range spanning the requested bytes.
583        let logical_page_size: u64 = logical_page_size.widen();
584        let first_page = offset / logical_page_size;
585        let last_page = (end - 1) / logical_page_size;
586        let mut out = IoBufs::default();
587
588        // Validate every spanning page and append only its intersection with the requested range.
589        for page in first_page..=last_page {
590            let (logical, page_start, page_end) =
591                Self::read_page(blob, page, logical_page_size, read_options).await?;
592            let overlap_start = offset.max(page_start);
593            let overlap_end = end.min(page_end);
594            if overlap_start >= overlap_end {
595                return Err(Error::BlobInsufficientLength);
596            }
597            let start = (overlap_start - page_start) as usize;
598            let end = (overlap_end - page_start) as usize;
599            out.append(logical.slice(start..end));
600        }
601
602        // A short terminal page can leave the requested range only partially covered.
603        if out.len() != len {
604            return Err(Error::BlobInsufficientLength);
605        }
606        Ok(out)
607    }
608
609    /// Read the terminal logical range of a raw paged blob and return its logical size.
610    ///
611    /// The blob must contain only complete physical pages. The last page is validated first to
612    /// determine the logical end. If `len` crosses a page boundary, preceding pages are validated
613    /// with [Self::read_range].
614    pub async fn read_tail(
615        blob: &B,
616        physical_size: u64,
617        logical_page_size: NonZeroU16,
618        len: usize,
619        read_options: ReadOptions,
620    ) -> Result<(u64, IoBufs), Error> {
621        if physical_size == 0 {
622            return if len == 0 {
623                Ok((0, IoBufs::default()))
624            } else {
625                Err(Error::BlobInsufficientLength)
626            };
627        }
628
629        // A trailing partial page means the caller did not size the blob to complete physical
630        // pages, so there is no trusted terminal page to read the logical end from.
631        let page_size: u64 = logical_page_size.widen();
632        let physical_page_size = page_size
633            .checked_add(CHECKSUM_SIZE)
634            .ok_or(Error::OffsetOverflow)?;
635        if !physical_size.is_multiple_of(physical_page_size) {
636            return Err(Error::BlobInsufficientLength);
637        }
638
639        // The checksum length on the terminal page determines the blob's logical end.
640        let page = physical_size / physical_page_size - 1;
641        let (tail, page_start, logical_size) =
642            Self::read_page(blob, page, page_size, read_options).await?;
643        let len_u64: u64 = Widen::widen(len);
644        let offset = logical_size
645            .checked_sub(len_u64)
646            .ok_or(Error::BlobInsufficientLength)?;
647
648        // Read only the portion preceding the terminal page, then append its already-validated
649        // suffix. This keeps a terminal item wholly within the last page to one blob read.
650        let mut out = if offset < page_start {
651            let prefix_len =
652                usize::try_from(page_start - offset).map_err(|_| Error::OffsetOverflow)?;
653            Self::read_range(blob, logical_page_size, offset, prefix_len, read_options).await?
654        } else {
655            IoBufs::default()
656        };
657        let tail_start = usize::try_from(offset.max(page_start) - page_start)
658            .map_err(|_| Error::OffsetOverflow)?;
659        out.append(tail.slice(tail_start..));
660        assert_eq!(out.len(), len);
661        Ok((logical_size, out))
662    }
663
664    /// Construct an immutable read handle for the current blob state.
665    fn sealed_handle(&self, id: u64) -> Sealed<B> {
666        let page_size: u64 = self.cache_ref.page_size().widen();
667        let full_pages = self.current_page;
668        assert_eq!(
669            full_pages.checked_mul(page_size),
670            Some(self.buffer.offset),
671            "flushed page count is inconsistent with the buffer offset"
672        );
673
674        // Readers own their partial page so appends can reuse the writer's unique mutable tail.
675        let partial_page = if self.buffer.is_empty() {
676            None
677        } else {
678            Some(IoBuf::copy_from_slice(self.buffer.partial()))
679        };
680        Sealed::new(
681            self.blob.clone(),
682            self.buffer.size(),
683            partial_page,
684            self.cache_ref.clone(),
685            id,
686        )
687    }
688
689    /// Consume the write handle, flushing buffered bytes and beginning a sync of the blob.
690    ///
691    /// Returns an immutable [`Sealed`] read handle plus a completion handle for the started sync.
692    /// Reads through the [`Sealed`] handle observe flushed bytes immediately.
693    /// Durability isn't guaranteed until the sync handle completes.
694    pub async fn seal(mut self) -> Result<(Sealed<B>, Handle<()>), Error> {
695        // Complete earlier syncs before flushing the final bytes under this writer's ownership.
696        self.sync_state.wait_for_pending().await?;
697        self.flush_internal(true, false).await?;
698
699        // Transfer reads to an immutable view while the runtime completes its durability barrier.
700        let handle = self.sync_state.start_sync(&self.blob).await;
701        Ok((self.sealed_handle(self.id), handle))
702    }
703}
704
705impl<B: Blob, Phase> Writer<B, Phase> {
706    /// Append all bytes in `buf` to the tip of the blob, returning the logical offset at which
707    /// the first byte was written.
708    pub async fn append(mut self, buf: &[u8]) -> Result<(Self, u64), Error> {
709        let page_size: usize = self.cache_ref.page_size().widen();
710
711        // Bypass the write buffer and write whole pages directly when `buf` is large.
712        if too_big_for_buffer(
713            self.buffer.len(),
714            self.buffer.capacity,
715            buf.len(),
716            page_size,
717        ) {
718            return self.append_owned(IoBuf::copy_from_slice(buf)).await;
719        }
720
721        let offset = self.buffer.size();
722        if self.buffer.append(buf) {
723            self.flush_internal(false, false).await?;
724        }
725        Ok((self, offset))
726    }
727
728    /// Append `buf` to the write buffer if it fits.
729    ///
730    /// Returns its logical offset on success. Returns `None` without changing the writer when
731    /// there is insufficient buffer space. Performs no I/O and does not make the append durable.
732    /// Callers can fall back to [`Self::append_owned`] when it does not fit.
733    pub fn try_append(&mut self, buf: &[u8]) -> Option<u64> {
734        self.buffer.try_append(buf)
735    }
736
737    /// Encode a value directly into the write buffer if it fits.
738    ///
739    /// Returns its logical offset on success. Returns `None` without encoding or changing the
740    /// writer when there is insufficient buffer space. Performs no I/O and does not make the
741    /// append durable. Callers can fall back to [`Self::append_owned`] when it does not fit.
742    ///
743    /// # Panics
744    ///
745    /// Panics if the encoder writes a different number of bytes than [`EncodeSize::encode_size`].
746    pub fn try_append_value<T: EncodeSize + Write>(&mut self, value: &T) -> Option<u64> {
747        self.buffer.try_append_value(value)
748    }
749
750    /// Append owned bytes to the tip of the blob.
751    ///
752    /// Large appends fill the current tip to a page boundary, write complete pages directly to the
753    /// blob, and leave only a sub-page suffix in the write buffer. This avoids copying full-page
754    /// payloads while preserving the invariant that the buffer starts at `current_page`.
755    pub async fn append_owned(mut self, buf: IoBuf) -> Result<(Self, u64), Error> {
756        let page_size: usize = self.cache_ref.page_size().widen();
757        let offset = self.buffer.size();
758
759        // Buffer the append unless `buf` is too big for the buffer.
760        if !too_big_for_buffer(
761            self.buffer.len(),
762            self.buffer.capacity,
763            buf.len(),
764            page_size,
765        ) {
766            if self.buffer.append(buf.as_ref()) {
767                self.flush_internal(false, false).await?;
768            }
769            return Ok((self, offset));
770        }
771
772        // Bytes needed to fill current page to a page boundary (0 if already aligned).
773        let fill = self.buffer.len().next_multiple_of(page_size) - self.buffer.len();
774
775        // Top up the tip to a page boundary so its contents flush as full pages, leaving any
776        // partial-page CRC handling to the regular flush path.
777        if fill > 0 {
778            self.buffer.append(&buf.as_ref()[..fill]);
779        }
780        let boundary = self.buffer.size();
781        if !self.buffer.is_empty() {
782            self.flush_internal(false, false).await?;
783            assert!(
784                self.buffer.size() == boundary && self.buffer.is_empty(),
785                "flush left unexpected buffered bytes before a direct-path append"
786            );
787        }
788
789        // Prepare physical pages for the whole pages remaining in `buf` without copying logical
790        // payload bytes.
791        let bulk_len = (buf.len() - fill) / page_size * page_size;
792        let bulk = buf.slice(fill..fill + bulk_len);
793        let mut physical_pages = IoBufs::default();
794        self.append_full_pages(std::iter::once(&bulk), None, &mut physical_pages);
795
796        assert!(
797            self.partial_page_state.is_none(),
798            "an empty tip implies no partial page state"
799        );
800
801        // Direct blob writes must not overtake an earlier started sync barrier.
802        self.sync_state.wait_for_pending().await?;
803
804        // Cache the pages before `replace` publishes the new size, so reads of the bulk range are
805        // served from the cache while the blob write is still in flight. Insert in
806        // write-buffer-sized chunks. The capacity is a whole number of pages (see
807        // [adjusted_capacity]), so each chunk is page-aligned.
808        let chunk_len = self.buffer.capacity;
809        let mut cache_offset = boundary;
810        for chunk in bulk.as_ref().chunks(chunk_len) {
811            let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
812            assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
813            cache_offset += chunk.len() as u64;
814        }
815
816        // Update state before writing. Copy the suffix into at most one page so the tip never
817        // retains the input's larger backing.
818        self.current_page += (bulk_len / page_size) as u64;
819        self.buffer
820            .replace(boundary + bulk_len as u64, &buf.as_ref()[fill + bulk_len..]);
821
822        // Make sure the buffer offset and underlying blob agree on the state of the tip.
823        let page_size: u64 = self.cache_ref.page_size().widen();
824        assert_eq!(self.current_page * page_size, self.buffer.offset);
825
826        let physical_page_size = page_size + CHECKSUM_SIZE;
827        let write_at_offset = boundary / page_size * physical_page_size;
828        self.sync_state
829            .write_at(
830                &self.blob,
831                write_at_offset,
832                physical_pages,
833                WriteOptions::DONT_CACHE,
834            )
835            .await?;
836
837        Ok((self, offset))
838    }
839
840    /// Whether a partial page of `partial_len` bytes needs a write when `full_pages` full
841    /// pages precede it in the same flush and `flushed` is the last flushed partial state: an
842    /// empty tip never writes, a moved tip always writes, and an unmoved tip writes only when
843    /// its length changed.
844    fn partial_page_dirty(
845        full_pages: usize,
846        partial_len: usize,
847        flushed: Option<&ActiveChecksum>,
848    ) -> bool {
849        partial_len != 0
850            && (full_pages > 0 || flushed.is_none_or(|state| state.len as usize != partial_len))
851    }
852
853    /// Whether a flush would emit any physical page write.
854    fn has_flush_work(&self, write_partial_page: bool) -> bool {
855        let page_size: usize = self.cache_ref.page_size().widen();
856        let full_pages = self.buffer.len() / page_size;
857        if full_pages > 0 {
858            return true;
859        }
860        write_partial_page
861            && Self::partial_page_dirty(
862                full_pages,
863                self.buffer.len() % page_size,
864                self.partial_page_state.as_ref(),
865            )
866    }
867
868    /// Flush all full pages from the buffer to disk, resetting the buffer to contain only the bytes
869    /// in any final partial page.
870    ///
871    /// If `write_partial_page` is true, the partial page will be written to the blob as well along
872    /// with a CRC record.
873    ///
874    /// A flush emits one write covering whole physical pages. A previously written partial page
875    /// is rewritten in full, preserving its durable bytes and protected checksum slot.
876    ///
877    /// If `sync` is true, the emitted write is made durable immediately. When an earlier mutation
878    /// is pending, the write is followed by a blob sync instead of relying on per-write durability.
879    ///
880    /// Returns `true` if the flush made its writes durable, so no additional sync is needed.
881    async fn flush_internal(
882        &mut self,
883        write_partial_page: bool,
884        sync: bool,
885    ) -> Result<bool, Error> {
886        // If there's nothing to write, return early without observing any pending barrier (an
887        // empty start_sync must remain a cheap re-observation of the in-flight sync).
888        if !self.has_flush_work(write_partial_page) {
889            return Ok(false);
890        }
891
892        // A flush mutates the blob, so first resolve any outstanding start_sync barrier. Once
893        // no unsynced mutation remains, the last flushed partial state is durable and becomes
894        // the checksum the rewrite below must preserve.
895        self.sync_state.wait_for_pending().await?;
896        if self.sync_state.is_clean() {
897            self.durable_page_state = self.partial_page_state;
898        }
899
900        // Capture the full pages' original range before draining advances the tip. Their backing
901        // moves into the flush, and the partial page is detached from any larger allocation.
902        let page_size: usize = self.cache_ref.page_size().widen();
903        let pages_to_cache = self.buffer.len() / page_size;
904        let cache_offset = self.buffer.offset;
905        let full_pages = self.buffer.drain_full_pages();
906        let new_offset = self.buffer.offset;
907
908        // Rewrites preserve the durable checksum, not merely the last flushed one: an unsynced
909        // flush may have rewritten the partial page without making its checksum durable.
910        let (physical_pages, partial_page_state) = self.to_physical_pages(
911            &full_pages,
912            self.buffer.partial(),
913            write_partial_page,
914            self.partial_page_state.as_ref(),
915            self.durable_page_state.as_ref(),
916        );
917        assert!(
918            !physical_pages.is_empty(),
919            "flush work predicate must match physical page construction"
920        );
921
922        // Cache full pages before publishing the new blob state so reads don't observe stale
923        // persisted bytes during the handoff from tip to cache.
924        if pages_to_cache > 0 {
925            let cached = self.cache_ref.cache_pages(
926                self.id,
927                full_pages
928                    .iter()
929                    .flat_map(|chunk| chunk.as_ref().chunks_exact(page_size)),
930                cache_offset,
931            );
932            assert_eq!(cached, pages_to_cache * page_size);
933        }
934
935        let physical_page_size = page_size + CHECKSUM_SIZE as usize;
936        let write_at_offset = self.current_page * physical_page_size as u64;
937
938        // Update state before writing. No blob mutation follows a failed write: the failing
939        // method drops the writer.
940        self.current_page += pages_to_cache as u64;
941        self.partial_page_state = partial_page_state;
942
943        // A new tip page has no durable contents to preserve yet.
944        if pages_to_cache > 0 {
945            self.durable_page_state = None;
946        }
947
948        // Make sure the buffer offset and underlying blob agree on the state of the tip.
949        let page_size: u64 = self.cache_ref.page_size().widen();
950        assert_eq!(self.current_page * page_size, new_offset);
951
952        // Rewriting a physical page resubmits its durable bytes and protected checksum
953        // unchanged, so a torn write leaves the durable state recoverable.
954        if sync {
955            // Publish the flushed checksum as durable only after the write's barrier completes.
956            self.sync_state
957                .write_at(
958                    &self.blob,
959                    write_at_offset,
960                    physical_pages,
961                    WriteOptions::SYNC | WriteOptions::DONT_CACHE,
962                )
963                .await?;
964            self.durable_page_state = partial_page_state;
965        } else {
966            // An unsynced write retains the checksum protecting the last durable prefix.
967            self.sync_state
968                .write_at(
969                    &self.blob,
970                    write_at_offset,
971                    physical_pages,
972                    WriteOptions::DONT_CACHE,
973                )
974                .await?;
975        }
976        Ok(sync)
977    }
978
979    /// Returns the size of the blob.
980    pub const fn size(&self) -> u64 {
981        self.buffer.size()
982    }
983
984    /// Whether [Self::sync] would write buffered bytes, sync the blob, or observe a started sync.
985    pub fn needs_sync(&self) -> bool {
986        !self.sync_state.is_clean() || self.has_flush_work(true)
987    }
988
989    /// Returns a borrowed view over this blob.
990    const fn view(&self) -> View<'_, B> {
991        View {
992            blob: &self.blob,
993            cache_ref: &self.cache_ref,
994            id: self.id,
995            size: self.buffer.size(),
996            tail_offset: self.buffer.offset,
997            tail: Tail::Buffered(&self.buffer),
998        }
999    }
1000
1001    /// Read into `buf` if it can be done synchronously without I/O. Returns `true` only if all
1002    /// `buf.len()` bytes were satisfied from the page cache and/or the in-memory tail. When `false`
1003    /// is returned, the contents of `buf` are unspecified.
1004    pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
1005        self.view().try_read_sync_into(buf, offset)
1006    }
1007
1008    /// Read exactly `len` immutable bytes starting at `offset`.
1009    pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
1010        self.view().read_at(offset, len).await
1011    }
1012
1013    /// Reads up to `len` bytes starting at `offset`, but only as many as are available.
1014    ///
1015    /// Returns the buffer (truncated to actual bytes read) and the number of bytes read. Returns
1016    /// an error if no bytes are available at the given offset.
1017    pub async fn read_up_to(
1018        &self,
1019        offset: u64,
1020        len: usize,
1021        bufs: impl Into<IoBufMut> + Send,
1022    ) -> Result<(IoBufMut, usize), Error> {
1023        self.view().read_up_to(offset, len, bufs).await
1024    }
1025
1026    /// Read multiple fixed-size items at sorted byte offsets into a contiguous caller buffer.
1027    ///
1028    /// `buf` must be exactly `offsets.len() * item_size` bytes. All offsets must be sorted,
1029    /// non-overlapping, and within bounds.
1030    ///
1031    /// Returns the number of items fully served without a blob read (from the in-memory tail and the
1032    /// page cache). The remaining items required at least one blob read.
1033    pub async fn read_many_into(
1034        &self,
1035        buf: &mut [u8],
1036        offsets: &[u64],
1037        item_size: NonZeroUsize,
1038    ) -> Result<usize, Error> {
1039        self.view().read_many_into(buf, offsets, item_size).await
1040    }
1041
1042    /// Like [`Self::read_many_into`], but each `(offset, len)` range has its own length. `buf`
1043    /// holds the ranges back to back.
1044    ///
1045    /// Returns how many ranges were served entirely from the in-memory tail and page cache before
1046    /// any blob read. Empty ranges count as served.
1047    ///
1048    /// # Panics
1049    ///
1050    /// Panics if a range starts before the previous range ends, or if `buf` is not the ranges'
1051    /// total length.
1052    pub async fn read_ranges_into(
1053        &self,
1054        buf: &mut [u8],
1055        ranges: &[(u64, usize)],
1056    ) -> Result<usize, Error> {
1057        self.view().read_ranges_into(buf, ranges).await
1058    }
1059
1060    /// Like [`Self::read_many_into`], but synchronous and cache-only. Returns the indices of
1061    /// items that require a blob read. Their slots in `buf` hold unspecified bytes.
1062    pub fn try_read_many_sync_into(
1063        &self,
1064        buf: &mut [u8],
1065        offsets: &[u64],
1066        item_size: NonZeroUsize,
1067    ) -> Vec<usize> {
1068        self.view().try_read_many_sync_into(buf, offsets, item_size)
1069    }
1070
1071    /// Like [`Self::try_read_many_sync_into`], but for variable-length `(offset, len)` ranges:
1072    /// `buf` holds one slot per range, back to back.
1073    pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
1074        self.view().try_read_ranges_sync_into(buf, ranges)
1075    }
1076
1077    /// Reads bytes starting at `offset` into `buf`.
1078    pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
1079        self.view().read_into(buf, offset).await
1080    }
1081
1082    /// Prepare physical-page writes from buffered logical bytes.
1083    ///
1084    /// Each physical page contains one logical page plus CRC record. If the last page is not yet
1085    /// full, it will be included only if `include_partial_page` is true.
1086    ///
1087    /// # Arguments
1088    ///
1089    /// * `full_pages` - Page-aligned chunks containing complete logical pages
1090    /// * `partial_page` - The trailing bytes that do not fill a page
1091    /// * `include_partial_page` - Whether to include a partial page if one exists
1092    /// * `flushed` - The active checksum of the last flushed partial page, if any. Used only to
1093    ///   detect a partial page with nothing new to write.
1094    /// * `durable` - The durable checksum of the page being rewritten, if any. When present, the
1095    ///   first page's CRC record preserves it in its original slot and places the new checksum
1096    ///   in the other slot.
1097    ///
1098    /// Returns the physical pages to write and, for any included partial page, its new active
1099    /// checksum.
1100    fn to_physical_pages(
1101        &self,
1102        full_pages: &IoBufs,
1103        partial_page: &[u8],
1104        include_partial_page: bool,
1105        flushed: Option<&ActiveChecksum>,
1106        durable: Option<&ActiveChecksum>,
1107    ) -> (IoBufs, Option<ActiveChecksum>) {
1108        let page_size: usize = self.cache_ref.page_size().widen();
1109        let physical_page_size = page_size + CHECKSUM_SIZE as usize;
1110        let pages_to_write = full_pages.len() / page_size;
1111        let mut write_buffer = IoBufs::default();
1112
1113        if pages_to_write > 0 {
1114            self.append_full_pages(full_pages.iter(), durable, &mut write_buffer);
1115        }
1116
1117        if !include_partial_page {
1118            return (write_buffer, None);
1119        }
1120
1121        if !Self::partial_page_dirty(pages_to_write, partial_page.len(), flushed) {
1122            return (write_buffer, None);
1123        }
1124        let partial_len = partial_page.len();
1125        let crc = Crc32::checksum(partial_page);
1126
1127        // For partial pages: if this is the first page and there's a durable CRC, preserve it.
1128        // Otherwise just use the new CRC in slot 0.
1129        let durable = if pages_to_write == 0 { durable } else { None };
1130        let (crc_record, active_checksum) =
1131            Self::build_crc_record(partial_len as u16, crc, durable);
1132
1133        // A persisted partial page still occupies one full physical page:
1134        // [partial logical bytes, zero padding, crc record].
1135        let mut padded = IoBufMut::with_capacity(physical_page_size);
1136        padded.put_slice(partial_page);
1137        let zero_count = page_size - partial_len;
1138        if zero_count > 0 {
1139            padded.put_bytes(0, zero_count);
1140        }
1141        padded.put_slice(&crc_record.to_bytes());
1142        write_buffer.append(padded.freeze());
1143
1144        (write_buffer, Some(active_checksum))
1145    }
1146
1147    /// Append page-aligned chunks in on-disk format, with one payload slice and CRC per page.
1148    /// The first page preserves `old_checksum` in its original slot when present.
1149    fn append_full_pages<'a>(
1150        &self,
1151        data: impl Iterator<Item = &'a IoBuf> + Clone,
1152        old_checksum: Option<&ActiveChecksum>,
1153        write_buffer: &mut IoBufs,
1154    ) {
1155        let page_size: usize = self.cache_ref.page_size().widen();
1156        let pages = data
1157            .clone()
1158            .map(|chunk| {
1159                assert!(chunk.len().is_multiple_of(page_size));
1160                chunk.len() / page_size
1161            })
1162            .sum::<usize>();
1163        assert!(pages > 0);
1164        let page_size_u16 =
1165            u16::try_from(page_size).expect("page size must fit in u16 for CRC record");
1166
1167        // Store the batch's CRC records in one allocation, in the same order as the payload pages.
1168        let mut crcs = IoBufMut::with_capacity(CHECKSUM_SIZE as usize * pages);
1169        for (page, logical_page) in data
1170            .clone()
1171            .flat_map(|chunk| chunk.as_ref().chunks_exact(page_size))
1172            .enumerate()
1173        {
1174            let crc = Crc32::checksum(logical_page);
1175            let old_checksum = if page == 0 { old_checksum } else { None };
1176            let (crc_record, _) = Self::build_crc_record(page_size_u16, crc, old_checksum);
1177            crcs.put_slice(&crc_record.to_bytes());
1178        }
1179        let crc_blob = crcs.freeze();
1180
1181        // Interleave payload pages and CRC records in physical write order. Slicing retains the
1182        // original payload chunks and the shared CRC allocation without copying their bytes.
1183        let mut crc_start = 0;
1184        for chunk in data {
1185            for start in (0..chunk.len()).step_by(page_size) {
1186                write_buffer.append(chunk.slice(start..start + page_size));
1187                write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
1188                crc_start += CHECKSUM_SIZE as usize;
1189            }
1190        }
1191    }
1192
1193    /// Build a CRC record and identify its active checksum. An old checksum remains in its original
1194    /// slot while the new checksum is placed in the other slot.
1195    const fn build_crc_record(
1196        new_len: u16,
1197        new_crc: u32,
1198        old_checksum: Option<&ActiveChecksum>,
1199    ) -> (Checksum, ActiveChecksum) {
1200        let Some(old_checksum) = old_checksum else {
1201            return (
1202                Checksum::new(new_len, new_crc),
1203                ActiveChecksum::new(Slot::First, new_len, new_crc),
1204            );
1205        };
1206
1207        let new_slot = old_checksum.slot.other();
1208        let record = match old_checksum.slot {
1209            Slot::First => Checksum {
1210                len1: old_checksum.len,
1211                crc1: old_checksum.crc,
1212                len2: new_len,
1213                crc2: new_crc,
1214            },
1215            Slot::Second => Checksum {
1216                len1: new_len,
1217                crc1: new_crc,
1218                len2: old_checksum.len,
1219                crc2: old_checksum.crc,
1220            },
1221        };
1222        (record, ActiveChecksum::new(new_slot, new_len, new_crc))
1223    }
1224
1225    /// Flushes any buffered data, then returns the writer with a [Replay] for the underlying blob.
1226    ///
1227    /// The returned replay validates that every stored page holds a full page and retains a
1228    /// frozen copy of the current partial page. CRCs are not included in the output. Every
1229    /// underlying blob read performed by the returned replay uses `read_options`, including
1230    /// refills after seeking.
1231    ///
1232    /// Pages are read in batches as for [`Sealed::replay`], which describes what `buffer_size`
1233    /// bounds.
1234    ///
1235    /// This does not establish durability. Use [`sync`](Self::sync) if the replayed bytes must
1236    /// survive a crash.
1237    pub async fn replay(
1238        mut self,
1239        buffer_size: NonZeroUsize,
1240        read_options: ReadOptions,
1241    ) -> Result<(Self, Replay<B>), Error> {
1242        // Flush the live tip so every full page is on the blob, then freeze the partial tail so
1243        // later rewrites cannot shorten the replay's view.
1244        self.flush_internal(true, false).await?;
1245        let page_size = self.cache_ref.page_size();
1246        let logical_size = self.size();
1247        let physical_page_size = u64::from(page_size.get()) + CHECKSUM_SIZE;
1248        let pages = logical_size.div_ceil(u64::from(page_size.get()));
1249        let physical_size = pages
1250            .checked_mul(physical_page_size)
1251            .ok_or(Error::OffsetOverflow)?;
1252        let buffer_pages = (buffer_size.get() / physical_page_size as usize).max(1);
1253        let partial_page = (logical_size > self.buffer.offset)
1254            .then(|| IoBuf::copy_from_slice(self.buffer.partial()));
1255        let replay = Replay::new(PageReader::new(
1256            self.blob.clone(),
1257            physical_size,
1258            logical_size,
1259            partial_page,
1260            buffer_pages,
1261            page_size,
1262            read_options,
1263        ));
1264        Ok((self, replay))
1265    }
1266
1267    /// Flushes buffered data and makes all pending mutations durable.
1268    ///
1269    /// A newly flushed write can carry [`WriteOptions::SYNC`] when no earlier mutation is pending.
1270    /// Otherwise, [`Blob::sync`] provides the barrier for all pending mutations.
1271    pub async fn sync(mut self) -> Result<Self, Error> {
1272        // Flush any buffered data, including any partial page. A flush that writes to the blob
1273        // makes that write durable itself and returns true.
1274        if self.flush_internal(true, true).await? {
1275            return Ok(self);
1276        }
1277
1278        // The flush had nothing to write. Sync only if a durability barrier is still pending.
1279        // Everything flushed is durable once it completes.
1280        self.sync_state.sync(&self.blob).await?;
1281        self.durable_page_state = self.partial_page_state;
1282        Ok(self)
1283    }
1284
1285    /// Flushes buffered data and begins making all pending mutations durable, returning a
1286    /// completion handle.
1287    ///
1288    /// Awaiting the returned [`Handle`] waits for the same durability guarantee as [`Self::sync`]
1289    /// for the state flushed by this call. Later calls to [`Self::sync`] and writer methods that
1290    /// mutate the blob first wait for any outstanding start_sync handles. A failure of the started
1291    /// sync is reported by the handle and by the next such call. Flush errors are returned
1292    /// directly.
1293    pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
1294        // Finish writes before handing their durability barrier to the runtime.
1295        self.flush_internal(true, false).await?;
1296        let handle = self.sync_state.start_sync(&self.blob).await;
1297        Ok((self, handle))
1298    }
1299
1300    /// Length of the longest contiguous prefix of well-formed pages on the blob.
1301    ///
1302    /// [`Recovery::open`] sizes a blob by scanning backward to its last valid page, which cannot
1303    /// detect an earlier page that was lost or corrupted. This scans forward instead, stopping
1304    /// at the first invalid or short page.
1305    ///
1306    /// Expects all appended bytes to have reached the blob (as after recovery): a partial page
1307    /// still buffered in this writer is unreadable from the blob and fails the scan. `buffer_size`
1308    /// bounds each blob read, with a minimum of one physical page. Applies `read_options` to
1309    /// every blob read.
1310    ///
1311    /// `proven` is a logical byte offset already known valid (a durability watermark or a
1312    /// replay-validated prefix). Pages wholly below it are accepted without reading, and the
1313    /// scan starts at the page containing it. A proof past the blob's content clamps to the
1314    /// full pages that exist. A partial tail must be read to determine its valid length.
1315    pub async fn recoverable_prefix_len(
1316        &self,
1317        proven: u64,
1318        buffer_size: NonZeroUsize,
1319        read_options: ReadOptions,
1320    ) -> Result<u64, Error> {
1321        self.recoverable_prefix_len_at_most(proven, u64::MAX, buffer_size, read_options)
1322            .await
1323    }
1324
1325    /// Like [`Self::recoverable_prefix_len`], but returns at most `max_size` logical bytes.
1326    ///
1327    /// Reads only pages intersecting the prefix below `max_size`. A cap inside a page requires
1328    /// validating that entire page's checksum. Zero requires no reads. A cap above the stored end
1329    /// does not extend the prefix. `proven` has the same meaning as in the unbounded scan.
1330    pub async fn recoverable_prefix_len_at_most(
1331        &self,
1332        proven: u64,
1333        max_size: u64,
1334        buffer_size: NonZeroUsize,
1335        read_options: ReadOptions,
1336    ) -> Result<u64, Error> {
1337        let logical_page_size: u64 = self.cache_ref.page_size().widen();
1338        let total_pages = (self.current_page + u64::from(self.partial_page_state.is_some()))
1339            .min(max_size.div_ceil(logical_page_size));
1340        let physical_page_size = logical_page_size
1341            .checked_add(CHECKSUM_SIZE)
1342            .ok_or(Error::OffsetOverflow)?;
1343        let physical_page_size_usize =
1344            usize::try_from(physical_page_size).map_err(|_| Error::OffsetOverflow)?;
1345        let max_batch_pages: u64 =
1346            Widen::widen((buffer_size.get() / physical_page_size_usize).max(1));
1347
1348        // Pages below the proof are accepted without reading. Clamp the starting page to the
1349        // full pages present: the partial tail must be read to determine how many bytes it adds
1350        // to the valid prefix.
1351        let start_page = (proven / logical_page_size)
1352            .min(self.current_page)
1353            .min(total_pages);
1354        let mut valid_len = start_page
1355            .checked_mul(logical_page_size)
1356            .ok_or(Error::OffsetOverflow)?;
1357        let mut page = start_page;
1358        while page < total_pages {
1359            // Bound each read while deriving its physical range with checked arithmetic.
1360            let batch_pages = max_batch_pages.min(total_pages - page);
1361            let batch_end = page.checked_add(batch_pages).ok_or(Error::OffsetOverflow)?;
1362            let physical_offset = page
1363                .checked_mul(physical_page_size)
1364                .ok_or(Error::OffsetOverflow)?;
1365            let physical_len = batch_pages
1366                .checked_mul(physical_page_size)
1367                .ok_or(Error::OffsetOverflow)?;
1368            let physical_len = usize::try_from(physical_len).map_err(|_| Error::OffsetOverflow)?;
1369
1370            // Coalesce once so each physical page can be checksum-validated in place.
1371            let physical = self
1372                .blob
1373                .read_at(physical_offset, physical_len, read_options)
1374                .await?
1375                .coalesce();
1376
1377            // The first invalid page terminates the only recoverable contiguous prefix.
1378            for physical_page in physical.as_ref().chunks_exact(physical_page_size_usize) {
1379                let Some(checksum) = Checksum::validate_page(physical_page) else {
1380                    return Ok(valid_len.min(max_size));
1381                };
1382                let len = u64::from(checksum.len);
1383                valid_len = valid_len.checked_add(len).ok_or(Error::OffsetOverflow)?;
1384
1385                // A valid partial logical page ends the contiguous prefix wherever it appears.
1386                if len < logical_page_size {
1387                    return Ok(valid_len.min(max_size));
1388                }
1389            }
1390            page = batch_end;
1391        }
1392        Ok(valid_len.min(max_size))
1393    }
1394
1395    /// Wait for any started sync to complete without starting a new sync.
1396    pub async fn wait_for_sync(mut self) -> Result<Self, Error> {
1397        self.sync_state.wait_for_pending().await?;
1398        Ok(self)
1399    }
1400}
1401
1402#[cfg(test)]
1403#[commonware_macros::test_group("miri")]
1404mod tests {
1405    use super::*;
1406    use crate::{
1407        Buf, Handle, IoBufsMut, Runner as _, Spawner as _, Storage as _, Supervisor as _,
1408        buffer::{paged::CHECKSUM_SLOT_SIZE, tests::SyncTrackingBlob},
1409        deterministic,
1410        mocks::{
1411            DelayedSyncBlob, RecordingContext, WriteFaultContext, WriteFaults, next_pending_sync,
1412        },
1413    };
1414    use commonware_codec::{Copying, FixedSize, ReadExt};
1415    use commonware_macros::test_traced;
1416    use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
1417    use futures::FutureExt as _;
1418    use std::{
1419        cell::Cell,
1420        num::NonZeroU16,
1421        sync::{
1422            Arc,
1423            atomic::{AtomicUsize, Ordering},
1424        },
1425    };
1426
1427    impl<B: Blob, Phase> Writer<B, Phase> {
1428        /// Return the page-cache id used for reads.
1429        pub(in super::super) const fn cache_id(&self) -> u64 {
1430            self.id
1431        }
1432    }
1433
1434    const PAGE_SIZE: NonZeroU16 = NZU16!(103); // janky size to ensure we test page alignment
1435    const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
1436
1437    /// `try_append` buffers bytes within the flush threshold at the writer's size and leaves the
1438    /// writer unchanged otherwise.
1439    #[test_traced]
1440    fn test_try_append_capacity() {
1441        deterministic::Runner::default().start(|context| async move {
1442            let (blob, size) = context.open("try_append", b"blob").await.unwrap();
1443            let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1444            let mut writer = Writer::new(blob, size, 32, cache).await.unwrap();
1445
1446            // Bytes within the threshold are buffered at the current size.
1447            assert_eq!(writer.try_append(&[1; 20]), Some(0));
1448            assert_eq!(writer.try_append(&[2; 12]), Some(20));
1449            assert_eq!(writer.size(), 32);
1450
1451            // Bytes past the threshold are refused without changing the writer.
1452            assert_eq!(writer.try_append(&[3]), None);
1453            assert_eq!(writer.size(), 32);
1454
1455            // The refused bytes take the ordinary flushing path, and everything reads back.
1456            let (writer, offset) = writer.append(&[3]).await.unwrap();
1457            assert_eq!(offset, 32);
1458            let read = writer.read_at(0, 33).await.unwrap().coalesce();
1459            assert_eq!(&read.as_ref()[..20], &[1; 20]);
1460            assert_eq!(&read.as_ref()[20..32], &[2; 12]);
1461            assert_eq!(read.as_ref()[32], 3);
1462        });
1463    }
1464
1465    #[test_traced]
1466    fn test_append_value_capacity_snapshot_and_recovery() {
1467        deterministic::Runner::default().start(|context| async move {
1468            struct MustNotEncode<const SIZE: usize>;
1469            impl<const SIZE: usize> FixedSize for MustNotEncode<SIZE> {
1470                const SIZE: usize = SIZE;
1471            }
1472            impl<const SIZE: usize> Write for MustNotEncode<SIZE> {
1473                fn write(&self, _: &mut impl BufMut) {
1474                    panic!("insufficient capacity must not invoke the encoder");
1475                }
1476            }
1477
1478            let (blob, size) = context.open("append_value", b"blob").await.unwrap();
1479            let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1480            let mut writer = Writer::new(blob, size, 32, cache.clone()).await.unwrap();
1481            assert_eq!(writer.try_append_value(&0u64), Some(0));
1482            assert_eq!(writer.try_append_value(&MustNotEncode::<25>), None);
1483            let (mut writer, snapshot) = writer.snapshot().await.unwrap();
1484            for i in 1..4u64 {
1485                assert_eq!(writer.try_append_value(&i), Some(i * 8));
1486            }
1487
1488            assert_eq!(writer.try_append_value(&MustNotEncode::<8>), None);
1489            assert_eq!(writer.size(), 32);
1490            assert_eq!(
1491                snapshot.read_at(0, 8).await.unwrap().coalesce().as_ref(),
1492                &0u64.to_be_bytes()
1493            );
1494
1495            // The rejected value can use the ordinary crossing path without losing data.
1496            let (writer, offset) = writer.append(&4u64.to_be_bytes()).await.unwrap();
1497            assert_eq!(offset, 32);
1498            let writer = writer.sync().await.unwrap();
1499
1500            // The snapshot shares the writer's blob, so drop both before reopening. The reopened
1501            // writer must recover all five synced values.
1502            drop(snapshot);
1503            drop(writer);
1504            let (blob, size) = context.open("append_value", b"blob").await.unwrap();
1505            let writer = Writer::new(blob, size, 32, cache).await.unwrap();
1506            assert_eq!(writer.size(), 40);
1507            let expected: Vec<_> = (0..5u64).flat_map(u64::to_be_bytes).collect();
1508            assert_eq!(
1509                writer.read_at(0, 40).await.unwrap().coalesce().as_ref(),
1510                expected
1511            );
1512        });
1513    }
1514
1515    #[test_traced]
1516    fn test_append_variable_value_encodes_in_place() {
1517        // Record encoder calls and the first writable address to detect staging copies.
1518        struct Value {
1519            size: usize,
1520            size_calls: Cell<usize>,
1521            writes: Cell<usize>,
1522            destination: Cell<usize>,
1523        }
1524
1525        impl EncodeSize for Value {
1526            fn encode_size(&self) -> usize {
1527                self.size_calls.set(self.size_calls.get() + 1);
1528                self.size
1529            }
1530        }
1531
1532        impl Write for Value {
1533            fn write(&self, buf: &mut impl BufMut) {
1534                self.writes.set(self.writes.get() + 1);
1535                if self.size > 0 {
1536                    self.destination.set(buf.chunk_mut().as_mut_ptr() as usize);
1537                }
1538                buf.put_bytes(7, self.size);
1539            }
1540        }
1541
1542        deterministic::Runner::default().start(|context| async move {
1543            let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
1544            let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1545            let mut writer = Writer::new(blob, size, 64, cache.clone()).await.unwrap();
1546            let mut value = Value {
1547                size: 3,
1548                size_calls: Cell::new(0),
1549                writes: Cell::new(0),
1550                destination: Cell::new(0),
1551            };
1552
1553            // The encoder writes into the tip itself, including when it crosses into a new
1554            // allocation and the old tail joins the full-page prefix.
1555            assert_eq!(writer.try_append_value(&value), Some(0));
1556            let tail = writer.buffer.parts().1.as_ptr() as usize;
1557            assert_eq!(value.destination.get(), tail);
1558            value.size = 5;
1559            assert_eq!(writer.try_append_value(&value), Some(3));
1560            assert_eq!(value.destination.get(), tail + 3);
1561            value.size = 17;
1562            assert_eq!(writer.try_append_value(&value), Some(8));
1563            assert_eq!(value.destination.get(), tail + 8);
1564            assert_eq!(
1565                writer.buffer.parts().0.chunk_at(0).unwrap().as_ptr() as usize,
1566                tail
1567            );
1568
1569            // Empty values are valid. Sizing happens once per attempt, and a capacity miss
1570            // never invokes the encoder or changes the writer.
1571            value.size = 0;
1572            assert_eq!(writer.try_append_value(&value), Some(25));
1573            value.size = 39;
1574            assert_eq!(writer.try_append_value(&value), Some(25));
1575            for size in [1, usize::MAX] {
1576                value.size = size;
1577                assert_eq!(writer.try_append_value(&value), None);
1578            }
1579            value.size = 0;
1580            assert_eq!(writer.try_append_value(&value), Some(64));
1581            assert_eq!(value.size_calls.get(), 8);
1582            assert_eq!(value.writes.get(), 6);
1583            assert_eq!(writer.size(), 64);
1584
1585            // Reopen the blob to verify that directly encoded bytes persist.
1586            writer = writer.sync().await.unwrap();
1587            drop(writer);
1588            let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
1589            let writer = Writer::new(blob, size, 64, cache).await.unwrap();
1590            assert_eq!(writer.size(), 64);
1591            assert_eq!(
1592                writer.read_at(0, 64).await.unwrap().coalesce().as_ref(),
1593                &[7; 64]
1594            );
1595        });
1596    }
1597
1598    #[test_traced("DEBUG")]
1599    fn test_writes_use_uncached_hint() {
1600        let executor = deterministic::Runner::default();
1601        executor.start(|_: deterministic::Context| async move {
1602            let blob = SyncTrackingBlob::new();
1603            let blob = Arc::new(blob);
1604            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1605            let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
1606                .await
1607                .unwrap();
1608            writer = writer.sync().await.unwrap();
1609
1610            (writer, _) = writer.append(b"first").await.unwrap();
1611            writer = writer.sync().await.unwrap();
1612            assert_eq!(blob.uncached_snapshot(), (0, 1));
1613
1614            (writer, _) = writer.append(b"second").await.unwrap();
1615            let (_, sync) = writer.seal().await.unwrap();
1616            sync.await.unwrap();
1617            assert_eq!(blob.uncached_snapshot(), (1, 1));
1618        });
1619    }
1620
1621    /// Unsynced partial-page flushes ([Writer::replay]) rewrite the tail page without a durability
1622    /// barrier. Every rewrite must keep the page's durable checksum slot byte-identical: a crash
1623    /// can cut the unsynced rewrites per byte, and whichever bytes land, the footer must still
1624    /// validate the synced prefix.
1625    #[test_traced("DEBUG")]
1626    fn test_unsynced_flushes_preserve_durable_checksum_slot() {
1627        let executor = deterministic::Runner::default();
1628        executor.start(|context: deterministic::Context| async move {
1629            let page_size = PAGE_SIZE.get() as usize;
1630            let physical_page = page_size + CHECKSUM_SIZE as usize;
1631            let (blob, blob_size) = context
1632                .open("test_partition", b"replay_torn")
1633                .await
1634                .unwrap();
1635            let blob = Arc::new(blob);
1636            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1637            let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref.clone())
1638                .await
1639                .unwrap();
1640
1641            // Make a mid-page prefix durable and capture the page's durable image.
1642            let synced: Vec<u8> = (1u8..=24).collect();
1643            (writer, _) = writer.append(&synced).await.unwrap();
1644            writer = writer.sync().await.unwrap();
1645            let durable_image = blob
1646                .read_at(0, physical_page, ReadOptions::default())
1647                .await
1648                .unwrap()
1649                .coalesce();
1650
1651            // Two unsynced rewrites of the same page.
1652            (writer, _) = writer.append(&[25u8; 8]).await.unwrap();
1653            (writer, _) = writer
1654                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1655                .await
1656                .unwrap();
1657            (writer, _) = writer.append(&[26u8; 8]).await.unwrap();
1658            writer
1659                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1660                .await
1661                .unwrap();
1662            let torn_image = blob
1663                .read_at(0, physical_page, ReadOptions::default())
1664                .await
1665                .unwrap()
1666                .coalesce();
1667
1668            // Crash: of the unsynced rewrites, only the final one's footer bytes land, while
1669            // the logical region keeps its durable bytes.
1670            let mut crash = durable_image.as_ref().to_vec();
1671            crash[page_size..].copy_from_slice(&torn_image.as_ref()[page_size..]);
1672            let (crashed, _) = context
1673                .open("test_partition", b"replay_crash")
1674                .await
1675                .unwrap();
1676            crashed
1677                .write_at(0, crash, WriteOptions::default())
1678                .await
1679                .unwrap();
1680            crashed.sync().await.unwrap();
1681
1682            // The synced prefix must recover through the preserved durable slot.
1683            let recovered = Writer::new(crashed, physical_page as u64, BUFFER_SIZE, cache_ref)
1684                .await
1685                .unwrap();
1686            assert_eq!(recovered.size(), synced.len() as u64);
1687            let read = recovered.read_at(0, synced.len()).await.unwrap().coalesce();
1688            assert_eq!(read.as_ref(), synced.as_slice());
1689        });
1690    }
1691
1692    /// `recoverable_prefix_len` returns the full logical size when every page is well-formed.
1693    #[test_traced("DEBUG")]
1694    fn test_recoverable_prefix_len_clean() {
1695        let executor = deterministic::Runner::default();
1696        executor.start(|context: deterministic::Context| async move {
1697            let (context, recordings) = RecordingContext::new(context);
1698            let (blob, blob_size) = context
1699                .open("test_partition", b"prefix_clean")
1700                .await
1701                .unwrap();
1702            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1703            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1704                .await
1705                .unwrap();
1706            assert_eq!(
1707                writer
1708                    .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1709                    .await
1710                    .unwrap(),
1711                0
1712            );
1713
1714            let total = PAGE_SIZE.get() as usize * 2 + 50;
1715            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1716            (writer, _) = writer.append(&data).await.unwrap();
1717            writer = writer.sync().await.unwrap();
1718
1719            // Prefix validation uses the default options so recovery can reuse the validated pages.
1720            recordings.clear();
1721            assert_eq!(
1722                writer
1723                    .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1724                    .await
1725                    .unwrap(),
1726                total as u64
1727            );
1728            let reads = recordings.snapshot().reads;
1729            assert!(!reads.is_empty());
1730            assert!(
1731                reads
1732                    .iter()
1733                    .all(|options| *options == ReadOptions::default())
1734            );
1735
1736            drop(writer);
1737            let (blob, blob_size) = context
1738                .open("test_partition", b"prefix_clean")
1739                .await
1740                .unwrap();
1741
1742            // Reopening also validates the disk-authoritative tail with the default options.
1743            recordings.clear();
1744            let _recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1745                .await
1746                .unwrap();
1747            let reads = recordings.snapshot().reads;
1748            assert!(!reads.is_empty());
1749            assert!(
1750                reads
1751                    .iter()
1752                    .all(|options| *options == ReadOptions::default())
1753            );
1754        });
1755    }
1756
1757    /// The scan stops at the first torn page even when later pages remain valid, catching the
1758    /// interior hole the backward scan in `Writer::new` misses.
1759    #[test_traced("DEBUG")]
1760    fn test_recoverable_prefix_len_torn_interior_page() {
1761        let executor = deterministic::Runner::default();
1762        executor.start(|context: deterministic::Context| async move {
1763            let (blob, blob_size) = context
1764                .open("test_partition", b"prefix_torn")
1765                .await
1766                .unwrap();
1767            let blob = Arc::new(blob);
1768            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1769            let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1770                .await
1771                .unwrap();
1772            let total = PAGE_SIZE.get() as usize * 3 + 10;
1773            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1774            (writer, _) = writer.append(&data).await.unwrap();
1775            writer = writer.sync().await.unwrap();
1776
1777            // Tear page 1, leaving pages 0 and 2+ valid.
1778            let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1779            let offset = physical_page_size + 7;
1780            let byte = blob
1781                .read_at(offset, 1, ReadOptions::default())
1782                .await
1783                .unwrap()
1784                .coalesce();
1785            blob.write_at(
1786                offset,
1787                vec![byte.as_ref()[0] ^ 0xFF],
1788                WriteOptions::default(),
1789            )
1790            .await
1791            .unwrap();
1792            blob.sync().await.unwrap();
1793
1794            assert_eq!(
1795                writer
1796                    .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1797                    .await
1798                    .unwrap(),
1799                PAGE_SIZE.get() as u64
1800            );
1801        });
1802    }
1803
1804    /// A proven prefix skips its pages without reading them: a scan started past a torn page
1805    /// accepts the caller's proof, and a proof past the blob's end clamps without panicking.
1806    #[test_traced("DEBUG")]
1807    fn test_recoverable_prefix_len_proven_prefix() {
1808        let executor = deterministic::Runner::default();
1809        executor.start(|context: deterministic::Context| async move {
1810            let (blob, blob_size) = context
1811                .open("test_partition", b"prefix_proven")
1812                .await
1813                .unwrap();
1814            let blob = Arc::new(blob);
1815            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1816            let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1817                .await
1818                .unwrap();
1819            let total = PAGE_SIZE.get() as usize * 4;
1820            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1821            (writer, _) = writer.append(&data).await.unwrap();
1822            writer = writer.sync().await.unwrap();
1823
1824            // Tear page 1, leaving pages 0 and 2+ valid.
1825            let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1826            let offset = physical_page_size + 7;
1827            let byte = blob
1828                .read_at(offset, 1, ReadOptions::default())
1829                .await
1830                .unwrap()
1831                .coalesce();
1832            blob.write_at(
1833                offset,
1834                vec![byte.as_ref()[0] ^ 0xFF],
1835                WriteOptions::default(),
1836            )
1837            .await
1838            .unwrap();
1839            blob.sync().await.unwrap();
1840
1841            // An unproven scan stops at the torn page. A proof past it skips the damage and
1842            // scans the remainder, and a mid-page proof rescans the page containing it.
1843            let page = u64::from(PAGE_SIZE.get());
1844            for (proven, expected) in [
1845                (0, page),
1846                (page, page),
1847                (2 * page, 4 * page),
1848                (2 * page + 3, 4 * page),
1849                (total as u64, 4 * page),
1850            ] {
1851                assert_eq!(
1852                    writer
1853                        .recoverable_prefix_len(
1854                            proven,
1855                            NZUsize!(BUFFER_SIZE),
1856                            ReadOptions::default()
1857                        )
1858                        .await
1859                        .unwrap(),
1860                    expected,
1861                    "proven {proven}"
1862                );
1863            }
1864
1865            // A proof past the blob clamps to the pages that exist. Callers only pass proven
1866            // prefixes, and storage-level guards reject a prefix the blob cannot back.
1867            assert_eq!(
1868                writer
1869                    .recoverable_prefix_len(
1870                        100 * page,
1871                        NZUsize!(BUFFER_SIZE),
1872                        ReadOptions::default()
1873                    )
1874                    .await
1875                    .unwrap(),
1876                4 * page
1877            );
1878
1879            // A partial tail is still read when the proof overshoots the blob: the clamp stops
1880            // at the full pages, so the tail contributes its logical length, not a full page.
1881            (writer, _) = writer.append(&data[..20]).await.unwrap();
1882            writer = writer.sync().await.unwrap();
1883            for proven in [4 * page + 20, 5 * page, 100 * page] {
1884                assert_eq!(
1885                    writer
1886                        .recoverable_prefix_len(
1887                            proven,
1888                            NZUsize!(BUFFER_SIZE),
1889                            ReadOptions::default()
1890                        )
1891                        .await
1892                        .unwrap(),
1893                    4 * page + 20,
1894                    "proven {proven}"
1895                );
1896            }
1897        });
1898    }
1899
1900    /// A valid-but-short interior page ends the prefix: a partial page's durable state can
1901    /// survive a crash while its extension to a full page is lost.
1902    #[test_traced("DEBUG")]
1903    fn test_recoverable_prefix_len_stale_short_interior_page() {
1904        let executor = deterministic::Runner::default();
1905        executor.start(|context: deterministic::Context| async move {
1906            let (blob, blob_size) = context
1907                .open("test_partition", b"prefix_stale")
1908                .await
1909                .unwrap();
1910            let blob = Arc::new(blob);
1911            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1912            let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1913                .await
1914                .unwrap();
1915
1916            // Persist a partial first page and capture its physical bytes.
1917            let total = PAGE_SIZE.get() as usize * 2;
1918            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1919            (writer, _) = writer.append(&data[..20]).await.unwrap();
1920            writer = writer.sync().await.unwrap();
1921            let physical_page_size = (PAGE_SIZE.get() as u64 + CHECKSUM_SIZE) as usize;
1922            let stale = blob
1923                .read_at(0, physical_page_size, ReadOptions::default())
1924                .await
1925                .unwrap()
1926                .coalesce();
1927            let stale = stale.as_ref().to_vec();
1928
1929            // Extend past the first page, persist, then restore page 0 to its stale partial
1930            // state as if the extension never reached disk.
1931            (writer, _) = writer.append(&data[20..]).await.unwrap();
1932            writer = writer.sync().await.unwrap();
1933            blob.write_at(0, stale, WriteOptions::default())
1934                .await
1935                .unwrap();
1936            blob.sync().await.unwrap();
1937
1938            assert_eq!(
1939                writer
1940                    .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1941                    .await
1942                    .unwrap(),
1943                20
1944            );
1945        });
1946    }
1947
1948    /// A clean scan derives its read batches from the supplied byte budget while always making
1949    /// progress by at least one page.
1950    #[test_traced("DEBUG")]
1951    fn test_recoverable_prefix_len_batches_reads() {
1952        let executor = deterministic::Runner::default();
1953        executor.start(|context: deterministic::Context| async move {
1954            let (context, recordings) = RecordingContext::new(context);
1955            let (blob, blob_size) = context
1956                .open("test_partition", b"prefix_batches")
1957                .await
1958                .unwrap();
1959            let blob = Arc::new(blob);
1960            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1961            let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1962                .await
1963                .unwrap();
1964
1965            // Eight full pages plus a partial tail span three batches under a three-page budget.
1966            let total = PAGE_SIZE.get() as usize * 8 + 10;
1967            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1968            (writer, _) = writer.append(&data).await.unwrap();
1969            writer = writer.sync().await.unwrap();
1970
1971            // A sub-page budget falls back to one page per read.
1972            recordings.clear();
1973            assert_eq!(
1974                writer
1975                    .recoverable_prefix_len(0, NZUsize!(1), ReadOptions::default())
1976                    .await
1977                    .unwrap(),
1978                total as u64
1979            );
1980            assert_eq!(recordings.snapshot().reads.len(), 9);
1981
1982            // A three-page budget coalesces the same scan into three reads.
1983            recordings.clear();
1984            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
1985            let scan_buffer = NonZeroUsize::new(physical_page_size * 3).unwrap();
1986            assert_eq!(
1987                writer
1988                    .recoverable_prefix_len(0, scan_buffer, ReadOptions::default())
1989                    .await
1990                    .unwrap(),
1991                total as u64
1992            );
1993            assert_eq!(recordings.snapshot().reads.len(), 3);
1994        });
1995    }
1996
1997    #[test_traced("DEBUG")]
1998    fn test_recoverable_prefix_len_at_most() {
1999        deterministic::Runner::default().start(|context| async move {
2000            let (context, recordings) = RecordingContext::new(context);
2001            let (blob, size) = context.open("bounded_prefix", b"blob").await.unwrap();
2002            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2003            let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
2004            let page = u64::from(PAGE_SIZE.get());
2005            let total = page * 8 + 10;
2006            (writer, _) = writer.append(&vec![7; total as usize]).await.unwrap();
2007            writer = writer.sync().await.unwrap();
2008
2009            // One-page reads make touching any page beyond the cap observable.
2010            for (cap, proven, expected_reads) in [
2011                (0, 0, 0),
2012                (1, 0, 1),
2013                (page, 0, 1),
2014                (page + 1, 0, 2),
2015                (page * 3, page, 2),
2016                (1, u64::MAX, 0),
2017                (total - 1, 0, 9),
2018                (total, 0, 9),
2019                (total + 1, 0, 9),
2020                (u64::MAX, 0, 9),
2021                (u64::MAX, u64::MAX, 1),
2022            ] {
2023                recordings.clear();
2024                assert_eq!(
2025                    writer
2026                        .recoverable_prefix_len_at_most(
2027                            proven,
2028                            cap,
2029                            NZUsize!(1),
2030                            ReadOptions::DONT_CACHE,
2031                        )
2032                        .await
2033                        .unwrap(),
2034                    cap.min(total),
2035                    "cap {cap}, proven {proven}",
2036                );
2037                let reads = recordings.snapshot().reads;
2038                assert_eq!(reads.len(), expected_reads, "cap {cap}, proven {proven}");
2039                assert!(
2040                    reads
2041                        .iter()
2042                        .all(|options| *options == ReadOptions::DONT_CACHE)
2043                );
2044            }
2045        });
2046    }
2047
2048    #[test_traced("DEBUG")]
2049    fn test_read_many_into_empty() {
2050        let executor = deterministic::Runner::default();
2051        executor.start(|context: deterministic::Context| async move {
2052            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2053            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2054            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2055                .await
2056                .unwrap();
2057
2058            (append, _) = append.append(&[0u8; 8]).await.unwrap();
2059            assert_eq!(append.size(), 8);
2060
2061            // Empty offsets should succeed immediately.
2062            let mut buf = [];
2063            append
2064                .read_many_into(&mut buf, &[], NZUsize!(4))
2065                .await
2066                .unwrap();
2067        });
2068    }
2069
2070    #[test_traced("DEBUG")]
2071    fn test_read_many_into_all_in_tip() {
2072        // All items reside in the unflushed tip buffer.
2073        let executor = deterministic::Runner::default();
2074        executor.start(|context: deterministic::Context| async move {
2075            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2076            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2077            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2078                .await
2079                .unwrap();
2080
2081            let data: Vec<u8> = (0..20).collect();
2082            (append, _) = append.append(&data).await.unwrap();
2083            assert_eq!(append.size(), 20);
2084
2085            // Read 4-byte items at offsets 0, 4, 8, 12, 16.
2086            let offsets = [0u64, 4, 8, 12, 16];
2087            let mut buf = vec![0u8; 5 * 4];
2088            append
2089                .read_many_into(&mut buf, &offsets, NZUsize!(4))
2090                .await
2091                .unwrap();
2092
2093            for (i, &off) in offsets.iter().enumerate() {
2094                assert_eq!(
2095                    &buf[i * 4..(i + 1) * 4],
2096                    &data[off as usize..off as usize + 4],
2097                );
2098            }
2099        });
2100    }
2101
2102    #[test_traced("DEBUG")]
2103    fn test_try_read_sync_all_in_tip() {
2104        let executor = deterministic::Runner::default();
2105        executor.start(|context: deterministic::Context| async move {
2106            let (blob, blob_size) = context
2107                .open("test_partition", b"try_read_sync_tip")
2108                .await
2109                .unwrap();
2110            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2111            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2112                .await
2113                .unwrap();
2114
2115            let data: Vec<u8> = (0..20).collect();
2116            (append, _) = append.append(&data).await.unwrap();
2117
2118            let mut buf = vec![0u8; data.len()];
2119            assert!(append.try_read_sync_into(&mut buf, 0));
2120            assert_eq!(buf, data);
2121        });
2122    }
2123
2124    #[test_traced("DEBUG")]
2125    fn test_try_read_sync_cache_miss() {
2126        let executor = deterministic::Runner::default();
2127        executor.start(|context: deterministic::Context| async move {
2128            let (blob, blob_size) = context
2129                .open("test_partition", b"try_read_sync_cache_miss")
2130                .await
2131                .unwrap();
2132
2133            // A one-page cache lets us prime the first page while leaving the second uncached.
2134            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2135            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2136                .await
2137                .unwrap();
2138
2139            let page_size = PAGE_SIZE.get() as usize;
2140            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
2141            (append, _) = append.append(&data).await.unwrap();
2142            append = append.sync().await.unwrap();
2143
2144            let _ = append.read_at(0, page_size).await.unwrap();
2145
2146            // A read straddling the cached first page and the uncached second page misses.
2147            let mut buf = vec![0xAA; 4];
2148            assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
2149        });
2150    }
2151
2152    #[test_traced("DEBUG")]
2153    fn test_read_many_into_all_from_cache() {
2154        // Synced bytes remain readable from the retained partial tip.
2155        let executor = deterministic::Runner::default();
2156        executor.start(|context: deterministic::Context| async move {
2157            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2158            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2159            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2160                .await
2161                .unwrap();
2162
2163            let data: Vec<u8> = (0..20).collect();
2164            (append, _) = append.append(&data).await.unwrap();
2165            append = append.sync().await.unwrap();
2166            assert_eq!(append.size(), 20);
2167
2168            let offsets = [0u64, 8, 16];
2169            let mut buf = vec![0u8; 3 * 4];
2170            append
2171                .read_many_into(&mut buf, &offsets, NZUsize!(4))
2172                .await
2173                .unwrap();
2174
2175            for (i, &off) in offsets.iter().enumerate() {
2176                assert_eq!(
2177                    &buf[i * 4..(i + 1) * 4],
2178                    &data[off as usize..off as usize + 4],
2179                );
2180            }
2181        });
2182    }
2183
2184    #[test_traced("DEBUG")]
2185    fn test_read_many_into_mixed_tip_and_cache() {
2186        // Synced and unsynced bytes share the same partial tip.
2187        let executor = deterministic::Runner::default();
2188        executor.start(|context: deterministic::Context| async move {
2189            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2190            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2191            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2192                .await
2193                .unwrap();
2194
2195            let first: Vec<u8> = (0..16).collect();
2196            (append, _) = append.append(&first).await.unwrap();
2197            append = append.sync().await.unwrap();
2198
2199            let second: Vec<u8> = (16..32).collect();
2200            (append, _) = append.append(&second).await.unwrap();
2201            assert_eq!(append.size(), 32);
2202
2203            // Offsets span both synced and unsynced regions.
2204            let offsets = [0u64, 4, 16, 24];
2205            let mut buf = vec![0u8; 4 * 4];
2206            append
2207                .read_many_into(&mut buf, &offsets, NZUsize!(4))
2208                .await
2209                .unwrap();
2210
2211            let all: Vec<u8> = (0..32).collect();
2212            for (i, &off) in offsets.iter().enumerate() {
2213                assert_eq!(
2214                    &buf[i * 4..(i + 1) * 4],
2215                    &all[off as usize..off as usize + 4],
2216                );
2217            }
2218        });
2219    }
2220
2221    #[test_traced("DEBUG")]
2222    fn test_read_many_into_out_of_bounds() {
2223        let executor = deterministic::Runner::default();
2224        executor.start(|context: deterministic::Context| async move {
2225            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2226            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2227            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2228                .await
2229                .unwrap();
2230
2231            (append, _) = append.append(&[0u8; 8]).await.unwrap();
2232            assert_eq!(append.size(), 8);
2233
2234            // Last offset's end (8 + 4 = 12) exceeds size (8).
2235            let mut buf = vec![0u8; 4];
2236            let err = append
2237                .read_many_into(&mut buf, &[8], NZUsize!(4))
2238                .await
2239                .unwrap_err();
2240            assert!(matches!(err, Error::BlobInsufficientLength));
2241        });
2242    }
2243
2244    #[test_traced("DEBUG")]
2245    fn test_read_many_into_single_item() {
2246        let executor = deterministic::Runner::default();
2247        executor.start(|context: deterministic::Context| async move {
2248            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2249            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2250            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2251                .await
2252                .unwrap();
2253
2254            let data = vec![0xAA; 8];
2255            (append, _) = append.append(&data).await.unwrap();
2256            assert_eq!(append.size(), 8);
2257
2258            let mut buf = vec![0u8; 8];
2259            append
2260                .read_many_into(&mut buf, &[0], NZUsize!(8))
2261                .await
2262                .unwrap();
2263            assert_eq!(&buf, &data);
2264        });
2265    }
2266
2267    #[test_traced("DEBUG")]
2268    #[should_panic(expected = "buf must hold one slot per range totaling its length")]
2269    fn test_read_many_into_rejects_invalid_buffer_len() {
2270        let executor = deterministic::Runner::default();
2271        executor.start(|context: deterministic::Context| async move {
2272            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2273            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2274            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2275                .await
2276                .unwrap();
2277
2278            let data: Vec<u8> = (0..16).collect();
2279            (append, _) = append.append(&data).await.unwrap();
2280
2281            let offsets = [0u64, 4];
2282            let mut buf = vec![0u8; 7];
2283            let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
2284        });
2285    }
2286
2287    #[test_traced("DEBUG")]
2288    #[should_panic(expected = "ranges must be sorted and non-overlapping")]
2289    fn test_read_many_into_rejects_unsorted_offsets() {
2290        let executor = deterministic::Runner::default();
2291        executor.start(|context: deterministic::Context| async move {
2292            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2293            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2294            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2295                .await
2296                .unwrap();
2297
2298            let data: Vec<u8> = (0..16).collect();
2299            (append, _) = append.append(&data).await.unwrap();
2300
2301            let mut buf = vec![0u8; 8];
2302            let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
2303        });
2304    }
2305
2306    #[test_traced("DEBUG")]
2307    #[should_panic(expected = "ranges must be sorted and non-overlapping")]
2308    fn test_read_many_into_rejects_overlapping_offsets() {
2309        let executor = deterministic::Runner::default();
2310        executor.start(|context: deterministic::Context| async move {
2311            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2312            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2313            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2314                .await
2315                .unwrap();
2316
2317            let data: Vec<u8> = (0..16).collect();
2318            (append, _) = append.append(&data).await.unwrap();
2319
2320            let mut buf = vec![0u8; 8];
2321            let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
2322        });
2323    }
2324
2325    #[test_traced("DEBUG")]
2326    fn test_read_many_into_rejects_offset_overflow() {
2327        let executor = deterministic::Runner::default();
2328        executor.start(|context: deterministic::Context| async move {
2329            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2330            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2331            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2332                .await
2333                .unwrap();
2334
2335            let data: Vec<u8> = (0..16).collect();
2336            (append, _) = append.append(&data).await.unwrap();
2337
2338            let mut buf = vec![0u8; 8];
2339            let err = append
2340                .read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
2341                .await
2342                .unwrap_err();
2343            assert!(matches!(err, Error::OffsetOverflow));
2344        });
2345    }
2346
2347    #[test_traced("DEBUG")]
2348    fn test_read_many_into_matches_read_at() {
2349        // Verify read_many_into returns the same bytes as individual read_at calls.
2350        let executor = deterministic::Runner::default();
2351        executor.start(|context: deterministic::Context| async move {
2352            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2353            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2354            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2355                .await
2356                .unwrap();
2357
2358            // Write enough data to span multiple pages (PAGE_SIZE=103).
2359            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
2360            (append, _) = append.append(&data).await.unwrap();
2361            append = append.sync().await.unwrap();
2362
2363            // Add more in tip buffer.
2364            let more: Vec<u8> = (0u8..50).collect();
2365            (append, _) = append.append(&more).await.unwrap();
2366            assert_eq!(append.size(), 350);
2367
2368            let item_size = 10;
2369            let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
2370            let mut batch_buf = vec![0u8; offsets.len() * item_size];
2371            append
2372                .read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
2373                .await
2374                .unwrap();
2375
2376            // Compare each item against individual read_at.
2377            for (i, &off) in offsets.iter().enumerate() {
2378                let single = append.read_at(off, item_size).await.unwrap().coalesce();
2379                assert_eq!(
2380                    &batch_buf[i * item_size..(i + 1) * item_size],
2381                    single.as_ref(),
2382                    "mismatch at offset {off}",
2383                );
2384            }
2385        });
2386    }
2387
2388    #[test_traced("DEBUG")]
2389    fn test_read_many_into_scattered_cache_misses() {
2390        // Exercises all three source paths in a single read_many_into call:
2391        // tip buffer, page cache hit, and page cache miss (blob I/O).
2392        // The tip holds a partial page so one item straddles the tip boundary.
2393        let executor = deterministic::Runner::default();
2394        executor.start(|context: deterministic::Context| async move {
2395            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2396
2397            // Small cache: only 2 pages, so we can force eviction.
2398            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(2));
2399            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2400                .await
2401                .unwrap();
2402
2403            // Write 3 pages of data and sync to disk.
2404            let synced: Vec<u8> = (0u8..=255)
2405                .cycle()
2406                .take(PAGE_SIZE.get() as usize * 3)
2407                .collect();
2408            (append, _) = append.append(&synced).await.unwrap();
2409            append = append.sync().await.unwrap();
2410
2411            // Write a partial page that stays in the tip buffer. The item_size
2412            // is chosen so the last item straddles the synced/tip boundary.
2413            let item_size = 10;
2414            let tip_len = PAGE_SIZE.get() as usize / 2;
2415            let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
2416            (append, _) = append.append(&tip).await.unwrap();
2417
2418            // Prime pages 0 and 2 into cache, leaving page 1 uncached.
2419            let _ = append.read_at(0, item_size).await.unwrap();
2420            let _ = append
2421                .read_at(PAGE_SIZE.get() as u64 * 2, item_size)
2422                .await
2423                .unwrap();
2424
2425            // Offset that straddles the synced/tip boundary: starts in the last
2426            // synced page, ends in the tip buffer.
2427            let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2428            let tip_off = synced.len() as u64 + item_size as u64;
2429            let offsets = [
2430                0u64,                       // page 0 (cached)
2431                PAGE_SIZE.get() as u64,     // page 1 (not cached - blob I/O)
2432                PAGE_SIZE.get() as u64 * 2, // page 2 (cached)
2433                straddle_off,               // straddles synced/tip boundary
2434                tip_off,                    // entirely in tip buffer
2435            ];
2436            let mut buf = vec![0u8; offsets.len() * item_size];
2437            append
2438                .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2439                .await
2440                .unwrap();
2441
2442            let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2443            for (i, &off) in offsets.iter().enumerate() {
2444                assert_eq!(
2445                    &buf[i * item_size..(i + 1) * item_size],
2446                    &read[off as usize..off as usize + item_size],
2447                );
2448            }
2449        });
2450    }
2451
2452    #[test_traced("DEBUG")]
2453    fn test_read_many_into_straddle_prefix_miss() {
2454        // A straddling item whose synced prefix page is NOT in the page cache: the
2455        // suffix is copied from the tip buffer and the prefix is read from the blob
2456        // without clobbering it, and the item is counted as a blob read.
2457        let executor = deterministic::Runner::default();
2458        executor.start(|context: deterministic::Context| async move {
2459            let (blob, blob_size) = context
2460                .open("test_partition", b"rmany_smiss")
2461                .await
2462                .unwrap();
2463
2464            // Single-page cache so residency is deterministic.
2465            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2466            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2467                .await
2468                .unwrap();
2469
2470            // Write 3 pages and sync, then a partial page that stays in the tip.
2471            let synced: Vec<u8> = (0u8..=255)
2472                .cycle()
2473                .take(PAGE_SIZE.get() as usize * 3)
2474                .collect();
2475            (append, _) = append.append(&synced).await.unwrap();
2476            append = append.sync().await.unwrap();
2477            let item_size = 10;
2478            let tip: Vec<u8> = (100u8..=255)
2479                .cycle()
2480                .take(PAGE_SIZE.get() as usize / 2)
2481                .collect();
2482            (append, _) = append.append(&tip).await.unwrap();
2483
2484            // Fault page 0 in, evicting whatever sync left resident, so the straddle
2485            // prefix page (page 2) is guaranteed not cached.
2486            let _ = append.read_at(0, item_size).await.unwrap();
2487
2488            let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2489            let tip_off = synced.len() as u64 + item_size as u64;
2490            let offsets = [straddle_off, tip_off];
2491            let mut buf = vec![0u8; offsets.len() * item_size];
2492            let hits = append
2493                .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2494                .await
2495                .unwrap();
2496
2497            // The tip-only item is a hit; the straddle item required a blob read.
2498            assert_eq!(hits, 1);
2499            let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2500            for (i, &off) in offsets.iter().enumerate() {
2501                assert_eq!(
2502                    &buf[i * item_size..(i + 1) * item_size],
2503                    &read[off as usize..off as usize + item_size],
2504                );
2505            }
2506        });
2507    }
2508
2509    #[test_traced("DEBUG")]
2510    fn test_append_crc_empty() {
2511        let executor = deterministic::Runner::default();
2512        executor.start(|context: deterministic::Context| async move {
2513            // Open a new blob.
2514            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2515            assert_eq!(blob_size, 0);
2516
2517            // Create a page cache reference.
2518            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2519
2520            // Create a Writer.
2521            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2522                .await
2523                .unwrap();
2524
2525            // Verify initial size is 0.
2526            assert_eq!(append.size(), 0);
2527
2528            // Close & re-open.
2529            append = append.sync().await.unwrap();
2530            drop(append);
2531
2532            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2533            assert_eq!(blob_size, 0); // There was no need to write a crc since there was no data.
2534
2535            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2536                .await
2537                .unwrap();
2538
2539            assert_eq!(append.size(), 0);
2540        });
2541    }
2542
2543    #[test_traced("DEBUG")]
2544    fn test_append_crc_basic() {
2545        let executor = deterministic::Runner::default();
2546        executor.start(|context: deterministic::Context| async move {
2547            // Open a new blob.
2548            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2549            assert_eq!(blob_size, 0);
2550
2551            // Create a page cache reference.
2552            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2553
2554            // Create a Writer.
2555            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2556                .await
2557                .unwrap();
2558
2559            // Verify initial size is 0.
2560            assert_eq!(append.size(), 0);
2561
2562            // Append some bytes.
2563            let data = vec![1, 2, 3, 4, 5];
2564            (append, _) = append.append(&data).await.unwrap();
2565
2566            // Verify size reflects appended data.
2567            assert_eq!(append.size(), 5);
2568
2569            // Append more bytes.
2570            let more_data = vec![6, 7, 8, 9, 10];
2571            (append, _) = append.append(&more_data).await.unwrap();
2572
2573            // Verify size is cumulative.
2574            assert_eq!(append.size(), 10);
2575
2576            // Read back the first chunk and verify.
2577            let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
2578            assert_eq!(read_buf, &data[..]);
2579
2580            // Read back the second chunk and verify.
2581            let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
2582            assert_eq!(read_buf, &more_data[..]);
2583
2584            // Read all data at once and verify.
2585            let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
2586            assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2587
2588            // Close and reopen the blob and make sure the data is still there and the trailing
2589            // checksum is written & stripped as expected.
2590            append = append.sync().await.unwrap();
2591            drop(append);
2592
2593            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2594            // Physical page = 103 logical + 12 Checksum = 115 bytes (padded partial page)
2595            assert_eq!(blob_size, 115);
2596            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2597                .await
2598                .unwrap();
2599            assert_eq!(append.size(), 10); // CRC should be stripped after verification
2600
2601            // Append data that spans a page boundary.
2602            // PAGE_SIZE=103 is the logical page size. We have 10 bytes, so writing
2603            // 100 more bytes (total 110) will cross the page boundary at byte 103.
2604            let spanning_data: Vec<u8> = (11..=110).collect();
2605            (append, _) = append.append(&spanning_data).await.unwrap();
2606            assert_eq!(append.size(), 110);
2607
2608            // Read back data that spans the page boundary.
2609            let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
2610            assert_eq!(read_buf, &spanning_data[..]);
2611
2612            // Read all 110 bytes at once.
2613            let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
2614            let expected: Vec<u8> = (1..=110).collect();
2615            assert_eq!(read_buf, &expected[..]);
2616
2617            // Drop and re-open and make sure bytes are still there.
2618            append = append.sync().await.unwrap();
2619            drop(append);
2620
2621            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2622            // 2 physical pages: 2 * 115 = 230 bytes
2623            assert_eq!(blob_size, 230);
2624            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2625                .await
2626                .unwrap();
2627            assert_eq!(append.size(), 110);
2628
2629            // Append data to reach exactly a page boundary.
2630            // Logical page size is 103. We have 110 bytes, next boundary is 206 (103 * 2).
2631            // So we need 96 more bytes.
2632            let boundary_data: Vec<u8> = (111..=206).collect();
2633            assert_eq!(boundary_data.len(), 96);
2634            (append, _) = append.append(&boundary_data).await.unwrap();
2635            assert_eq!(append.size(), 206);
2636
2637            // Verify we can read it back.
2638            let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2639            let expected: Vec<u8> = (1..=206).collect();
2640            assert_eq!(read_buf, &expected[..]);
2641
2642            // Drop and re-open at the page boundary.
2643            append = append.sync().await.unwrap();
2644            drop(append);
2645
2646            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2647            // Physical size should be exactly 2 pages: 115 * 2 = 230 bytes
2648            assert_eq!(blob_size, 230);
2649            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2650                .await
2651                .unwrap();
2652            assert_eq!(append.size(), 206);
2653
2654            // Verify data is still readable after reopen.
2655            let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2656            assert_eq!(read_buf, &expected[..]);
2657        });
2658    }
2659
2660    #[test_traced("DEBUG")]
2661    fn test_append_owned_bypass_from_empty_tip() {
2662        // A large owned append from an empty, page-aligned tip writes whole pages directly to the
2663        // blob and leaves the partial-page suffix buffered.
2664        let executor = deterministic::Runner::default();
2665        executor.start(|context: deterministic::Context| async move {
2666            let (blob, blob_size) = context
2667                .open("test_partition", b"owned_empty")
2668                .await
2669                .unwrap();
2670            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2671            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2672                .await
2673                .unwrap();
2674
2675            // 500 bytes = 4 full pages (412 bytes) + 88-byte remainder.
2676            let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2677            let src = IoBuf::from(data.clone());
2678            let src_start = src.as_ptr() as usize;
2679            let src_range = src_start..src_start + src.len();
2680            (append, _) = append.append_owned(src.clone()).await.unwrap();
2681            assert_eq!(append.size(), 500);
2682
2683            // The buffered suffix is a copy, not a view that would pin the input allocation.
2684            let tip_ptr = append.buffer.parts().1.as_ptr() as usize;
2685            assert!(!src_range.contains(&tip_ptr));
2686
2687            // The directly written pages populate the page cache, exactly as a buffered flush
2688            // would.
2689            let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
2690            assert_eq!(
2691                append.cache_ref.read_cached(append.id, &mut probe, 0),
2692                PAGE_SIZE.get() as usize
2693            );
2694            assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
2695
2696            // All bytes are readable before any sync (bulk from the cache, suffix from tip).
2697            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2698            assert_eq!(read_buf, &data[..]);
2699
2700            // Everything becomes durable with a single sync.
2701            append = append.sync().await.unwrap();
2702            drop(append);
2703
2704            let (blob, blob_size) = context
2705                .open("test_partition", b"owned_empty")
2706                .await
2707                .unwrap();
2708            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2709                .await
2710                .unwrap();
2711            assert_eq!(append.size(), 500);
2712            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2713            assert_eq!(read_buf, &data[..]);
2714        });
2715    }
2716
2717    #[test_traced("DEBUG")]
2718    fn test_append_owned_bypass_with_synced_partial_page() {
2719        // A large owned append on top of a synced partial page must rewrite the first page in
2720        // full (preserving the old CRC slot) before writing the bulk directly.
2721        let executor = deterministic::Runner::default();
2722        executor.start(|context: deterministic::Context| async move {
2723            let (blob, blob_size) = context
2724                .open("test_partition", b"owned_partial")
2725                .await
2726                .unwrap();
2727            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2728            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2729                .await
2730                .unwrap();
2731
2732            // Durably write a 50-byte partial page.
2733            let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
2734            (append, _) = append.append(&all[..50]).await.unwrap();
2735            append = append.sync().await.unwrap();
2736
2737            // 450 more bytes: 53 fill the first page (rewritten in full), 3 whole pages (309 bytes)
2738            // bypass the buffer, 88 remain in the tip.
2739            (append, _) = append
2740                .append_owned(IoBuf::from(all[50..].to_vec()))
2741                .await
2742                .unwrap();
2743            assert_eq!(append.size(), 500);
2744            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2745            assert_eq!(read_buf, &all[..]);
2746
2747            // The direct write is not durable until sync: dropping without one preserves only the
2748            // synced 50-byte prefix.
2749            drop(append);
2750            let (blob, blob_size) = context
2751                .open("test_partition", b"owned_partial")
2752                .await
2753                .unwrap();
2754            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2755                .await
2756                .unwrap();
2757            assert_eq!(append.size(), 50);
2758            let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
2759            assert_eq!(read_buf, &all[..50]);
2760
2761            // Repeating the owned append after recovery and syncing makes everything durable,
2762            // exercising the full rewrite of the recovered partial page.
2763            (append, _) = append
2764                .append_owned(IoBuf::from(all[50..].to_vec()))
2765                .await
2766                .unwrap();
2767            append = append.sync().await.unwrap();
2768            drop(append);
2769
2770            let (blob, blob_size) = context
2771                .open("test_partition", b"owned_partial")
2772                .await
2773                .unwrap();
2774            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2775                .await
2776                .unwrap();
2777            assert_eq!(append.size(), 500);
2778            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2779            assert_eq!(read_buf, &all[..]);
2780        });
2781    }
2782
2783    #[test_traced("DEBUG")]
2784    fn test_append_owned_bypass_with_buffered_tip() {
2785        // A large owned append merges with unsynced buffered bytes: the fill completes the
2786        // current page, the bulk bypasses the buffer, and everything is readable.
2787        let executor = deterministic::Runner::default();
2788        executor.start(|context: deterministic::Context| async move {
2789            let (blob, blob_size) = context
2790                .open("test_partition", b"owned_buffered")
2791                .await
2792                .unwrap();
2793            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2794            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2795                .await
2796                .unwrap();
2797
2798            let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
2799            (append, _) = append.append(&all[..30]).await.unwrap();
2800            (append, _) = append
2801                .append_owned(IoBuf::from(all[30..].to_vec()))
2802                .await
2803                .unwrap();
2804            assert_eq!(append.size(), 430);
2805            let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2806            assert_eq!(read_buf, &all[..]);
2807
2808            append = append.sync().await.unwrap();
2809            drop(append);
2810
2811            let (blob, blob_size) = context
2812                .open("test_partition", b"owned_buffered")
2813                .await
2814                .unwrap();
2815            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2816                .await
2817                .unwrap();
2818            assert_eq!(append.size(), 430);
2819            let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2820            assert_eq!(read_buf, &all[..]);
2821        });
2822    }
2823
2824    #[test_traced("DEBUG")]
2825    fn test_append_owned_exact_page_multiple_and_small() {
2826        // An owned append of an exact page multiple leaves an empty tip that later buffered and
2827        // small owned appends continue from; small owned appends use the buffered path.
2828        let executor = deterministic::Runner::default();
2829        executor.start(|context: deterministic::Context| async move {
2830            let (blob, blob_size) = context
2831                .open("test_partition", b"owned_exact")
2832                .await
2833                .unwrap();
2834            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2835            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2836                .await
2837                .unwrap();
2838
2839            // Exactly 4 pages: no remainder.
2840            let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
2841            (append, _) = append
2842                .append_owned(IoBuf::from(bulk.clone()))
2843                .await
2844                .unwrap();
2845            assert_eq!(append.size(), 412);
2846
2847            // A small owned append takes the buffered path.
2848            let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
2849            (append, _) = append
2850                .append_owned(IoBuf::from(small.clone()))
2851                .await
2852                .unwrap();
2853            assert_eq!(append.size(), 422);
2854
2855            let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
2856            assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
2857            assert_eq!(&read_buf.as_ref()[412..], &small[..]);
2858
2859            append = append.sync().await.unwrap();
2860            drop(append);
2861
2862            let (blob, blob_size) = context
2863                .open("test_partition", b"owned_exact")
2864                .await
2865                .unwrap();
2866            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2867                .await
2868                .unwrap();
2869            assert_eq!(append.size(), 422);
2870        });
2871    }
2872
2873    #[test_traced("DEBUG")]
2874    fn test_append_owned_physical_bytes_match_buffered() {
2875        // The direct path must produce byte-identical physical output (page layout, CRC slot
2876        // placement, zero padding) to the buffered path for the same logical content.
2877        let executor = deterministic::Runner::default();
2878        executor.start(|context: deterministic::Context| async move {
2879            let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2880            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2881
2882            let (blob, size) = context
2883                .open("test_partition", b"phys_direct")
2884                .await
2885                .unwrap();
2886            let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
2887                .await
2888                .unwrap();
2889            (direct, _) = direct
2890                .append_owned(IoBuf::from(data.clone()))
2891                .await
2892                .unwrap();
2893            direct = direct.sync().await.unwrap();
2894            drop(direct);
2895
2896            // Small appends always stay on the buffered path and force intermediate flushes.
2897            let (blob, size) = context
2898                .open("test_partition", b"phys_buffered")
2899                .await
2900                .unwrap();
2901            let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2902                .await
2903                .unwrap();
2904            for chunk in data.chunks(10) {
2905                (buffered, _) = buffered.append(chunk).await.unwrap();
2906            }
2907            buffered = buffered.sync().await.unwrap();
2908            drop(buffered);
2909
2910            let (blob_a, size_a) = context
2911                .open("test_partition", b"phys_direct")
2912                .await
2913                .unwrap();
2914            let (blob_b, size_b) = context
2915                .open("test_partition", b"phys_buffered")
2916                .await
2917                .unwrap();
2918            assert_eq!(size_a, size_b);
2919            let bytes_a = blob_a
2920                .read_at(0, size_a as usize, ReadOptions::default())
2921                .await
2922                .unwrap()
2923                .coalesce();
2924            let bytes_b = blob_b
2925                .read_at(0, size_b as usize, ReadOptions::default())
2926                .await
2927                .unwrap()
2928                .coalesce();
2929            assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
2930        });
2931    }
2932
2933    #[test_traced("DEBUG")]
2934    fn test_append_borrowed_large_takes_direct_path() {
2935        // A plain `append` larger than the write buffer is routed through the direct path, so the
2936        // write buffer holds only the partial-page suffix afterwards instead of the whole input.
2937        let executor = deterministic::Runner::default();
2938        executor.start(|context: deterministic::Context| async move {
2939            let (blob, blob_size) = context
2940                .open("test_partition", b"borrowed_large")
2941                .await
2942                .unwrap();
2943            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2944            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2945                .await
2946                .unwrap();
2947
2948            // Start misaligned with a small buffered prefix.
2949            let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
2950            (append, _) = append.append(&all[..30]).await.unwrap();
2951
2952            // 500 more bytes exceed the 206-byte write buffer and take the direct path.
2953            (append, _) = append.append(&all[30..]).await.unwrap();
2954            assert_eq!(append.size(), 530);
2955
2956            // Only the partial-page suffix remains buffered (530 = 5 full pages + 15 bytes).
2957            assert_eq!(append.buffer.len(), 15);
2958
2959            let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2960            assert_eq!(read_buf, &all[..]);
2961
2962            append = append.sync().await.unwrap();
2963            drop(append);
2964
2965            let (blob, blob_size) = context
2966                .open("test_partition", b"borrowed_large")
2967                .await
2968                .unwrap();
2969            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2970                .await
2971                .unwrap();
2972            assert_eq!(append.size(), 530);
2973            let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2974            assert_eq!(read_buf, &all[..]);
2975        });
2976    }
2977
2978    #[test]
2979    fn test_sync_preserves_partial_tip_allocation() {
2980        const PAGE_SIZE: NonZeroU16 = NZU16!(4096);
2981        const BUFFER_SIZE: usize = 8192;
2982        deterministic::Runner::default().start(|context| async move {
2983            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2984            let (blob, size) = context
2985                .open("test_partition", b"partial_seal")
2986                .await
2987                .unwrap();
2988            let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
2989            (writer, _) = writer.append(&[7; 16]).await.unwrap();
2990            let ptr = writer.buffer.parts().1.as_ptr();
2991
2992            // Active partial-page syncs preserve the reusable write allocation.
2993            writer = writer.sync().await.unwrap();
2994            assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
2995            (writer, _) = writer.append(&[7; 16]).await.unwrap();
2996            assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
2997
2998            let (sealed, sync) = writer.seal().await.unwrap();
2999            assert_eq!(
3000                sealed.read_at(0, 32).await.unwrap().coalesce().as_ref(),
3001                &[7; 32]
3002            );
3003            sync.await.unwrap();
3004        });
3005    }
3006
3007    #[test]
3008    fn test_recovery_sync_preserves_truncate_and_append() {
3009        deterministic::Runner::default().start(|context| async move {
3010            let page_size = NZU16!(64);
3011            let cache = CacheRef::new(page_size, NZUsize!(4));
3012            let (blob, size) = context
3013                .open("test_partition", b"compact_recovery")
3014                .await
3015                .unwrap();
3016            let mut recovery = Recovery::open(blob, size, 256, cache.clone())
3017                .await
3018                .unwrap();
3019            let mut expected: Vec<u8> = (0..129).collect();
3020            (recovery, _) = recovery.append(&expected).await.unwrap();
3021            recovery = recovery.sync().await.unwrap();
3022            assert_eq!(recovery.buffer.len(), 1);
3023            assert_eq!(recovery.buffer.capacity, 256);
3024
3025            // The earlier partial page is larger than the compacted tail.
3026            recovery = recovery.truncate(95).await.unwrap();
3027            expected.truncate(95);
3028            let suffix = [7; 80];
3029            (recovery, _) = recovery.append(&suffix).await.unwrap();
3030            expected.extend_from_slice(&suffix);
3031            recovery = recovery.sync().await.unwrap();
3032            assert_eq!(recovery.buffer.capacity, 256);
3033            drop(recovery);
3034
3035            let (blob, size) = context
3036                .open("test_partition", b"compact_recovery")
3037                .await
3038                .unwrap();
3039            let writer = Writer::new(blob, size, 256, cache).await.unwrap();
3040            assert_eq!(writer.size(), expected.len() as u64);
3041            assert_eq!(
3042                writer
3043                    .read_at(0, expected.len())
3044                    .await
3045                    .unwrap()
3046                    .coalesce()
3047                    .as_ref(),
3048                expected
3049            );
3050        });
3051    }
3052
3053    #[test_traced("DEBUG")]
3054    fn test_sync_uses_range_sync_for_single_flush() {
3055        let executor = deterministic::Runner::default();
3056        executor.start(|_: deterministic::Context| async move {
3057            let blob = SyncTrackingBlob::new();
3058            let blob = Arc::new(blob);
3059            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3060            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3061                .await
3062                .unwrap();
3063
3064            // A newly wrapped blob preserves one full barrier before range sync is used.
3065            append = append.sync().await.unwrap();
3066            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3067            assert_eq!(writes, 0);
3068            assert_eq!(full_syncs, 1);
3069            assert_eq!(range_syncs, 0);
3070
3071            // A single buffered write with no remaining dirty state can be made durable directly.
3072            let data = b"hello world";
3073            (append, _) = append.append(data).await.unwrap();
3074            append = append.sync().await.unwrap();
3075
3076            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3077            assert_eq!(writes, 1);
3078            assert_eq!(full_syncs, 1);
3079            assert_eq!(range_syncs, 1);
3080
3081            // With no new writes and no pending full-sync barrier, sync has no work left.
3082            append.sync().await.unwrap();
3083            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3084            assert_eq!(writes, 1);
3085            assert_eq!(full_syncs, 1);
3086            assert_eq!(range_syncs, 1);
3087
3088            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3089            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3090                .await
3091                .unwrap();
3092            let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
3093            assert_eq!(read.as_ref(), data);
3094        });
3095    }
3096
3097    /// Observing a completed started sync lets the next write use range sync.
3098    #[test_traced("DEBUG")]
3099    fn test_start_sync_persists_and_marks_clean() {
3100        let executor = deterministic::Runner::default();
3101        executor.start(|_: deterministic::Context| async move {
3102            let blob = SyncTrackingBlob::new();
3103            let blob = Arc::new(blob);
3104            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3105            let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3106                .await
3107                .unwrap();
3108
3109            // Complete the fresh writer's full sync with no buffered bytes to write.
3110            let (mut writer, handle) = writer.start_sync().await.unwrap();
3111            handle.await.unwrap();
3112            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3113            assert_eq!(writes, 0);
3114            assert_eq!(full_syncs, 1);
3115            assert_eq!(range_syncs, 0);
3116
3117            // The next mutation observes the completed sync and can sync just its new range.
3118            let data = b"hello world";
3119            (writer, _) = writer.append(data).await.unwrap();
3120            writer = writer.sync().await.unwrap();
3121            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3122            assert_eq!(writes, 1);
3123            assert_eq!(full_syncs, 1);
3124            assert_eq!(range_syncs, 1);
3125
3126            // Nothing left to sync, so start_sync does nothing.
3127            let (_, handle) = writer.start_sync().await.unwrap();
3128            handle.await.unwrap();
3129            let (_, _, full_syncs, range_syncs) = blob.snapshot();
3130            assert_eq!(full_syncs, 1);
3131            assert_eq!(range_syncs, 1);
3132
3133            // Durable and readable after reopening.
3134            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3135            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3136                .await
3137                .unwrap();
3138            let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
3139            assert_eq!(read.as_ref(), data);
3140        });
3141    }
3142
3143    /// A failed flush inside `start_sync` returns an error.
3144    #[test_traced("DEBUG")]
3145    fn test_start_sync_flush_failure_returns_err() {
3146        let executor = deterministic::Runner::default();
3147        executor.start(|context: deterministic::Context| async move {
3148            let faults = WriteFaults::default();
3149            let context = WriteFaultContext {
3150                inner: context,
3151                faults: faults.clone(),
3152            };
3153            let (blob, size) = context
3154                .open("test_partition", b"start_sync_flush_failure")
3155                .await
3156                .unwrap();
3157            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3158            let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3159                .await
3160                .unwrap();
3161
3162            // Make a partial page durable, then extend it so the next flush rewrites the page.
3163            (writer, _) = writer.append(&[1u8; 24]).await.unwrap();
3164            writer = writer.sync().await.unwrap();
3165            (writer, _) = writer.append(&[2u8; 8]).await.unwrap();
3166
3167            // The rewrite inside start_sync fails.
3168            faults.arm();
3169            assert!(writer.start_sync().await.is_err());
3170        });
3171    }
3172
3173    /// `needs_sync` reports whether `sync` has work: buffered pages, unsynced mutations, or a
3174    /// started sync that the writer has not observed.
3175    #[test_traced("DEBUG")]
3176    fn test_needs_sync_tracks_sync_work() {
3177        let executor = deterministic::Runner::default();
3178        executor.start(|context: deterministic::Context| async move {
3179            let (blob, size) = context.open("test_partition", b"needs_sync").await.unwrap();
3180            let (blob, pending) = DelayedSyncBlob::new(blob);
3181            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3182            let writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3183                .await
3184                .unwrap();
3185
3186            // A fresh writer starts dirty. Its tail page must be durable before appends, but
3187            // earlier full pages may still hold unsynced writes, so the first sync is a full-blob
3188            // barrier.
3189            assert!(writer.needs_sync());
3190            let mut writer = writer.sync().await.unwrap();
3191            assert!(!writer.needs_sync());
3192
3193            // A buffered partial page is flush work even though the blob is clean.
3194            (writer, _) = writer.append(&[1u8; 20]).await.unwrap();
3195            assert!(writer.needs_sync());
3196            writer = writer.sync().await.unwrap();
3197            assert!(!writer.needs_sync());
3198
3199            // A started sync needs observation while it is in flight and after it completes.
3200            (writer, _) = writer.append(&[2u8; 4]).await.unwrap();
3201            let (writer, handle) = writer.start_sync().await.unwrap();
3202            assert!(writer.needs_sync());
3203            next_pending_sync(&pending).release.send(Ok(())).unwrap();
3204            handle.await.unwrap();
3205            assert!(writer.needs_sync());
3206            let mut writer = writer.wait_for_sync().await.unwrap();
3207            assert!(!writer.needs_sync());
3208
3209            // A failed started sync keeps the writer in need of a sync. The handle reports the
3210            // failure, and so does the next sync.
3211            (writer, _) = writer.append(&[3u8; 8]).await.unwrap();
3212            let (writer, handle) = writer.start_sync().await.unwrap();
3213            next_pending_sync(&pending)
3214                .release
3215                .send(Err(Error::Closed))
3216                .unwrap();
3217            assert!(handle.await.is_err());
3218            assert!(writer.needs_sync());
3219            assert!(writer.sync().await.is_err());
3220        });
3221    }
3222
3223    /// Verifies sync waits for a pending start_sync with no new writes.
3224    #[test_traced("DEBUG")]
3225    fn test_sync_waits_for_outstanding_start_sync() {
3226        let executor = deterministic::Runner::default();
3227        executor.start(|_: deterministic::Context| async move {
3228            let inner = SyncTrackingBlob::new();
3229            let inner = Arc::new(inner);
3230            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3231            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3232            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3233
3234            let (writer, handle) = writer.start_sync().await.unwrap();
3235            let deferred = next_pending_sync(&pending);
3236
3237            // Try to sync while the started sync is still blocked.
3238            let mut sync = Box::pin(writer.sync());
3239            assert!(
3240                sync.as_mut().now_or_never().is_none(),
3241                "sync must wait for the outstanding start_sync handle"
3242            );
3243            let (_, _, full_syncs, range_syncs) = inner.snapshot();
3244            assert_eq!(full_syncs, 0);
3245            assert_eq!(range_syncs, 0);
3246
3247            // Release the started sync and finish the same sync.
3248            deferred.release.send(Ok(())).unwrap();
3249            sync.await.unwrap();
3250            handle.await.unwrap();
3251            let (_, _, full_syncs, range_syncs) = inner.snapshot();
3252            assert_eq!(full_syncs, 1);
3253            assert_eq!(range_syncs, 0);
3254        });
3255    }
3256
3257    /// Verifies a small buffered write cannot range-sync before pending start_sync finishes.
3258    #[test_traced("DEBUG")]
3259    fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
3260        let executor = deterministic::Runner::default();
3261        executor.start(|_: deterministic::Context| async move {
3262            let inner = SyncTrackingBlob::new();
3263            let inner = Arc::new(inner);
3264            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3265            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3266            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3267
3268            let (writer, handle) = writer.start_sync().await.unwrap();
3269            let deferred = next_pending_sync(&pending);
3270            let (writer, _) = writer.append(b"hello world").await.unwrap();
3271
3272            // Sync must wait before flushing the buffered write.
3273            let mut sync = Box::pin(writer.sync());
3274            assert!(
3275                sync.as_mut().now_or_never().is_none(),
3276                "sync must join the outstanding barrier before flushing the small write"
3277            );
3278            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3279            assert_eq!(writes, 0);
3280            assert_eq!(full_syncs, 0);
3281            assert_eq!(range_syncs, 0);
3282
3283            // Release the started sync, then let the same sync flush the buffered write.
3284            deferred.release.send(Ok(())).unwrap();
3285            sync.await.unwrap();
3286            handle.await.unwrap();
3287            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3288            assert_eq!(writes, 1);
3289            assert_eq!(full_syncs, 1);
3290            assert_eq!(range_syncs, 1);
3291        });
3292    }
3293
3294    /// Verifies a large append cannot flush before pending start_sync finishes.
3295    #[test_traced("DEBUG")]
3296    fn test_write_flush_waits_for_outstanding_start_sync() {
3297        let executor = deterministic::Runner::default();
3298        executor.start(|context: deterministic::Context| async move {
3299            let inner = SyncTrackingBlob::new();
3300            let inner = Arc::new(inner);
3301            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3302            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3303            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3304
3305            let (mut writer, handle) = writer.start_sync().await.unwrap();
3306            let deferred = next_pending_sync(&pending);
3307
3308            let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
3309            let append = context.child("append").spawn(move |_| async move {
3310                (writer, _) = writer.append(&data).await.unwrap();
3311                writer
3312            });
3313
3314            // The append has reached the pending sync wait.
3315            deferred
3316                .blocked
3317                .await
3318                .expect("append never waited on start_sync");
3319            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3320            assert_eq!(writes, 0);
3321            assert_eq!(full_syncs, 0);
3322            assert_eq!(range_syncs, 0);
3323
3324            // Release the started sync so the append can flush.
3325            deferred.release.send(Ok(())).unwrap();
3326            let writer = append.await.unwrap();
3327            handle.await.unwrap();
3328            writer.sync().await.unwrap();
3329            let (_, writes, full_syncs, _) = inner.snapshot();
3330            assert!(writes > 0);
3331            assert!(full_syncs > 0);
3332        });
3333    }
3334
3335    /// Verifies seal cannot flush buffered bytes before pending start_sync finishes.
3336    #[test_traced("DEBUG")]
3337    fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
3338        let executor = deterministic::Runner::default();
3339        executor.start(|context: deterministic::Context| async move {
3340            let inner = SyncTrackingBlob::new();
3341            let inner = Arc::new(inner);
3342            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3343            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3344            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3345
3346            let (mut writer, prior) = writer.start_sync().await.unwrap();
3347            let deferred = next_pending_sync(&pending);
3348            (writer, _) = writer.append(b"hello world").await.unwrap();
3349
3350            let seal = context
3351                .child("seal")
3352                .spawn(move |_| async move { writer.seal().await });
3353
3354            // The seal has reached the pending sync wait.
3355            deferred
3356                .blocked
3357                .await
3358                .expect("seal never waited on start_sync");
3359            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3360            assert_eq!(writes, 0);
3361            assert_eq!(full_syncs, 0);
3362            assert_eq!(range_syncs, 0);
3363
3364            // Release the started sync so seal can flush.
3365            deferred.release.send(Ok(())).unwrap();
3366            let (sealed, sync) = seal.await.unwrap().unwrap();
3367            prior.await.unwrap();
3368
3369            // Release the sync started by seal itself.
3370            let deferred = next_pending_sync(&pending);
3371            deferred.release.send(Ok(())).unwrap();
3372            sync.await.unwrap();
3373            let read = sealed
3374                .read_at(0, b"hello world".len())
3375                .await
3376                .unwrap()
3377                .coalesce();
3378            assert_eq!(read.as_ref(), b"hello world");
3379            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3380            assert_eq!(writes, 1);
3381            assert_eq!(full_syncs, 2);
3382            assert_eq!(range_syncs, 0);
3383        });
3384    }
3385
3386    /// Verifies snapshot cannot flush buffered bytes before pending start_sync finishes.
3387    #[test_traced("DEBUG")]
3388    fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
3389        let executor = deterministic::Runner::default();
3390        executor.start(|context: deterministic::Context| async move {
3391            let inner = SyncTrackingBlob::new();
3392            let inner = Arc::new(inner);
3393            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3394            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3395            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3396
3397            // Start a sync, then buffer a full page and a partial page not covered by it.
3398            let data = [7u8; PAGE_SIZE.get() as usize + 47];
3399            let (mut writer, prior) = writer.start_sync().await.unwrap();
3400            let deferred = next_pending_sync(&pending);
3401            (writer, _) = writer.append(&data).await.unwrap();
3402
3403            let snapshot = context.child("snapshot").spawn(move |_| async move {
3404                let (writer, snapshot) = writer.snapshot().await.unwrap();
3405                let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
3406                assert_eq!(read.as_ref(), data.as_slice());
3407                writer
3408            });
3409
3410            // Snapshot must wait before flushing the full page.
3411            deferred
3412                .blocked
3413                .await
3414                .expect("snapshot never waited on start_sync");
3415            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3416            assert_eq!(writes, 0);
3417            assert_eq!(full_syncs, 0);
3418            assert_eq!(range_syncs, 0);
3419
3420            // Releasing the sync lets snapshot flush the full page and read the buffered bytes.
3421            deferred.release.send(Ok(())).unwrap();
3422            let _writer = snapshot.await.unwrap();
3423            prior.await.unwrap();
3424            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3425            assert_eq!(writes, 1);
3426            assert_eq!(full_syncs, 1);
3427            assert_eq!(range_syncs, 0);
3428        });
3429    }
3430
3431    /// A snapshot copies the partial page instead of writing it, and later appends and syncs
3432    /// leave its view unchanged.
3433    #[test_traced("DEBUG")]
3434    fn test_snapshot_copies_partial_page_without_writing() {
3435        let executor = deterministic::Runner::default();
3436        executor.start(|_: deterministic::Context| async move {
3437            let blob = Arc::new(SyncTrackingBlob::new());
3438            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3439            let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3440                .await
3441                .unwrap();
3442
3443            // Snapshot a partial page without writing it.
3444            let (writer, _) = writer.append(b"hello").await.unwrap();
3445            let (writer, snapshot) = writer.snapshot().await.unwrap();
3446            let (_, writes, _, _) = blob.snapshot();
3447            assert_eq!(writes, 0);
3448
3449            // Appending and syncing later bytes writes the page without changing the view.
3450            let (writer, _) = writer.append(b" world").await.unwrap();
3451            let _writer = writer.sync().await.unwrap();
3452            let (_, writes, _, _) = blob.snapshot();
3453            assert_eq!(writes, 1);
3454            assert_eq!(snapshot.size(), 5);
3455            let read = snapshot.read_at(0, 5).await.unwrap().coalesce();
3456            assert_eq!(read.as_ref(), b"hello");
3457        });
3458    }
3459
3460    /// A view without I/O is available only while every full page is already written.
3461    #[test_traced]
3462    fn test_try_snapshot_requires_written_full_pages() {
3463        let executor = deterministic::Runner::default();
3464        executor.start(|_: deterministic::Context| async move {
3465            let blob = Arc::new(SyncTrackingBlob::new());
3466            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3467            let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3468                .await
3469                .unwrap();
3470
3471            // A partial page alone is copied into the view without a write.
3472            let (writer, _) = writer.append(b"hello").await.unwrap();
3473            let view = writer.try_snapshot().expect("partial page needs no write");
3474            assert_eq!(view.size(), 5);
3475            let read = view.read_at(0, 5).await.unwrap().coalesce();
3476            assert_eq!(read.as_ref(), b"hello");
3477            let (_, writes, _, _) = blob.snapshot();
3478            assert_eq!(writes, 0);
3479
3480            // A buffered full page must be written before a view can be taken.
3481            let page = vec![b'x'; PAGE_SIZE.get() as usize];
3482            let (writer, _) = writer.append(&page).await.unwrap();
3483            assert!(writer.try_snapshot().is_none());
3484
3485            // Once written, the view covers every byte and replays the full page from the blob.
3486            let (writer, snapshot) = writer.snapshot().await.unwrap();
3487            let (_, writes, _, _) = blob.snapshot();
3488            assert_eq!(writes, 1);
3489            let view = writer.try_snapshot().expect("full pages are written");
3490            assert_eq!(view.size(), snapshot.size());
3491            let mut replay = view
3492                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
3493                .unwrap();
3494            let total = 5 + page.len();
3495            assert!(replay.ensure(total).await.unwrap());
3496            let bytes = replay.copy_to_bytes(total);
3497            assert_eq!(&bytes[..5], b"hello");
3498            assert!(bytes[5..].iter().all(|byte| *byte == b'x'));
3499            assert!(!replay.ensure(1).await.unwrap());
3500        });
3501    }
3502
3503    /// Verifies replay cannot flush buffered bytes before pending start_sync finishes.
3504    #[test_traced("DEBUG")]
3505    fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
3506        let executor = deterministic::Runner::default();
3507        executor.start(|context: deterministic::Context| async move {
3508            let inner = SyncTrackingBlob::new();
3509            let inner = Arc::new(inner);
3510            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3511            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3512            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3513
3514            // Start a sync, then buffer newer bytes not covered by it.
3515            let (mut writer, prior) = writer.start_sync().await.unwrap();
3516            let deferred = next_pending_sync(&pending);
3517            (writer, _) = writer.append(b"hello world").await.unwrap();
3518
3519            let replay = context.child("replay").spawn(move |_| async move {
3520                let (writer, mut replay) = writer
3521                    .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
3522                    .await
3523                    .unwrap();
3524                assert!(replay.ensure(1).await.unwrap());
3525                assert_eq!(replay.chunk()[0], b'h');
3526                writer
3527            });
3528
3529            // Replay must wait before flushing buffered bytes.
3530            deferred
3531                .blocked
3532                .await
3533                .expect("replay never waited on start_sync");
3534            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3535            assert_eq!(writes, 0);
3536            assert_eq!(full_syncs, 0);
3537            assert_eq!(range_syncs, 0);
3538
3539            // Releasing the sync lets replay flush and read the buffered bytes.
3540            deferred.release.send(Ok(())).unwrap();
3541            let _writer = replay.await.unwrap();
3542            prior.await.unwrap();
3543            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3544            assert_eq!(writes, 1);
3545            assert_eq!(full_syncs, 1);
3546            assert_eq!(range_syncs, 0);
3547        });
3548    }
3549
3550    #[test_traced("DEBUG")]
3551    fn test_append_waits_for_outstanding_start_sync_before_writing() {
3552        let executor = deterministic::Runner::default();
3553        executor.start(|context: deterministic::Context| async move {
3554            let inner = SyncTrackingBlob::new();
3555            let inner = Arc::new(inner);
3556            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3557            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3558            let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3559
3560            // Start a sync before growing into the direct-write path.
3561            let (mut writer, prior) = writer.start_sync().await.unwrap();
3562            let deferred = next_pending_sync(&pending);
3563
3564            let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
3565            let append = context.child("append").spawn(move |_| async move {
3566                (writer, _) = writer.append(&vec![0; target_size as usize]).await.unwrap();
3567                writer
3568            });
3569
3570            // Growth must wait before writing zero-filled pages.
3571            deferred
3572                .blocked
3573                .await
3574                .expect("append never waited on start_sync");
3575            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3576            assert_eq!(writes, 0);
3577            assert_eq!(full_syncs, 0);
3578            assert_eq!(range_syncs, 0);
3579
3580            // Releasing the sync lets the append complete.
3581            deferred.release.send(Ok(())).unwrap();
3582            let writer = append.await.unwrap();
3583            prior.await.unwrap();
3584            assert_eq!(writer.size(), target_size);
3585            writer.sync().await.unwrap();
3586            let (_, writes, full_syncs, _) = inner.snapshot();
3587            assert!(writes > 0);
3588            assert!(full_syncs > 0);
3589        });
3590    }
3591
3592    // Recovery cannot resize the blob before a pending start_sync finishes.
3593    #[test_traced("DEBUG")]
3594    fn test_recovery_truncate_shrink_waits_for_outstanding_start_sync_before_resizing() {
3595        let executor = deterministic::Runner::default();
3596        executor.start(|context: deterministic::Context| async move {
3597            let inner = SyncTrackingBlob::new();
3598            let inner = Arc::new(inner);
3599            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3600            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3601            let mut writer = Recovery::open(blob, 0, BUFFER_SIZE, cache_ref)
3602                .await
3603                .unwrap();
3604
3605            // Build durable pages, then start a sync for a newer partial page.
3606            let data = vec![3; PAGE_SIZE.get() as usize * 2];
3607            (writer, _) = writer.append(&data).await.unwrap();
3608            writer = writer.sync().await.unwrap();
3609            (writer, _) = writer.append(b"x").await.unwrap();
3610            let (mut writer, prior) = writer.start_sync().await.unwrap();
3611            let deferred = next_pending_sync(&pending);
3612            let physical_size = inner.size();
3613
3614            let resize = context.child("resize_shrink").spawn(move |_| async move {
3615                writer = writer.truncate(PAGE_SIZE.get() as u64).await.unwrap();
3616                writer
3617            });
3618
3619            // Shrink must wait before truncating the physical blob.
3620            deferred
3621                .blocked
3622                .await
3623                .expect("resize shrink never waited on start_sync");
3624            assert_eq!(
3625                inner.size(),
3626                physical_size,
3627                "resize must not shrink the blob before the pending sync finishes"
3628            );
3629
3630            // Releasing the sync lets the shrink truncate the blob.
3631            deferred.release.send(Ok(())).unwrap();
3632            let writer = resize.await.unwrap();
3633            prior.await.unwrap();
3634            assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
3635            assert!(inner.size() < physical_size);
3636        });
3637    }
3638
3639    #[test_traced("DEBUG")]
3640    fn test_sync_uses_full_sync_after_prior_plain_flush() {
3641        let executor = deterministic::Runner::default();
3642        executor.start(|_: deterministic::Context| async move {
3643            let blob = SyncTrackingBlob::new();
3644            let blob = Arc::new(blob);
3645            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3646            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3647                .await
3648                .unwrap();
3649
3650            // This append overflows the buffer, so a plain flush happens before sync writes the
3651            // remaining tip.
3652            let data = vec![7u8; BUFFER_SIZE + 1];
3653            (append, _) = append.append(&data).await.unwrap();
3654            append = append.sync().await.unwrap();
3655
3656            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3657            assert_eq!(writes, 2);
3658            assert_eq!(full_syncs, 1);
3659            assert_eq!(range_syncs, 0);
3660
3661            // With no new work, sync should not issue another durability operation.
3662            append = append.sync().await.unwrap();
3663            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3664            assert_eq!(writes, 2);
3665            assert_eq!(full_syncs, 1);
3666            assert_eq!(range_syncs, 0);
3667
3668            // With the earlier flush already durable, extending the partial page is one
3669            // full-page rewrite fused with a range sync.
3670            (append, _) = append.append(b"tip").await.unwrap();
3671            append.sync().await.unwrap();
3672
3673            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3674            assert_eq!(writes, 3);
3675            assert_eq!(full_syncs, 1);
3676            assert_eq!(range_syncs, 1);
3677
3678            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3679            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3680                .await
3681                .unwrap();
3682            let mut expected = data;
3683            expected.extend_from_slice(b"tip");
3684            let read = reopened
3685                .read_at(0, expected.len())
3686                .await
3687                .unwrap()
3688                .coalesce();
3689            assert_eq!(read.as_ref(), expected.as_slice());
3690        });
3691    }
3692
3693    #[test_traced("DEBUG")]
3694    fn test_sync_uses_full_sync_after_replay_plain_flush() {
3695        let executor = deterministic::Runner::default();
3696        executor.start(|_: deterministic::Context| async move {
3697            let blob = SyncTrackingBlob::new();
3698            let blob = Arc::new(blob);
3699            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3700            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3701                .await
3702                .unwrap();
3703
3704            // Keep data buffered so replay has to flush it without syncing.
3705            (append, _) = append.append(b"replayed").await.unwrap();
3706
3707            // Replay flushes buffered data for reading, but does not make that write durable.
3708            let (append, mut replay) = append
3709                .replay(NZUsize!(1024), ReadOptions::default())
3710                .await
3711                .unwrap();
3712            assert!(replay.ensure(b"replayed".len()).await.unwrap());
3713            assert_eq!(replay.remaining(), b"replayed".len());
3714            assert_eq!(replay.chunk(), b"replayed");
3715
3716            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3717            assert_eq!(writes, 1);
3718            assert_eq!(full_syncs, 0);
3719            assert_eq!(range_syncs, 0);
3720
3721            // A later sync must use a full barrier for the plain replay flush.
3722            append.sync().await.unwrap();
3723            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3724            assert_eq!(writes, 1);
3725            assert_eq!(full_syncs, 1);
3726            assert_eq!(range_syncs, 0);
3727        });
3728    }
3729
3730    #[test_traced("DEBUG")]
3731    fn test_replay_uses_read_options_for_refills_and_seek() {
3732        let executor = deterministic::Runner::default();
3733        executor.start(|context: deterministic::Context| async move {
3734            let (blob, size) = context
3735                .open("test_partition", b"replay_read_options")
3736                .await
3737                .unwrap();
3738            let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
3739            let read_options = blob.read_options();
3740            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3741            let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3742                .await
3743                .unwrap();
3744
3745            let page_size = PAGE_SIZE.get() as usize;
3746            let data = vec![3; page_size * 2];
3747            (writer, _) = writer.append(&data).await.unwrap();
3748            writer = writer.sync().await.unwrap();
3749            read_options.lock().clear();
3750
3751            let physical_page_size = page_size + CHECKSUM_SIZE as usize;
3752            let (_, mut replay) = writer
3753                .replay(NZUsize!(physical_page_size), ReadOptions::DONT_CACHE)
3754                .await
3755                .unwrap();
3756
3757            // The first fill reads page 0 and starts page 1 in flight, both with the replay's
3758            // read options, so crossing the page boundary needs no further read.
3759            assert!(replay.ensure(1).await.unwrap());
3760            replay.advance(page_size);
3761            assert!(replay.ensure(1).await.unwrap());
3762            assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 2]);
3763
3764            // Seeking discards the buffered pages and reads both again with the same options.
3765            replay.seek_to(0).unwrap();
3766            assert!(replay.ensure(1).await.unwrap());
3767            assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 4]);
3768        });
3769    }
3770
3771    #[test_traced("DEBUG")]
3772    fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
3773        let executor = deterministic::Runner::default();
3774        executor.start(|_: deterministic::Context| async move {
3775            let blob = SyncTrackingBlob::new();
3776            let blob = Arc::new(blob);
3777            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3778            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3779                .await
3780                .unwrap();
3781
3782            (append, _) = append.append(b"replayed").await.unwrap();
3783            let (append, mut replay) = append
3784                .replay(NZUsize!(1024), ReadOptions::default())
3785                .await
3786                .unwrap();
3787            assert!(replay.ensure(b"replayed".len()).await.unwrap());
3788            assert_eq!(replay.remaining(), b"replayed".len());
3789            assert_eq!(replay.chunk(), b"replayed");
3790            drop(replay);
3791            drop(append);
3792
3793            let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3794            assert!(durable.is_empty());
3795            assert_eq!(writes, 1);
3796            assert_eq!(full_syncs, 0);
3797            assert_eq!(range_syncs, 0);
3798
3799            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3800            let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3801                .await
3802                .unwrap();
3803            assert_eq!(reopened.size(), b"replayed".len() as u64);
3804            reopened.sync().await.unwrap();
3805
3806            let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3807            assert_eq!(durable.len(), blob.size() as usize);
3808            assert_eq!(writes, 1);
3809            assert_eq!(full_syncs, 1);
3810            assert_eq!(range_syncs, 0);
3811        });
3812    }
3813
3814    #[test_traced("DEBUG")]
3815    fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
3816        let executor = deterministic::Runner::default();
3817        executor.start(|_: deterministic::Context| async move {
3818            let blob = SyncTrackingBlob::new();
3819            let blob = Arc::new(blob);
3820            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3821            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3822                .await
3823                .unwrap();
3824            append = append.sync().await.unwrap();
3825            (append, _) = append.append(b"valid").await.unwrap();
3826            append = append.sync().await.unwrap();
3827            drop(append);
3828
3829            blob.write_at(blob.size(), b"junk", WriteOptions::default())
3830                .await
3831                .unwrap();
3832
3833            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3834            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3835                .await
3836                .unwrap();
3837            assert_eq!(reopened.size(), b"valid".len() as u64);
3838
3839            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3840            assert_eq!(writes, 2);
3841            assert_eq!(full_syncs, 2);
3842            assert_eq!(range_syncs, 1);
3843
3844            reopened.sync().await.unwrap();
3845
3846            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3847            assert_eq!(writes, 2);
3848            assert_eq!(full_syncs, 2);
3849            assert_eq!(range_syncs, 1);
3850        });
3851    }
3852
3853    #[test_traced("DEBUG")]
3854    fn test_sync_fuses_partial_page_rewrite() {
3855        let executor = deterministic::Runner::default();
3856        executor.start(|_: deterministic::Context| async move {
3857            let blob = SyncTrackingBlob::new();
3858            let blob = Arc::new(blob);
3859            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3860            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3861                .await
3862                .unwrap();
3863            append = append.sync().await.unwrap();
3864
3865            // Establish a persisted partial page with the authoritative CRC in slot 0.
3866            (append, _) = append.append(b"abc").await.unwrap();
3867            append = append.sync().await.unwrap();
3868
3869            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3870            assert_eq!(writes, 1);
3871            assert_eq!(full_syncs, 1);
3872            assert_eq!(range_syncs, 1);
3873
3874            let slot0_offset = PAGE_SIZE.get() as u64;
3875            let slot1_offset = slot0_offset + CHECKSUM_SLOT_SIZE as u64;
3876            let slot0_before: Vec<u8> = blob
3877                .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3878                .await
3879                .unwrap()
3880                .coalesce()
3881                .freeze()
3882                .into();
3883
3884            // Extending that partial page rewrites the whole physical page in one write fused
3885            // with a range sync, resubmitting the protected slot 0 byte-identically and placing
3886            // the new CRC in slot 1.
3887            (append, _) = append.append(b"de").await.unwrap();
3888            append = append.sync().await.unwrap();
3889
3890            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3891            assert_eq!(writes, 2);
3892            assert_eq!(full_syncs, 1);
3893            assert_eq!(range_syncs, 2);
3894
3895            let slot0_after: Vec<u8> = blob
3896                .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3897                .await
3898                .unwrap()
3899                .coalesce()
3900                .freeze()
3901                .into();
3902            assert_eq!(
3903                slot0_before, slot0_after,
3904                "protected slot must be resubmitted byte-identically"
3905            );
3906            let slot1_between: Vec<u8> = blob
3907                .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3908                .await
3909                .unwrap()
3910                .coalesce()
3911                .freeze()
3912                .into();
3913
3914            // The next extension protects slot 1 and rewrites slot 0, again as one fused write.
3915            (append, _) = append.append(b"fg").await.unwrap();
3916            append.sync().await.unwrap();
3917
3918            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3919            assert_eq!(writes, 3);
3920            assert_eq!(full_syncs, 1);
3921            assert_eq!(range_syncs, 3);
3922
3923            let slot1_after: Vec<u8> = blob
3924                .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3925                .await
3926                .unwrap()
3927                .coalesce()
3928                .freeze()
3929                .into();
3930            assert_eq!(
3931                slot1_between, slot1_after,
3932                "protected slot must be resubmitted byte-identically"
3933            );
3934
3935            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3936            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3937                .await
3938                .unwrap();
3939            let read = reopened.read_at(0, 7).await.unwrap().coalesce();
3940            assert_eq!(read.as_ref(), b"abcdefg");
3941        });
3942    }
3943
3944    #[test_traced("DEBUG")]
3945    fn test_read_up_to_zero_len_truncates_buffer() {
3946        let executor = deterministic::Runner::default();
3947        executor.start(|context: deterministic::Context| async move {
3948            // Open a new blob.
3949            let (blob, blob_size) = context
3950                .open("test_partition", b"read_up_to_zero_len")
3951                .await
3952                .unwrap();
3953            assert_eq!(blob_size, 0);
3954
3955            // Create a page cache reference.
3956            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3957
3958            // Create a Writer and write some data.
3959            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
3960                .await
3961                .unwrap();
3962            (append, _) = append.append(&[1, 2, 3, 4]).await.unwrap();
3963
3964            // Request a zero-length read with a reused, non-empty buffer.
3965            let stale = vec![9u8, 8, 7, 6];
3966            let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
3967
3968            assert_eq!(read, 0);
3969            assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
3970            assert_eq!(buf.freeze().as_ref(), b"");
3971        });
3972    }
3973
3974    /// Helper to read the CRC record from raw blob bytes at the end of a physical page.
3975    fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
3976        let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
3977        Checksum::read(&mut Copying(&page_bytes[crc_start..])).unwrap()
3978    }
3979
3980    /// Blob wrapper that turns one write into a durable partial write followed by an error.
3981    struct PartialWriteBlob<B: Blob> {
3982        inner: B,
3983        writes: Arc<AtomicUsize>,
3984        read_options: Arc<Mutex<Vec<ReadOptions>>>,
3985        failed_write_len: Arc<AtomicUsize>,
3986        fail_on: usize,
3987        partial_len: usize,
3988    }
3989
3990    impl<B: Blob> PartialWriteBlob<B> {
3991        fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
3992            Self {
3993                inner,
3994                writes: Arc::new(AtomicUsize::new(0)),
3995                read_options: Arc::new(Mutex::new(Vec::new())),
3996                failed_write_len: Arc::new(AtomicUsize::new(0)),
3997                fail_on,
3998                partial_len,
3999            }
4000        }
4001
4002        fn failed_write_len(&self) -> Arc<AtomicUsize> {
4003            self.failed_write_len.clone()
4004        }
4005
4006        fn write_count(&self) -> Arc<AtomicUsize> {
4007            self.writes.clone()
4008        }
4009
4010        fn read_options(&self) -> Arc<Mutex<Vec<ReadOptions>>> {
4011            self.read_options.clone()
4012        }
4013    }
4014
4015    impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
4016        async fn read_at(
4017            &self,
4018            offset: u64,
4019            len: usize,
4020            options: ReadOptions,
4021        ) -> Result<IoBufsMut, Error> {
4022            self.read_options.lock().push(options);
4023            self.inner.read_at(offset, len, options).await
4024        }
4025
4026        async fn read_at_buf(
4027            &self,
4028            offset: u64,
4029            len: usize,
4030            bufs: impl Into<IoBufsMut> + Send,
4031            options: ReadOptions,
4032        ) -> Result<IoBufsMut, Error> {
4033            self.read_options.lock().push(options);
4034            self.inner.read_at_buf(offset, len, bufs, options).await
4035        }
4036
4037        async fn write_at(
4038            &self,
4039            offset: u64,
4040            bufs: impl Into<IoBufs> + Send,
4041            options: WriteOptions,
4042        ) -> Result<(), Error> {
4043            let bufs = bufs.into();
4044            let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
4045            if write == self.fail_on {
4046                let bytes = bufs.coalesce();
4047                self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
4048                let partial_len = self.partial_len.min(bytes.len());
4049                self.inner
4050                    .write_at(offset, bytes.slice(..partial_len), options)
4051                    .await?;
4052                if !options.contains(WriteOptions::SYNC) {
4053                    self.inner.sync().await?;
4054                }
4055                return Err(Error::Io(
4056                    std::io::Error::other("injected partial write").into(),
4057                ));
4058            }
4059
4060            self.inner.write_at(offset, bufs, options).await
4061        }
4062
4063        async fn resize(&self, len: u64) -> Result<(), Error> {
4064            self.inner.resize(len).await
4065        }
4066
4067        async fn sync(&self) -> Result<(), Error> {
4068            self.inner.sync().await
4069        }
4070
4071        async fn start_sync(&self) -> Handle<()> {
4072            self.inner.start_sync().await
4073        }
4074    }
4075
4076    /// Blob wrapper that durably writes a torn extension and its complete incoming footer.
4077    struct TornExtensionBlob<B: Blob> {
4078        inner: B,
4079        writes: Arc<AtomicUsize>,
4080        fail_on: usize,
4081        durable_payload_len: usize,
4082    }
4083
4084    impl<B: Blob> crate::Blob for TornExtensionBlob<B> {
4085        async fn read_at(
4086            &self,
4087            offset: u64,
4088            len: usize,
4089            options: ReadOptions,
4090        ) -> Result<IoBufsMut, Error> {
4091            self.inner.read_at(offset, len, options).await
4092        }
4093
4094        async fn read_at_buf(
4095            &self,
4096            offset: u64,
4097            len: usize,
4098            bufs: impl Into<IoBufsMut> + Send,
4099            options: ReadOptions,
4100        ) -> Result<IoBufsMut, Error> {
4101            self.inner.read_at_buf(offset, len, bufs, options).await
4102        }
4103
4104        async fn write_at(
4105            &self,
4106            offset: u64,
4107            bufs: impl Into<IoBufs> + Send,
4108            options: WriteOptions,
4109        ) -> Result<(), Error> {
4110            let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
4111            if write != self.fail_on {
4112                return self.inner.write_at(offset, bufs, options).await;
4113            }
4114
4115            let bytes = bufs.into().coalesce();
4116            let footer_start = bytes
4117                .len()
4118                .checked_sub(CHECKSUM_SIZE as usize)
4119                .expect("physical page must contain a checksum footer");
4120            assert!(self.durable_payload_len <= footer_start);
4121            let footer_offset = offset
4122                .checked_add(footer_start as u64)
4123                .ok_or(Error::OffsetOverflow)?;
4124            let write_options = options.without(WriteOptions::SYNC);
4125
4126            self.inner
4127                .write_at(
4128                    offset,
4129                    bytes.slice(..self.durable_payload_len),
4130                    write_options,
4131                )
4132                .await?;
4133            self.inner
4134                .write_at(footer_offset, bytes.slice(footer_start..), write_options)
4135                .await?;
4136            self.inner.sync().await?;
4137
4138            Err(Error::Io(
4139                std::io::Error::other("injected torn extension").into(),
4140            ))
4141        }
4142
4143        async fn resize(&self, len: u64) -> Result<(), Error> {
4144            self.inner.resize(len).await
4145        }
4146
4147        async fn sync(&self) -> Result<(), Error> {
4148            self.inner.sync().await
4149        }
4150
4151        async fn start_sync(&self) -> Handle<()> {
4152            self.inner.start_sync().await
4153        }
4154    }
4155
4156    #[test_traced("DEBUG")]
4157    fn test_torn_extension_with_new_footer_recovers_previous_prefix() {
4158        let executor = deterministic::Runner::default();
4159        executor.start(|context: deterministic::Context| async move {
4160            let (blob, blob_size) = context
4161                .open("test_partition", b"torn_extension_footer")
4162                .await
4163                .unwrap();
4164            let blob = Arc::new(blob);
4165            let write_count = Arc::new(AtomicUsize::new(0));
4166            let faulty_blob = TornExtensionBlob {
4167                inner: blob.clone(),
4168                writes: write_count.clone(),
4169                fail_on: 2,
4170                durable_payload_len: 4,
4171            };
4172            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4173            let mut writer = Writer::new(faulty_blob, blob_size, BUFFER_SIZE, cache_ref)
4174                .await
4175                .unwrap();
4176
4177            (writer, _) = writer.append(b"abc").await.unwrap();
4178            writer = writer.sync().await.unwrap();
4179            assert_eq!(write_count.load(Ordering::SeqCst), 1);
4180
4181            (writer, _) = writer.append(b"def").await.unwrap();
4182            assert!(writer.sync().await.is_err(), "extension write should fail");
4183            assert_eq!(write_count.load(Ordering::SeqCst), 2);
4184
4185            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4186            let page = blob
4187                .read_at(0, physical_page_size, ReadOptions::default())
4188                .await
4189                .unwrap()
4190                .coalesce();
4191            assert_eq!(&page.as_ref()[..6], b"abcd\0\0");
4192            let checksum = read_crc_record_from_page(page.as_ref());
4193            assert_eq!(checksum.len1, 3);
4194            assert_eq!(checksum.len2, 6);
4195
4196            drop(blob);
4197            let (blob, blob_size) = context
4198                .open("test_partition", b"torn_extension_footer")
4199                .await
4200                .unwrap();
4201            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4202            let recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4203                .await
4204                .unwrap();
4205            assert_eq!(recovered.size(), 3);
4206
4207            // The protected slot proves exactly the previously committed boundary: the torn
4208            // extension cannot make any later byte part of the recoverable prefix.
4209            assert_eq!(
4210                recovered
4211                    .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
4212                    .await
4213                    .unwrap(),
4214                3
4215            );
4216            let data = recovered.read_at(0, 3).await.unwrap().coalesce();
4217            assert_eq!(data.as_ref(), b"abc");
4218        });
4219    }
4220
4221    /// Blob wrapper that delays one selected read after capturing its current bytes.
4222    struct DelayedReadBlob<B: Blob> {
4223        inner: B,
4224        offset: u64,
4225        len: usize,
4226        reads: AtomicUsize,
4227        started: Mutex<Option<oneshot::Sender<()>>>,
4228        release: Mutex<Option<oneshot::Receiver<()>>>,
4229    }
4230
4231    impl<B: Blob> DelayedReadBlob<B> {
4232        const fn new(
4233            inner: B,
4234            offset: u64,
4235            len: usize,
4236            started: oneshot::Sender<()>,
4237            release: oneshot::Receiver<()>,
4238        ) -> Self {
4239            Self {
4240                inner,
4241                offset,
4242                len,
4243                reads: AtomicUsize::new(0),
4244                started: Mutex::new(Some(started)),
4245                release: Mutex::new(Some(release)),
4246            }
4247        }
4248    }
4249
4250    impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
4251        async fn read_at(
4252            &self,
4253            offset: u64,
4254            len: usize,
4255            options: ReadOptions,
4256        ) -> Result<IoBufsMut, Error> {
4257            if offset == self.offset
4258                && len == self.len
4259                && self.reads.fetch_add(1, Ordering::SeqCst) == 0
4260            {
4261                let bytes = self.inner.read_at(offset, len, options).await?;
4262
4263                let sender = self
4264                    .started
4265                    .lock()
4266                    .take()
4267                    .expect("delayed read start signal consumed more than once");
4268                let _ = sender.send(());
4269
4270                let release = self
4271                    .release
4272                    .lock()
4273                    .take()
4274                    .expect("delayed read release receiver consumed more than once");
4275                release.await.expect("release signal dropped");
4276
4277                return Ok(bytes);
4278            }
4279
4280            self.inner.read_at(offset, len, options).await
4281        }
4282
4283        async fn read_at_buf(
4284            &self,
4285            offset: u64,
4286            len: usize,
4287            bufs: impl Into<IoBufsMut> + Send,
4288            options: ReadOptions,
4289        ) -> Result<IoBufsMut, Error> {
4290            if offset == self.offset
4291                && len == self.len
4292                && self.reads.fetch_add(1, Ordering::SeqCst) == 0
4293            {
4294                let bytes = self.inner.read_at_buf(offset, len, bufs, options).await?;
4295
4296                let sender = self
4297                    .started
4298                    .lock()
4299                    .take()
4300                    .expect("delayed read start signal consumed more than once");
4301                let _ = sender.send(());
4302
4303                let release = self
4304                    .release
4305                    .lock()
4306                    .take()
4307                    .expect("delayed read release receiver consumed more than once");
4308                release.await.expect("release signal dropped");
4309
4310                return Ok(bytes);
4311            }
4312
4313            self.inner.read_at_buf(offset, len, bufs, options).await
4314        }
4315
4316        async fn write_at(
4317            &self,
4318            offset: u64,
4319            bufs: impl Into<IoBufs> + Send,
4320            options: WriteOptions,
4321        ) -> Result<(), Error> {
4322            self.inner.write_at(offset, bufs, options).await
4323        }
4324
4325        async fn resize(&self, len: u64) -> Result<(), Error> {
4326            self.inner.resize(len).await
4327        }
4328
4329        async fn sync(&self) -> Result<(), Error> {
4330            self.inner.sync().await
4331        }
4332
4333        async fn start_sync(&self) -> Handle<()> {
4334            self.inner.start_sync().await
4335        }
4336    }
4337
4338    /// Dummy marker bytes with len=0 so the mangled slot is never authoritative.
4339    /// Format: [len_hi=0, len_lo=0, 0xDE, 0xAD, 0xBE, 0xEF]
4340    const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
4341
4342    /// Physical encoding retains full-page owners and preserves the first page's durable checksum.
4343    #[rstest::rstest]
4344    #[case(103)]
4345    #[case(4084)]
4346    #[case(4096)]
4347    fn test_to_physical_pages_preserves_chunk_owners(#[case] page_size: u16) {
4348        deterministic::Runner::default().start(|context| async move {
4349            let page_size = NonZeroU16::new(page_size).unwrap();
4350            let cache = CacheRef::new(page_size, NZUsize!(4));
4351            let (blob, size) = context
4352                .open("test_partition", b"physical_chunks")
4353                .await
4354                .unwrap();
4355            let page_size = usize::from(page_size.get());
4356            let capacity = page_size * 32;
4357            let writer = Writer::new(blob, size, capacity, cache).await.unwrap();
4358
4359            // Span several allocations with 17 full pages and a partial tail without flushing.
4360            let data: Vec<_> = (0..page_size * 17 + 17).map(|i| (i % 251) as u8).collect();
4361            let mut buffer = Buffer::from(0, &[], capacity, page_size);
4362            for chunk in data.chunks(page_size) {
4363                assert!(!buffer.append(chunk));
4364            }
4365            assert!(buffer.parts().0.chunk_count() >= 4);
4366
4367            // Record full-page addresses before draining so a payload copy cannot pass unnoticed.
4368            let (prefix, tail) = buffer.parts();
4369            let pointers: Vec<_> = prefix
4370                .iter()
4371                .map(AsRef::as_ref)
4372                .chain(std::iter::once(tail))
4373                .flat_map(|chunk| chunk.chunks_exact(page_size))
4374                .map(|page| page.as_ptr())
4375                .collect();
4376
4377            // Encode two slices per full page and one padded partial page. Keep a durable checksum
4378            // in the first page's second slot, and verify that full-page payload addresses survive.
4379            let full_pages = buffer.drain_full_pages();
4380            let durable = ActiveChecksum::new(Slot::Second, 7, Crc32::checksum(&data[..7]));
4381            let (physical, partial) =
4382                writer.to_physical_pages(&full_pages, buffer.parts().1, true, None, Some(&durable));
4383            assert_eq!(physical.chunk_count(), 17 * 2 + 1);
4384            assert_eq!(partial.unwrap().len, 17);
4385            for (page, ptr) in pointers.into_iter().enumerate() {
4386                assert_eq!(physical.chunk_at(page * 2).unwrap().as_ptr(), ptr);
4387            }
4388
4389            // After the ownership checks, coalesce to validate payload bytes, padding, and CRCs.
4390            // The protected checksum belongs only to the first page.
4391            let physical = physical.coalesce();
4392            let physical_size = page_size + CHECKSUM_SIZE as usize;
4393            for (idx, page) in physical.as_ref().chunks_exact(physical_size).enumerate() {
4394                let logical = &data[idx * page_size..data.len().min((idx + 1) * page_size)];
4395                assert_eq!(&page[..logical.len()], logical);
4396                assert!(page[logical.len()..page_size].iter().all(|byte| *byte == 0));
4397                assert_eq!(
4398                    Checksum::validate_page(page).unwrap().len as usize,
4399                    logical.len()
4400                );
4401                let record = read_crc_record_from_page(page);
4402                if idx == 0 {
4403                    assert_eq!(record.get_slot(Slot::Second), (durable.len, durable.crc));
4404                } else {
4405                    assert_eq!(record.get_slot(Slot::Second), (0, 0));
4406                }
4407            }
4408        });
4409    }
4410
4411    /// Test that slot 1's durable bytes are unchanged when it's the protected slot.
4412    ///
4413    /// Strategy: After extending twice (so slot 1 becomes authoritative with larger len),
4414    /// mangle the non-authoritative slot 0. Then extend again. Slot 0 should be overwritten
4415    /// with the new CRC, while slot 1 (protected) is resubmitted byte-identically.
4416    #[test_traced("DEBUG")]
4417    fn test_crc_slot1_protected() {
4418        let executor = deterministic::Runner::default();
4419        executor.start(|context: deterministic::Context| async move {
4420            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4421            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4422            let slot0_offset = PAGE_SIZE.get() as u64;
4423            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4424
4425            // Write 10 bytes: slot 0 authoritative (len=10).
4426            let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
4427            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4428                .await
4429                .unwrap();
4430            (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4431            append = append.sync().await.unwrap();
4432            drop(append);
4433
4434            // Extend to 30 bytes: slot 1 authoritative (len=30).
4435            let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
4436            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4437                .await
4438                .unwrap();
4439            (append, _) = append
4440                .append(&(11..=30).collect::<Vec<u8>>())
4441                .await
4442                .unwrap();
4443            append = append.sync().await.unwrap();
4444            drop(append);
4445
4446            // Verify slot 1 is now authoritative
4447            let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
4448            let page = blob
4449                .read_at(0, physical_page_size, ReadOptions::default())
4450                .await
4451                .unwrap()
4452                .coalesce();
4453            let crc = read_crc_record_from_page(page.as_ref());
4454            assert!(
4455                crc.len2 > crc.len1,
4456                "Slot 1 should be authoritative (len2={} > len1={})",
4457                crc.len2,
4458                crc.len1
4459            );
4460
4461            // Capture slot 1 bytes before mangling slot 0
4462            let slot1_before: Vec<u8> = blob
4463                .read_at(slot1_offset, 6, ReadOptions::default())
4464                .await
4465                .unwrap()
4466                .coalesce()
4467                .freeze()
4468                .into();
4469
4470            // Mangle slot 0 (non-authoritative).
4471            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4472                .await
4473                .unwrap();
4474            blob.sync().await.unwrap();
4475
4476            // Verify mangle worked
4477            let slot0_mangled: Vec<u8> = blob
4478                .read_at(slot0_offset, 6, ReadOptions::default())
4479                .await
4480                .unwrap()
4481                .coalesce()
4482                .freeze()
4483                .into();
4484            assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
4485
4486            // Extend to 50 bytes: new CRC goes to slot 0, slot 1 protected.
4487            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4488                .await
4489                .unwrap();
4490            (append, _) = append
4491                .append(&(31..=50).collect::<Vec<u8>>())
4492                .await
4493                .unwrap();
4494            append = append.sync().await.unwrap();
4495            drop(append);
4496
4497            // Verify slot 0 was overwritten, slot 1 unchanged.
4498            let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
4499
4500            // Slot 0 should have new CRC (not our dummy marker)
4501            let slot0_after: Vec<u8> = blob
4502                .read_at(slot0_offset, 6, ReadOptions::default())
4503                .await
4504                .unwrap()
4505                .coalesce()
4506                .freeze()
4507                .into();
4508            assert_ne!(
4509                slot0_after, DUMMY_MARKER,
4510                "Slot 0 should have been overwritten with new CRC"
4511            );
4512
4513            // Slot 1 should be UNCHANGED (protected)
4514            let slot1_after: Vec<u8> = blob
4515                .read_at(slot1_offset, 6, ReadOptions::default())
4516                .await
4517                .unwrap()
4518                .coalesce()
4519                .freeze()
4520                .into();
4521            assert_eq!(
4522                slot1_before, slot1_after,
4523                "Slot 1 was modified! Protected region violated."
4524            );
4525
4526            // Verify the new CRC in slot 0 has len=50
4527            let page = blob
4528                .read_at(0, physical_page_size, ReadOptions::default())
4529                .await
4530                .unwrap()
4531                .coalesce();
4532            let crc = read_crc_record_from_page(page.as_ref());
4533            assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
4534        });
4535    }
4536
4537    /// Test that slot 0's durable bytes are unchanged when it's the protected slot.
4538    ///
4539    /// Strategy: After extending three times (slot 0 becomes authoritative again with largest len),
4540    /// mangle the non-authoritative slot 1. Then extend again. Slot 1 should be overwritten with
4541    /// the new CRC, while slot 0 (protected) is resubmitted byte-identically.
4542    #[test_traced("DEBUG")]
4543    fn test_crc_slot0_protected() {
4544        let executor = deterministic::Runner::default();
4545        executor.start(|context: deterministic::Context| async move {
4546            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4547            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4548            let slot0_offset = PAGE_SIZE.get() as u64;
4549            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4550
4551            // Write 10 bytes: slot 0 authoritative (len=10).
4552            let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4553            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4554                .await
4555                .unwrap();
4556            (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4557            append = append.sync().await.unwrap();
4558            drop(append);
4559
4560            // Extend to 30 bytes: slot 1 authoritative (len=30).
4561            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4562            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4563                .await
4564                .unwrap();
4565            (append, _) = append
4566                .append(&(11..=30).collect::<Vec<u8>>())
4567                .await
4568                .unwrap();
4569            append = append.sync().await.unwrap();
4570            drop(append);
4571
4572            // Extend to 50 bytes: slot 0 authoritative (len=50).
4573            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4574            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4575                .await
4576                .unwrap();
4577            (append, _) = append
4578                .append(&(31..=50).collect::<Vec<u8>>())
4579                .await
4580                .unwrap();
4581            append = append.sync().await.unwrap();
4582            drop(append);
4583
4584            // Verify slot 0 is now authoritative
4585            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4586            let page = blob
4587                .read_at(0, physical_page_size, ReadOptions::default())
4588                .await
4589                .unwrap()
4590                .coalesce();
4591            let crc = read_crc_record_from_page(page.as_ref());
4592            assert!(
4593                crc.len1 > crc.len2,
4594                "Slot 0 should be authoritative (len1={} > len2={})",
4595                crc.len1,
4596                crc.len2
4597            );
4598
4599            // Capture slot 0 bytes before mangling slot 1
4600            let slot0_before: Vec<u8> = blob
4601                .read_at(slot0_offset, 6, ReadOptions::default())
4602                .await
4603                .unwrap()
4604                .coalesce()
4605                .freeze()
4606                .into();
4607
4608            // Mangle slot 1 (non-authoritative).
4609            blob.write_at(slot1_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4610                .await
4611                .unwrap();
4612            blob.sync().await.unwrap();
4613
4614            // Verify mangle worked
4615            let slot1_mangled: Vec<u8> = blob
4616                .read_at(slot1_offset, 6, ReadOptions::default())
4617                .await
4618                .unwrap()
4619                .coalesce()
4620                .freeze()
4621                .into();
4622            assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
4623
4624            // Extend to 70 bytes: new CRC goes to slot 1, slot 0 protected.
4625            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4626                .await
4627                .unwrap();
4628            (append, _) = append
4629                .append(&(51..=70).collect::<Vec<u8>>())
4630                .await
4631                .unwrap();
4632            append = append.sync().await.unwrap();
4633            drop(append);
4634
4635            // Verify slot 1 was overwritten, slot 0 unchanged.
4636            let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4637
4638            // Slot 1 should have new CRC (not our dummy marker)
4639            let slot1_after: Vec<u8> = blob
4640                .read_at(slot1_offset, 6, ReadOptions::default())
4641                .await
4642                .unwrap()
4643                .coalesce()
4644                .freeze()
4645                .into();
4646            assert_ne!(
4647                slot1_after, DUMMY_MARKER,
4648                "Slot 1 should have been overwritten with new CRC"
4649            );
4650
4651            // Slot 0 should be UNCHANGED (protected)
4652            let slot0_after: Vec<u8> = blob
4653                .read_at(slot0_offset, 6, ReadOptions::default())
4654                .await
4655                .unwrap()
4656                .coalesce()
4657                .freeze()
4658                .into();
4659            assert_eq!(
4660                slot0_before, slot0_after,
4661                "Slot 0 was modified! Protected region violated."
4662            );
4663
4664            // Verify the new CRC in slot 1 has len=70
4665            let page = blob
4666                .read_at(0, physical_page_size, ReadOptions::default())
4667                .await
4668                .unwrap()
4669                .coalesce();
4670            let crc = read_crc_record_from_page(page.as_ref());
4671            assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
4672        });
4673    }
4674
4675    /// Test that the data prefix content is preserved when extending a partial page: the
4676    /// rewrite resubmits the committed prefix byte-identically.
4677    ///
4678    /// Strategy: Write data, then mangle the padding area (between data end and CRC start).
4679    /// After extending, the original data should be unchanged but the mangled padding
4680    /// should be overwritten with new data.
4681    #[test_traced("DEBUG")]
4682    fn test_data_prefix_preserved_when_extending() {
4683        let executor = deterministic::Runner::default();
4684        executor.start(|context: deterministic::Context| async move {
4685            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4686            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4687
4688            // Write 20 bytes.
4689            let (blob, _) = context
4690                .open("test_partition", b"prefix_test")
4691                .await
4692                .unwrap();
4693            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4694                .await
4695                .unwrap();
4696            let data1: Vec<u8> = (1..=20).collect();
4697            (append, _) = append.append(&data1).await.unwrap();
4698            append = append.sync().await.unwrap();
4699            drop(append);
4700
4701            // Capture the first 20 bytes and mangle bytes 25-30 (in padding area).
4702            let (blob, size) = context
4703                .open("test_partition", b"prefix_test")
4704                .await
4705                .unwrap();
4706            assert_eq!(size, physical_page_size as u64);
4707
4708            let prefix_before: Vec<u8> = blob
4709                .read_at(0, 20, ReadOptions::default())
4710                .await
4711                .unwrap()
4712                .coalesce()
4713                .freeze()
4714                .into();
4715
4716            // Mangle bytes 25-30 (safely in the padding area, after our 20 bytes of data)
4717            blob.write_at(25, DUMMY_MARKER.to_vec(), WriteOptions::default())
4718                .await
4719                .unwrap();
4720            blob.sync().await.unwrap();
4721
4722            // Extend to 40 bytes.
4723            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4724                .await
4725                .unwrap();
4726            (append, _) = append
4727                .append(&(21..=40).collect::<Vec<u8>>())
4728                .await
4729                .unwrap();
4730            append = append.sync().await.unwrap();
4731            drop(append);
4732
4733            // Verify prefix unchanged, mangled area overwritten.
4734            let (blob, _) = context
4735                .open("test_partition", b"prefix_test")
4736                .await
4737                .unwrap();
4738
4739            // Original 20 bytes should be unchanged
4740            let prefix_after: Vec<u8> = blob
4741                .read_at(0, 20, ReadOptions::default())
4742                .await
4743                .unwrap()
4744                .coalesce()
4745                .freeze()
4746                .into();
4747            assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
4748
4749            // Bytes at offset 25-30: data (21..=40) starts at offset 20, so offset 25 has value 26
4750            let overwritten: Vec<u8> = blob
4751                .read_at(25, 6, ReadOptions::default())
4752                .await
4753                .unwrap()
4754                .coalesce()
4755                .freeze()
4756                .into();
4757            assert_eq!(
4758                overwritten,
4759                vec![26, 27, 28, 29, 30, 31],
4760                "New data should overwrite padding area"
4761            );
4762        });
4763    }
4764
4765    /// Test CRC slot protection when extending past a page boundary.
4766    ///
4767    /// Strategy: Write partial page, mangle slot 0 (non-authoritative after we do first extend),
4768    /// then extend past page boundary. Verify slot 0 gets new full-page CRC while
4769    /// the mangled marker is overwritten, and second page is written correctly.
4770    #[test_traced("DEBUG")]
4771    fn test_crc_slot_protection_across_page_boundary() {
4772        let executor = deterministic::Runner::default();
4773        executor.start(|context: deterministic::Context| async move {
4774            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4775            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4776            let slot0_offset = PAGE_SIZE.get() as u64;
4777            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4778
4779            // Write 50 bytes: slot 0 authoritative.
4780            let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
4781            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4782                .await
4783                .unwrap();
4784            (append, _) = append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
4785            append = append.sync().await.unwrap();
4786            drop(append);
4787
4788            // Extend to 80 bytes: slot 1 authoritative.
4789            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4790            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4791                .await
4792                .unwrap();
4793            (append, _) = append
4794                .append(&(51..=80).collect::<Vec<u8>>())
4795                .await
4796                .unwrap();
4797            append = append.sync().await.unwrap();
4798            drop(append);
4799
4800            // Verify slot 1 is authoritative
4801            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4802            let page = blob
4803                .read_at(0, physical_page_size, ReadOptions::default())
4804                .await
4805                .unwrap()
4806                .coalesce();
4807            let crc = read_crc_record_from_page(page.as_ref());
4808            assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
4809
4810            // Capture slot 1 before extending past page boundary
4811            let slot1_before: Vec<u8> = blob
4812                .read_at(slot1_offset, 6, ReadOptions::default())
4813                .await
4814                .unwrap()
4815                .coalesce()
4816                .freeze()
4817                .into();
4818
4819            // Mangle slot 0 (non-authoritative)
4820            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4821                .await
4822                .unwrap();
4823            blob.sync().await.unwrap();
4824
4825            // Extend past page boundary (80 + 40 = 120, PAGE_SIZE=103).
4826            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4827                .await
4828                .unwrap();
4829            (append, _) = append
4830                .append(&(81..=120).collect::<Vec<u8>>())
4831                .await
4832                .unwrap();
4833            append = append.sync().await.unwrap();
4834            drop(append);
4835
4836            // Verify results.
4837            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4838            assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
4839
4840            // Slot 0 should have been overwritten with full-page CRC (not dummy marker)
4841            let slot0_after: Vec<u8> = blob
4842                .read_at(slot0_offset, 6, ReadOptions::default())
4843                .await
4844                .unwrap()
4845                .coalesce()
4846                .freeze()
4847                .into();
4848            assert_ne!(
4849                slot0_after, DUMMY_MARKER,
4850                "Slot 0 should have full-page CRC"
4851            );
4852
4853            // Slot 1 should be UNCHANGED (protected during boundary crossing)
4854            let slot1_after: Vec<u8> = blob
4855                .read_at(slot1_offset, 6, ReadOptions::default())
4856                .await
4857                .unwrap()
4858                .coalesce()
4859                .freeze()
4860                .into();
4861            assert_eq!(
4862                slot1_before, slot1_after,
4863                "Slot 1 was modified during page boundary crossing!"
4864            );
4865
4866            // Verify page 0 has correct CRC structure
4867            let page0 = blob
4868                .read_at(0, physical_page_size, ReadOptions::default())
4869                .await
4870                .unwrap()
4871                .coalesce();
4872            let crc0 = read_crc_record_from_page(page0.as_ref());
4873            assert_eq!(
4874                crc0.len1,
4875                PAGE_SIZE.get(),
4876                "Slot 0 should have full page length"
4877            );
4878
4879            // Verify data integrity
4880            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4881                .await
4882                .unwrap();
4883            assert_eq!(append.size(), 120);
4884            let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
4885            let expected: Vec<u8> = (1..=120).collect();
4886            assert_eq!(all_data, expected);
4887        });
4888    }
4889
4890    /// Test that corrupting the primary CRC (but not its length) causes fallback to the previous
4891    /// partial page contents.
4892    ///
4893    /// Strategy:
4894    /// 1. Write 10 bytes: slot 0 authoritative (len=10, valid crc)
4895    /// 2. Extend to 30 bytes: slot 1 authoritative (len=30, valid crc)
4896    /// 3. Corrupt ONLY the crc2 value in slot 1 (not the length)
4897    /// 4. Re-open and verify we fall back to slot 0's 10 bytes
4898    #[test_traced("DEBUG")]
4899    fn test_crc_fallback_on_corrupted_primary() {
4900        let executor = deterministic::Runner::default();
4901        executor.start(|context: deterministic::Context| async move {
4902            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4903            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4904
4905            // crc2 is at offset: PAGE_SIZE + 6 (for len2) + 2 (skip len2 bytes) = PAGE_SIZE + 8
4906            let crc2_offset = PAGE_SIZE.get() as u64 + 8;
4907
4908            // Write 10 bytes: slot 0 authoritative (len=10).
4909            let (blob, _) = context
4910                .open("test_partition", b"crc_fallback")
4911                .await
4912                .unwrap();
4913            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4914                .await
4915                .unwrap();
4916            let data1: Vec<u8> = (1..=10).collect();
4917            (append, _) = append.append(&data1).await.unwrap();
4918            append = append.sync().await.unwrap();
4919            drop(append);
4920
4921            // Extend to 30 bytes: slot 1 authoritative (len=30).
4922            let (blob, size) = context
4923                .open("test_partition", b"crc_fallback")
4924                .await
4925                .unwrap();
4926            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4927                .await
4928                .unwrap();
4929            (append, _) = append
4930                .append(&(11..=30).collect::<Vec<u8>>())
4931                .await
4932                .unwrap();
4933            append = append.sync().await.unwrap();
4934            drop(append);
4935
4936            // Verify slot 1 is now authoritative and data reads correctly
4937            let (blob, size) = context
4938                .open("test_partition", b"crc_fallback")
4939                .await
4940                .unwrap();
4941            let blob = Arc::new(blob);
4942            assert_eq!(size, physical_page_size as u64);
4943
4944            let page = blob
4945                .read_at(0, physical_page_size, ReadOptions::default())
4946                .await
4947                .unwrap()
4948                .coalesce();
4949            let crc = read_crc_record_from_page(page.as_ref());
4950            assert!(
4951                crc.len2 > crc.len1,
4952                "Slot 1 should be authoritative (len2={} > len1={})",
4953                crc.len2,
4954                crc.len1
4955            );
4956            assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
4957            assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
4958
4959            // Verify we can read all 30 bytes before corruption
4960            let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
4961                .await
4962                .unwrap();
4963            assert_eq!(append.size(), 30);
4964            let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
4965            let expected: Vec<u8> = (1..=30).collect();
4966            assert_eq!(all_data, expected);
4967            drop(append);
4968
4969            // Corrupt ONLY crc2 (not len2).
4970            // crc2 is 4 bytes at offset PAGE_SIZE + 8
4971            blob.write_at(
4972                crc2_offset,
4973                vec![0xDE, 0xAD, 0xBE, 0xEF],
4974                WriteOptions::default(),
4975            )
4976            .await
4977            .unwrap();
4978            blob.sync().await.unwrap();
4979
4980            // Verify corruption: len2 should still be 30, but crc2 is now garbage
4981            let page = blob
4982                .read_at(0, physical_page_size, ReadOptions::default())
4983                .await
4984                .unwrap()
4985                .coalesce();
4986            let crc = read_crc_record_from_page(page.as_ref());
4987            assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
4988            assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
4989
4990            // Re-open and verify fallback to slot 0's 10 bytes.
4991            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4992                .await
4993                .unwrap();
4994
4995            // Should fall back to 10 bytes (slot 0's length)
4996            assert_eq!(
4997                append.size(),
4998                10,
4999                "Should fall back to slot 0's 10 bytes after primary CRC corruption"
5000            );
5001
5002            // Verify the data is the original 10 bytes
5003            let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
5004            assert_eq!(
5005                fallback_data, data1,
5006                "Fallback data should match original 10 bytes"
5007            );
5008
5009            // Reading beyond 10 bytes should fail
5010            let result = append.read_at(0, 11).await;
5011            assert!(result.is_err(), "Reading beyond fallback size should fail");
5012        });
5013    }
5014
5015    /// Test that corrupting a non-last page's primary CRC fails even if fallback is valid.
5016    ///
5017    /// Non-last pages must always be full. If the primary CRC is corrupted and the fallback
5018    /// indicates a partial page, validation should fail entirely (not fall back to partial).
5019    ///
5020    /// Strategy:
5021    /// 1. Write 10 bytes: slot 0 has len=10 (partial)
5022    /// 2. Extend to full page (103 bytes): slot 1 has len=103 (full, authoritative)
5023    /// 3. Extend past page boundary (e.g., 110 bytes): page 0 is now non-last
5024    /// 4. Corrupt the primary CRC of page 0 (slot 1's crc, which has len=103)
5025    /// 5. Re-open and verify that reading from page 0 fails (fallback has len=10, not full)
5026    #[test_traced("DEBUG")]
5027    fn test_non_last_page_rejects_partial_fallback() {
5028        let executor = deterministic::Runner::default();
5029        executor.start(|context: deterministic::Context| async move {
5030            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5031            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
5032
5033            // crc2 for page 0 is at offset: PAGE_SIZE + 8
5034            let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
5035
5036            // Write 10 bytes: slot 0 has len=10.
5037            let (blob, _) = context
5038                .open("test_partition", b"non_last_page")
5039                .await
5040                .unwrap();
5041            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
5042                .await
5043                .unwrap();
5044            (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
5045            append = append.sync().await.unwrap();
5046            drop(append);
5047
5048            // Extend to exactly full page (103 bytes): slot 1 has len=103.
5049            let (blob, size) = context
5050                .open("test_partition", b"non_last_page")
5051                .await
5052                .unwrap();
5053            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5054                .await
5055                .unwrap();
5056            // Add bytes 11 through 103 (93 more bytes)
5057            (append, _) = append
5058                .append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
5059                .await
5060                .unwrap();
5061            append = append.sync().await.unwrap();
5062            drop(append);
5063
5064            // Verify page 0 slot 1 is authoritative with len=103 (full page)
5065            let (blob, size) = context
5066                .open("test_partition", b"non_last_page")
5067                .await
5068                .unwrap();
5069            let page = blob
5070                .read_at(0, physical_page_size, ReadOptions::default())
5071                .await
5072                .unwrap()
5073                .coalesce();
5074            let crc = read_crc_record_from_page(page.as_ref());
5075            assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
5076            assert_eq!(
5077                crc.len2,
5078                PAGE_SIZE.get(),
5079                "Slot 1 should have len=103 (full page)"
5080            );
5081            assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
5082
5083            // Extend past page boundary (add 10 more bytes for total of 113).
5084            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5085                .await
5086                .unwrap();
5087            // Add bytes 104 through 113 (10 more bytes, now on page 1)
5088            (append, _) = append
5089                .append(&(104..=113).collect::<Vec<u8>>())
5090                .await
5091                .unwrap();
5092            append = append.sync().await.unwrap();
5093            drop(append);
5094
5095            // Verify we now have 2 pages
5096            let (blob, size) = context
5097                .open("test_partition", b"non_last_page")
5098                .await
5099                .unwrap();
5100            let blob = Arc::new(blob);
5101            assert_eq!(
5102                size,
5103                (physical_page_size * 2) as u64,
5104                "Should have 2 physical pages"
5105            );
5106
5107            // Verify data is readable before corruption
5108            let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
5109                .await
5110                .unwrap();
5111            assert_eq!(append.size(), 113);
5112            let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
5113            let expected: Vec<u8> = (1..=113).collect();
5114            assert_eq!(all_data, expected);
5115            drop(append);
5116
5117            // Corrupt page 0's primary CRC (slot 1's crc2).
5118            blob.write_at(
5119                page0_crc2_offset,
5120                vec![0xDE, 0xAD, 0xBE, 0xEF],
5121                WriteOptions::default(),
5122            )
5123            .await
5124            .unwrap();
5125            blob.sync().await.unwrap();
5126
5127            // Verify corruption: page 0's slot 1 still has len=103 but bad CRC
5128            let page = blob
5129                .read_at(0, physical_page_size, ReadOptions::default())
5130                .await
5131                .unwrap()
5132                .coalesce();
5133            let crc = read_crc_record_from_page(page.as_ref());
5134            assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
5135            assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
5136            // Slot 0 fallback has len=10 (partial), which is invalid for non-last page
5137            assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
5138
5139            // Re-open and try to read from page 0.
5140            // The first page's primary CRC is bad, and fallback indicates partial (len=10).
5141            // Since page 0 is not the last page, a partial fallback is invalid.
5142            // Reading from page 0 should fail because the fallback CRC indicates a partial
5143            // page, which is not allowed for non-last pages.
5144            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5145                .await
5146                .unwrap();
5147
5148            // The blob still reports 113 bytes because init only validates the last page.
5149            // But reading from page 0 should fail because the CRC fallback is partial.
5150            assert_eq!(append.size(), 113);
5151
5152            // Reading from page 0 should fail with InvalidChecksum because the fallback CRC has
5153            // len=10 (partial), which is invalid for a non-last page.
5154            let result = append.read_at(0, 10).await;
5155            assert!(
5156                result.is_err(),
5157                "Reading from corrupted non-last page via Append should fail, but got: {:?}",
5158                result
5159            );
5160            drop(append);
5161
5162            // Also verify that reading via Replay fails the same way.
5163            let (blob, size) = context
5164                .open("test_partition", b"non_last_page")
5165                .await
5166                .unwrap();
5167            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5168                .await
5169                .unwrap();
5170            let (_, mut replay) = append
5171                .replay(NZUsize!(1024), ReadOptions::default())
5172                .await
5173                .unwrap();
5174
5175            // Filling pages should fail on CRC validation.
5176            let result = replay.ensure(1).await;
5177            assert!(
5178                result.is_err(),
5179                "Reading from corrupted non-last page via Replay should fail, but got: {:?}",
5180                result
5181            );
5182        });
5183    }
5184
5185    /// A crash can cut a flush so that the pages after a filled page persist while that page
5186    /// keeps only its earlier, shorter checksum slot. A replay fails the fill that reaches that
5187    /// page with [Error::InvalidChecksum], and so does a fill after seeking into it. While the
5188    /// full-page slot persisted, the replay yields every byte.
5189    #[test]
5190    fn test_replay_rejects_short_interior_page() {
5191        let executor = deterministic::Runner::default();
5192        executor.start(|context: deterministic::Context| async move {
5193            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5194            let page_size: u64 = PAGE_SIZE.widen();
5195
5196            // Page 0 keeps a 10-byte slot from its partial era and a full-page slot once it
5197            // fills. Page 1 holds the final 10 bytes, so page 0 is an interior page.
5198            let (blob, _) = context
5199                .open("test_partition", b"short_bound")
5200                .await
5201                .unwrap();
5202            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
5203                .await
5204                .unwrap();
5205            (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
5206            append = append.sync().await.unwrap();
5207            (append, _) = append
5208                .append(&(11..=113).collect::<Vec<u8>>())
5209                .await
5210                .unwrap();
5211            append = append.sync().await.unwrap();
5212            drop(append);
5213
5214            let (blob, size) = context
5215                .open("test_partition", b"short_bound")
5216                .await
5217                .unwrap();
5218            let blob = Arc::new(blob);
5219
5220            // While the full-page slot persisted, the replay yields every byte.
5221            let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
5222                .await
5223                .unwrap();
5224            let (_, mut replay) = append
5225                .replay(NZUsize!(1024), ReadOptions::default())
5226                .await
5227                .unwrap();
5228            assert!(replay.ensure(113).await.unwrap());
5229            assert_eq!(replay.remaining(), 113);
5230            assert_eq!(
5231                replay.copy_to_bytes(113).as_ref(),
5232                (1..=113).collect::<Vec<u8>>()
5233            );
5234            drop(replay);
5235
5236            // Model the crash cut that persisted page 1 but not page 0's full-page slot: overwrite
5237            // that slot's CRC so page 0 validates only through its 10-byte slot.
5238            let page = blob
5239                .read_at(
5240                    0,
5241                    usize::from(PAGE_SIZE.get()) + CHECKSUM_SIZE as usize,
5242                    ReadOptions::default(),
5243                )
5244                .await
5245                .unwrap()
5246                .coalesce();
5247            let crc = read_crc_record_from_page(page.as_ref());
5248            assert_eq!((crc.len1, crc.len2), (10, PAGE_SIZE.get()));
5249            blob.write_at(
5250                page_size + 8,
5251                vec![0xDE, 0xAD, 0xBE, 0xEF],
5252                WriteOptions::default(),
5253            )
5254            .await
5255            .unwrap();
5256            blob.sync().await.unwrap();
5257
5258            // Page 0 is an interior page that validates only 10 of its bytes.
5259            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5260                .await
5261                .unwrap();
5262            assert_eq!(append.size(), 113);
5263            let (_, mut replay) = append
5264                .replay(NZUsize!(1024), ReadOptions::default())
5265                .await
5266                .unwrap();
5267            assert_eq!(replay.blob_size(), 113);
5268            assert!(
5269                matches!(replay.ensure(50).await, Err(Error::InvalidChecksum)),
5270                "an interior page covering fewer bytes than a page must fail validation"
5271            );
5272
5273            // A fill after seeking past the surviving slot's bytes fails the same way.
5274            replay.seek_to(40).unwrap();
5275            assert!(matches!(
5276                replay.ensure(1).await,
5277                Err(Error::InvalidChecksum)
5278            ));
5279            assert!(replay.chunk().is_empty());
5280            assert_eq!(replay.remaining(), 0);
5281        });
5282    }
5283
5284    #[test]
5285    fn test_recovery_truncate_partial_shrink_uses_uncached_read_hint() {
5286        let executor = deterministic::Runner::default();
5287        executor.start(|context| async move {
5288            let (blob, size) = context
5289                .open("test_partition", b"resize_read_options")
5290                .await
5291                .unwrap();
5292            let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
5293            let read_options = blob.read_options();
5294            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5295            let mut writer = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5296                .await
5297                .unwrap();
5298
5299            let data = vec![7; PAGE_SIZE.get() as usize];
5300            (writer, _) = writer.append(&data).await.unwrap();
5301            writer = writer.sync().await.unwrap();
5302
5303            // The shrink retains the prefix in the in-memory tip, so its backing read requests
5304            // DONT_CACHE.
5305            writer.truncate(50).await.unwrap();
5306
5307            assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE]);
5308        });
5309    }
5310
5311    #[test]
5312    fn test_recovery_truncate_shrink_validates_crc() {
5313        // Verify that shrinking a blob to a partial page validates the CRC, rather than
5314        // blindly reading raw bytes which could silently load corrupted data.
5315        let executor = deterministic::Runner::default();
5316        executor.start(|context| async move {
5317            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5318            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
5319
5320            let (blob, size) = context
5321                .open("test_partition", b"resize_crc_test")
5322                .await
5323                .unwrap();
5324
5325            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5326                .await
5327                .unwrap();
5328
5329            // Write two full pages and one partial page.
5330            // PAGE_SIZE = 103, so 250 bytes = 103 + 103 + 44.
5331            let data: Vec<u8> = (0..=249).collect();
5332            (append, _) = append.append(&data).await.unwrap();
5333            append = append.sync().await.unwrap();
5334            assert_eq!(append.size(), 250);
5335            drop(append);
5336
5337            // Corrupt the CRC record of page 1 (middle page).
5338            let (blob, size) = context
5339                .open("test_partition", b"resize_crc_test")
5340                .await
5341                .unwrap();
5342            assert_eq!(size as usize, physical_page_size * 3);
5343
5344            // Page 1 CRC record is at the end of the second physical page.
5345            let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
5346            blob.write_at(
5347                page1_crc_offset,
5348                vec![0xFF; CHECKSUM_SIZE as usize],
5349                WriteOptions::default(),
5350            )
5351            .await
5352            .unwrap();
5353            blob.sync().await.unwrap();
5354
5355            // Recovery validates only the terminal page. Truncation discovers page 1 corruption.
5356            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5357                .await
5358                .unwrap();
5359            assert_eq!(append.size(), 250);
5360
5361            // Try to shrink to 150 bytes, which ends in page 1 (the corrupted page).
5362            // 150 bytes = page 0 (103 full) + page 1 (47 partial).
5363            // This should fail because page 1's CRC is corrupted.
5364            let result = append.truncate(150).await.err();
5365            assert!(
5366                matches!(result, Some(crate::Error::InvalidChecksum)),
5367                "Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
5368                result
5369            );
5370        });
5371    }
5372
5373    #[test]
5374    fn test_recovery_preserves_cached_prefix_on_publication() {
5375        // Exercise publication with an unchanged tail and with two shorter retained prefixes.
5376        let page = PAGE_SIZE.get() as u64;
5377        for end in [None, Some(page * 2 + 10), Some(page + 10)] {
5378            deterministic::Runner::default().start(|context| async move {
5379                // Persist several pages and prime the first page in the recovery cache.
5380                let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5381                let (blob, size) = context.open("recovery-cache", b"blob").await.unwrap();
5382                let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
5383                    .await
5384                    .unwrap();
5385                let page = PAGE_SIZE.get() as usize;
5386                (recovery, _) = recovery.append(&vec![0xA5; page * 2 + 20]).await.unwrap();
5387                recovery = recovery.sync().await.unwrap();
5388                recovery.read_at(0, page).await.unwrap();
5389
5390                // Shortening the tail must leave the retained first page cached.
5391                if let Some(end) = end {
5392                    recovery = recovery.truncate(end).await.unwrap();
5393                }
5394
5395                // Publishing the append owner keeps the retained cache available.
5396                let writer: Writer<_> = recovery.into();
5397                let mut cached = vec![0; page];
5398                assert!(writer.try_read_sync_into(&mut cached, 0));
5399                assert_eq!(cached, vec![0xA5; page]);
5400            });
5401        }
5402    }
5403
5404    #[test]
5405    fn test_recovery_truncate_direct_append_replaces_cached_pages() {
5406        deterministic::Runner::default().start(|context| async move {
5407            // Persist a prefix and cache pages that the replacement will overlap.
5408            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(2));
5409            let (blob, size) = context.open("recovery-cache", b"direct").await.unwrap();
5410            let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
5411                .await
5412                .unwrap();
5413            let page = PAGE_SIZE.get() as usize;
5414            let mut model: Vec<u8> = (0..5 * page + 20).map(|i| (i % 251) as u8).collect();
5415            (recovery, _) = recovery.append(&model).await.unwrap();
5416            recovery = recovery.sync().await.unwrap();
5417            cache.clear();
5418            recovery.read_at(page as u64, page).await.unwrap();
5419            recovery.read_at((2 * page) as u64, page).await.unwrap();
5420
5421            // Shorten into a cached page, then bypass the buffer with a multi-page replacement.
5422            recovery = recovery.truncate((page + 10) as u64).await.unwrap();
5423            model.truncate(page + 10);
5424            let replacement = vec![0xCC; 3 * page + 7];
5425            (recovery, _) = recovery
5426                .append_owned(replacement.clone().into())
5427                .await
5428                .unwrap();
5429            model.extend_from_slice(&replacement);
5430
5431            // Reads must use replacement bytes rather than the discarded cached suffix.
5432            assert_eq!(
5433                recovery
5434                    .read_at((2 * page) as u64, page)
5435                    .await
5436                    .unwrap()
5437                    .coalesce()
5438                    .as_ref(),
5439                &model[2 * page..3 * page]
5440            );
5441            assert_eq!(
5442                recovery
5443                    .read_at(0, model.len())
5444                    .await
5445                    .unwrap()
5446                    .coalesce()
5447                    .as_ref(),
5448                model
5449            );
5450            recovery = recovery.sync().await.unwrap();
5451
5452            // A published snapshot retains its prefix while later appends extend the live writer.
5453            let writer: Writer<_> = recovery.into();
5454            let (mut writer, snapshot) = writer.snapshot().await.unwrap();
5455            (writer, _) = writer.append(&vec![0xDD; page - 17]).await.unwrap();
5456            writer = writer.sync().await.unwrap();
5457            assert_eq!(
5458                snapshot
5459                    .read_at(0, model.len())
5460                    .await
5461                    .unwrap()
5462                    .coalesce()
5463                    .as_ref(),
5464                model
5465            );
5466            model.extend_from_slice(&vec![0xDD; page - 17]);
5467            assert_eq!(
5468                writer
5469                    .read_at(0, model.len())
5470                    .await
5471                    .unwrap()
5472                    .coalesce()
5473                    .as_ref(),
5474                model
5475            );
5476        });
5477    }
5478
5479    #[test]
5480    fn test_cancelled_recovery_read_cannot_repopulate_after_truncate() {
5481        let cfg =
5482            deterministic::Config::default().with_timeout(Some(std::time::Duration::from_secs(5)));
5483        deterministic::Runner::new(cfg).start(|context| async move {
5484            // Park a read of the second page after it has captured the persisted bytes.
5485            let page = PAGE_SIZE.get() as usize;
5486            let physical = page + CHECKSUM_SIZE as usize;
5487            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
5488            let (inner, size) = context.open("recovery-cache", b"cancelled").await.unwrap();
5489            let (started_tx, started_rx) = oneshot::channel();
5490            let (release_tx, release_rx) = oneshot::channel();
5491            let blob =
5492                DelayedReadBlob::new(inner, physical as u64, physical, started_tx, release_rx);
5493            let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
5494                .await
5495                .unwrap();
5496            (recovery, _) = recovery.append(&vec![0xAA; 3 * page + 20]).await.unwrap();
5497            recovery = recovery.sync().await.unwrap();
5498            cache.clear();
5499
5500            // Drop the parked read before replacing its source page, cancelling its cache
5501            // publication.
5502            {
5503                let mut read = Box::pin(recovery.read_at(page as u64, page));
5504                commonware_macros::select! {
5505                    _ = started_rx => {},
5506                    _ = read.as_mut() => panic!("read completed before release"),
5507                }
5508            }
5509
5510            // Replace the discarded suffix and evict its page from the single-page cache.
5511            recovery = recovery.truncate((page + 10) as u64).await.unwrap();
5512            (recovery, _) = recovery.append(&vec![0xBB; page - 10]).await.unwrap();
5513            recovery = recovery.sync().await.unwrap();
5514            recovery.read_at(0, page).await.unwrap();
5515
5516            // Releasing the abandoned read must not restore old bytes to the cache.
5517            let _ = release_tx.send(());
5518            let actual = recovery
5519                .read_at(page as u64, page)
5520                .await
5521                .unwrap()
5522                .coalesce();
5523            assert_eq!(&actual.as_ref()[..10], &[0xAA; 10]);
5524            assert_eq!(&actual.as_ref()[10..], vec![0xBB; page - 10]);
5525        });
5526    }
5527
5528    #[test]
5529    fn test_recovery_truncate_replaces_cached_tail() {
5530        deterministic::Runner::default().start(|context| async move {
5531            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5532            let (blob, size) = context.open("recovery-cache", b"truncate").await.unwrap();
5533            let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
5534                .await
5535                .unwrap();
5536            let page = PAGE_SIZE.get() as usize;
5537            (recovery, _) = recovery.append(&vec![0xAA; page * 2 + 16]).await.unwrap();
5538            recovery = recovery.sync().await.unwrap();
5539            recovery.read_at(0, page * 2).await.unwrap();
5540            let mut cached = vec![0; page * 2];
5541            assert!(recovery.try_read_sync_into(&mut cached, 0));
5542            assert_eq!(cached, vec![0xAA; page * 2]);
5543
5544            recovery = recovery.truncate((page + 16) as u64).await.unwrap();
5545            recovery.read_at(0, page).await.unwrap();
5546            recovery = recovery.truncate((page + 8) as u64).await.unwrap();
5547            assert!(recovery.try_read_sync_into(&mut cached[..page], 0));
5548
5549            (recovery, _) = recovery.append(&vec![0xBB; page - 8]).await.unwrap();
5550            recovery = recovery.sync().await.unwrap();
5551
5552            // Page 1 is now full, so this read exercises cached bytes rather than the tip.
5553            let mut rewritten = vec![0; page];
5554            assert!(recovery.try_read_sync_into(&mut rewritten, page as u64));
5555            assert_eq!(&rewritten[..8], &[0xAA; 8]);
5556            assert_eq!(&rewritten[8..], vec![0xBB; page - 8]);
5557        });
5558    }
5559
5560    #[test]
5561    fn test_snapshot_fetch_populates_shared_cache_across_append() {
5562        let executor = deterministic::Runner::default();
5563        executor.start(|context: deterministic::Context| async move {
5564            // Delay a snapshot read of the second persisted page until after a later append.
5565            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5566            let page_size = PAGE_SIZE.get() as usize;
5567            let physical_page_size = page_size + CHECKSUM_SIZE as usize;
5568            let (inner, blob_size) = context
5569                .open("test_partition", b"snapshot_resize_cache")
5570                .await
5571                .unwrap();
5572            let (started_tx, started_rx) = oneshot::channel();
5573            let (release_tx, release_rx) = oneshot::channel();
5574            let blob = DelayedReadBlob::new(
5575                inner,
5576                physical_page_size as u64,
5577                physical_page_size,
5578                started_tx,
5579                release_rx,
5580            );
5581            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5582                .await
5583                .unwrap();
5584
5585            // Create two immutable pages that both the writer and snapshot can cache.
5586            let old_page0 = vec![0x11u8; page_size];
5587            let old_page1 = vec![0x22u8; page_size];
5588            (writer, _) = writer.append(&old_page0).await.unwrap();
5589            (writer, _) = writer.append(&old_page1).await.unwrap();
5590            writer = writer.sync().await.unwrap();
5591
5592            writer.cache_ref.clear();
5593
5594            // Start a cache miss through the snapshot and park the captured read.
5595            let (mut writer, snapshot) = writer.snapshot().await.unwrap();
5596            let snapshot_task = context
5597                .child("snapshot")
5598                .spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
5599            started_rx.await.expect("snapshot read never started");
5600
5601            // Append another page while the snapshot still owns its earlier view.
5602            let new_page1 = vec![0x33u8; page_size];
5603            (writer, _) = writer.append(&new_page1).await.unwrap();
5604            writer = writer.sync().await.unwrap();
5605
5606            // Finish the snapshot read and verify it fills the cache shared with the writer.
5607            let _ = release_tx.send(());
5608            let stale = snapshot_task
5609                .await
5610                .expect("snapshot task failed")
5611                .expect("snapshot read failed")
5612                .coalesce();
5613            assert_eq!(stale.as_ref(), old_page1.as_slice());
5614
5615            let mut probe = vec![0u8; page_size];
5616            assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
5617            assert_eq!(probe, old_page1);
5618        });
5619    }
5620
5621    #[test]
5622    fn test_append_preserves_snapshot_partial_page() {
5623        let executor = deterministic::Runner::default();
5624        executor.start(|context: deterministic::Context| async move {
5625            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5626            let (blob, blob_size) = context
5627                .open("test_partition", b"snapshot_blocks_shrink")
5628                .await
5629                .unwrap();
5630            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5631                .await
5632                .unwrap();
5633
5634            let page_size = PAGE_SIZE.get() as usize;
5635            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
5636            (append, _) = append.append(&data).await.unwrap();
5637            append = append.sync().await.unwrap();
5638
5639            let tail_ptr = append.buffer.parts().1.as_ptr();
5640            let (mut append, snapshot) = append.snapshot().await.unwrap();
5641            let snapshot_clone = snapshot.clone();
5642            let snapshot_size = snapshot.size();
5643
5644            let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
5645            assert_eq!(read.as_ref(), data.as_slice());
5646
5647            // The snapshot owns a copied partial page, so appends can reuse the writer's unique
5648            // tail while the snapshot continues serving its original bytes.
5649            (append, _) = append.append(&[0; 3]).await.unwrap();
5650            assert_eq!(append.size(), snapshot_size + 3);
5651            assert_eq!(append.buffer.parts().1.as_ptr(), tail_ptr);
5652
5653            let original = snapshot_clone
5654                .read_at(0, data.len())
5655                .await
5656                .unwrap()
5657                .coalesce();
5658            assert_eq!(original.as_ref(), data.as_slice());
5659            assert_eq!(snapshot_clone.size(), snapshot_size);
5660        });
5661    }
5662
5663    #[test]
5664    fn test_snapshot_read_many_into_matches_read_at() {
5665        let executor = deterministic::Runner::default();
5666        executor.start(|context: deterministic::Context| async move {
5667            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5668            let (blob, blob_size) = context
5669                .open("test_partition", b"snapshot_read_many")
5670                .await
5671                .unwrap();
5672            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5673                .await
5674                .unwrap();
5675
5676            let item_size = 4;
5677            let page_size = PAGE_SIZE.get() as usize;
5678            let total = page_size * 3 + 13;
5679            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
5680            (append, _) = append.append(&data).await.unwrap();
5681            append = append.sync().await.unwrap();
5682
5683            let (_, snapshot) = append.snapshot().await.unwrap();
5684
5685            // Batch reads cover page starts, page ends, page interiors, and the frozen tail.
5686            let offsets = [
5687                0,
5688                (page_size - item_size) as u64,
5689                (page_size + 5) as u64,
5690                (page_size * 3 + 2) as u64,
5691            ];
5692            let mut batch = vec![0u8; offsets.len() * item_size];
5693            snapshot
5694                .read_many_into(&mut batch, &offsets, NZUsize!(item_size))
5695                .await
5696                .unwrap();
5697
5698            for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
5699                let single = snapshot
5700                    .read_at(offset, item_size)
5701                    .await
5702                    .unwrap()
5703                    .coalesce();
5704                assert_eq!(item, single.as_ref());
5705                assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
5706            }
5707        });
5708    }
5709
5710    #[test]
5711    fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
5712        let executor = deterministic::Runner::default();
5713        executor.start(|context: deterministic::Context| async move {
5714            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5715            let (blob, blob_size) = context
5716                .open("test_partition", b"snapshot_try_read_tail")
5717                .await
5718                .unwrap();
5719            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5720                .await
5721                .unwrap();
5722
5723            let page_size = PAGE_SIZE.get() as usize;
5724            (append, _) = append.append(&vec![0xAA; page_size]).await.unwrap();
5725            append = append.sync().await.unwrap();
5726
5727            let tail = b"oldtail";
5728            (append, _) = append.append(tail).await.unwrap();
5729            let (append, snapshot) = append.snapshot().await.unwrap();
5730
5731            // Stale cached bytes must not replace the snapshot's owned partial tail.
5732            let poison = vec![0xBB; page_size];
5733            assert_eq!(
5734                append.cache_ref.cache(append.id, &poison, page_size as u64),
5735                0
5736            );
5737
5738            let mut read = vec![0; tail.len()];
5739            assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
5740            assert_eq!(read.as_slice(), tail);
5741        });
5742    }
5743
5744    #[test]
5745    fn test_recovery_truncate_same_size_preserves_contents() {
5746        let executor = deterministic::Runner::default();
5747        executor.start(|context: deterministic::Context| async move {
5748            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5749            let (blob, blob_size) = context
5750                .open("test_partition", b"resize_same_size")
5751                .await
5752                .unwrap();
5753            let mut append = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
5754                .await
5755                .unwrap();
5756
5757            (append, _) = append.append(b"hello world").await.unwrap();
5758            assert_eq!(append.size(), 11);
5759
5760            // Truncating at the existing end preserves the contents.
5761            append = append.truncate(11).await.unwrap();
5762            assert_eq!(append.size(), 11);
5763
5764            // Verify content is still readable and intact.
5765            let read = append.read_at(0, 11).await.unwrap().coalesce();
5766            assert_eq!(read.as_ref(), b"hello world");
5767        });
5768    }
5769
5770    #[test]
5771    fn test_recovery_truncate_same_page_shrink_reopens_at_shorter_size() {
5772        let executor = deterministic::Runner::default();
5773        executor.start(|context| async move {
5774            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5775            let data: Vec<u8> = (0..50).collect();
5776
5777            let (blob, size) = context
5778                .open("test_partition", b"same_page_shrink")
5779                .await
5780                .unwrap();
5781            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5782                .await
5783                .unwrap();
5784
5785            // Create a partial page whose authoritative checksum is in the first slot.
5786            (append, _) = append.append(&data).await.unwrap();
5787            append = append.sync().await.unwrap();
5788
5789            // Durably shorten within the page and release the writer before recovery.
5790            append = append.truncate(45).await.unwrap();
5791            drop(append);
5792
5793            // A fresh recovery must select the shorter checksum and return its exact prefix.
5794            let (blob, size) = context
5795                .open("test_partition", b"same_page_shrink")
5796                .await
5797                .unwrap();
5798            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5799                .await
5800                .unwrap();
5801            assert_eq!(append.size(), 45);
5802            let read = append.read_at(0, 45).await.unwrap().coalesce();
5803            assert_eq!(read.as_ref(), &data[..45]);
5804        });
5805    }
5806
5807    /// A torn tail-page rewrite after a durable shrink must not resurrect the retired slot:
5808    /// the shrink zeroes the whole slot, so stale length bytes alone cannot reassemble a valid
5809    /// checksum over the pre-shrink bytes still on the page.
5810    #[test_traced("DEBUG")]
5811    fn test_shrink_then_torn_rewrite_does_not_resurrect() {
5812        let executor = deterministic::Runner::default();
5813        executor.start(|context: deterministic::Context| async move {
5814            let (blob, blob_size) = context
5815                .open("test_partition", b"shrink_torn")
5816                .await
5817                .unwrap();
5818            let blob = Arc::new(blob);
5819            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5820            let mut writer = Recovery::open(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
5821                .await
5822                .unwrap();
5823
5824            // Commit 80 bytes into the first slot, then durably shrink the tail page to 50.
5825            let data: Vec<u8> = (1u8..=80).collect();
5826            (writer, _) = writer.append(&data).await.unwrap();
5827            writer = writer.sync().await.unwrap();
5828            writer = writer.truncate(50).await.unwrap();
5829            drop(writer);
5830
5831            // Model a rewrite of the tail page back to 80 bytes torn down to only the retired
5832            // slot's length bytes: the pre-shrink data still on the page must not revalidate.
5833            let page_size = u64::from(PAGE_SIZE.get());
5834            blob.write_at(
5835                page_size,
5836                80u16.to_be_bytes().to_vec(),
5837                WriteOptions::default(),
5838            )
5839            .await
5840            .unwrap();
5841            blob.sync().await.unwrap();
5842
5843            drop(blob);
5844            let (blob, blob_size) = context
5845                .open("test_partition", b"shrink_torn")
5846                .await
5847                .unwrap();
5848            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5849            let recovered = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
5850                .await
5851                .unwrap();
5852            assert_eq!(recovered.size(), 50);
5853            let read = recovered.read_at(0, 50).await.unwrap().coalesce();
5854            assert_eq!(read.as_ref(), &data[..50]);
5855        });
5856    }
5857
5858    #[test]
5859    fn test_recovery_truncate_same_page_shrink_survives_interrupted_crc_stage() {
5860        let executor = deterministic::Runner::default();
5861        executor.start(|context| async move {
5862            // Put the durable 50-byte prefix in the second checksum slot.
5863            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5864            let data: Vec<u8> = (0..50).collect();
5865
5866            let (blob, size) = context
5867                .open("test_partition", b"same_page_shrink_interrupted")
5868                .await
5869                .unwrap();
5870            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5871                .await
5872                .unwrap();
5873            (append, _) = append.append(&data[..40]).await.unwrap();
5874            append = append.sync().await.unwrap();
5875            (append, _) = append.append(&data[40..]).await.unwrap();
5876            append = append.sync().await.unwrap();
5877            drop(append);
5878
5879            // Tear the staged shorter checksum before its length can be published.
5880            let (blob, size) = context
5881                .open("test_partition", b"same_page_shrink_interrupted")
5882                .await
5883                .unwrap();
5884            let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
5885            let write_count = faulty_blob.write_count();
5886            let failed_write_len = faulty_blob.failed_write_len();
5887            let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5888                .await
5889                .unwrap();
5890
5891            assert!(
5892                append.truncate(45).await.is_err(),
5893                "phase-1 partial write should fail"
5894            );
5895            assert_eq!(write_count.load(Ordering::SeqCst), 1);
5896            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5897
5898            // The untouched authoritative slot must recover all 50 durable bytes.
5899            let (blob, size) = context
5900                .open("test_partition", b"same_page_shrink_interrupted")
5901                .await
5902                .unwrap();
5903            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5904                .await
5905                .unwrap();
5906            assert_eq!(append.size(), 50);
5907            let read = append.read_at(0, 50).await.unwrap().coalesce();
5908            assert_eq!(read.as_ref(), &data);
5909        });
5910    }
5911
5912    #[test]
5913    fn test_recovery_truncate_same_page_shrink_survives_interrupted_len_stage() {
5914        let executor = deterministic::Runner::default();
5915        executor.start(|context| async move {
5916            // Use a length above 255 so a one-byte tear changes the decoded length.
5917            const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
5918            const LARGE_BUFFER_SIZE: usize = 1_200;
5919
5920            let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
5921            let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
5922
5923            let (blob, size) = context
5924                .open("test_partition", b"same_page_shrink_len_stage")
5925                .await
5926                .unwrap();
5927            let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5928                .await
5929                .unwrap();
5930            (append, _) = append.append(&data[..255]).await.unwrap();
5931            append = append.sync().await.unwrap();
5932            (append, _) = append.append(&data[255..]).await.unwrap();
5933            append = append.sync().await.unwrap();
5934            drop(append);
5935
5936            // Tear the shorter slot's published length after its checksum is staged.
5937            let (blob, size) = context
5938                .open("test_partition", b"same_page_shrink_len_stage")
5939                .await
5940                .unwrap();
5941            let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
5942            let write_count = faulty_blob.write_count();
5943            let failed_write_len = faulty_blob.failed_write_len();
5944            let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5945                .await
5946                .unwrap();
5947
5948            assert!(
5949                append.truncate(257).await.is_err(),
5950                "length-stage partial write should fail"
5951            );
5952            assert_eq!(write_count.load(Ordering::SeqCst), 2);
5953            assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
5954
5955            // The torn new length must leave the original 300-byte slot authoritative.
5956            let (blob, size) = context
5957                .open("test_partition", b"same_page_shrink_len_stage")
5958                .await
5959                .unwrap();
5960            let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
5961                .await
5962                .unwrap();
5963            assert_eq!(append.size(), 300);
5964            let read = append.read_at(0, 300).await.unwrap().coalesce();
5965            assert_eq!(read.as_ref(), &data);
5966        });
5967    }
5968
5969    #[test]
5970    fn test_recovery_truncate_same_page_shrink_preserves_validated_fallback_slot() {
5971        let executor = deterministic::Runner::default();
5972        executor.start(|context| async move {
5973            // Prepare two durable checksum slots and tear the next checksum write.
5974            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5975            let data: Vec<u8> = (0..52).collect();
5976
5977            let (blob, size) = context
5978                .open("test_partition", b"same_page_shrink_fallback_slot")
5979                .await
5980                .unwrap();
5981            let blob = Arc::new(blob);
5982            let faulty_blob = PartialWriteBlob::new(blob.clone(), 4, 3);
5983            let write_count = faulty_blob.write_count();
5984            let failed_write_len = faulty_blob.failed_write_len();
5985            let mut append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5986                .await
5987                .unwrap();
5988            (append, _) = append.append(&data[..48]).await.unwrap();
5989            append = append.sync().await.unwrap();
5990            assert_eq!(write_count.load(Ordering::SeqCst), 1);
5991
5992            (append, _) = append.append(&data[48..50]).await.unwrap();
5993            append = append.sync().await.unwrap();
5994            assert_eq!(write_count.load(Ordering::SeqCst), 2);
5995
5996            (append, _) = append.append(&data[50..]).await.unwrap();
5997            append = append.sync().await.unwrap();
5998            assert_eq!(write_count.load(Ordering::SeqCst), 3);
5999
6000            // Corrupt the newer authoritative slot. The older slot still covers the shrink target.
6001            // The cached footer no longer matches storage. A torn phase-1 shrink write must
6002            // preserve the fallback slot validated from storage.
6003            let slot0_offset = PAGE_SIZE.get() as u64;
6004            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
6005                .await
6006                .unwrap();
6007            blob.sync().await.unwrap();
6008
6009            assert!(
6010                append.truncate(45).await.is_err(),
6011                "phase-1 partial write should fail"
6012            );
6013            assert_eq!(write_count.load(Ordering::SeqCst), 4);
6014            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6015
6016            // Recovery must use the older validated slot after the failed shrink.
6017            drop(blob);
6018            let (blob, size) = context
6019                .open("test_partition", b"same_page_shrink_fallback_slot")
6020                .await
6021                .unwrap();
6022            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6023                .await
6024                .unwrap();
6025            assert_eq!(append.size(), 50);
6026            let read = append.read_at(0, 50).await.unwrap().coalesce();
6027            assert_eq!(read.as_ref(), &data[..50]);
6028        });
6029    }
6030
6031    #[test]
6032    fn test_recovery_truncate_full_page_to_partial_reopens_at_shorter_size() {
6033        let executor = deterministic::Runner::default();
6034        executor.start(|context| async move {
6035            // Start with two full pages and choose a shorter tip inside the second page.
6036            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6037            let page_size = PAGE_SIZE.get() as u64;
6038            let target = page_size + 45;
6039            let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
6040
6041            let (blob, size) = context
6042                .open("test_partition", b"full_page_to_partial")
6043                .await
6044                .unwrap();
6045            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6046                .await
6047                .unwrap();
6048            (append, _) = append.append(&data).await.unwrap();
6049            append = append.sync().await.unwrap();
6050
6051            // Persist the shorter checksum before releasing the recovery owner.
6052            append = append.truncate(target).await.unwrap();
6053            drop(append);
6054
6055            // A fresh recovery must expose exactly the selected prefix.
6056            let (blob, size) = context
6057                .open("test_partition", b"full_page_to_partial")
6058                .await
6059                .unwrap();
6060            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6061                .await
6062                .unwrap();
6063            assert_eq!(append.size(), target);
6064            let read = append.read_at(0, target as usize).await.unwrap().coalesce();
6065            assert_eq!(read.as_ref(), &data[..target as usize]);
6066        });
6067    }
6068
6069    #[test]
6070    fn test_recovery_truncate_full_page_to_partial_survives_interrupted_crc_stage() {
6071        let executor = deterministic::Runner::default();
6072        executor.start(|context| async move {
6073            // Persist three full pages so truncation first removes the physical suffix.
6074            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6075            let page_size = PAGE_SIZE.get() as u64;
6076            let target = page_size + 45;
6077            let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
6078
6079            let (blob, size) = context
6080                .open("test_partition", b"full_page_to_partial_interrupted")
6081                .await
6082                .unwrap();
6083            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6084                .await
6085                .unwrap();
6086            (append, _) = append.append(&data).await.unwrap();
6087            append = append.sync().await.unwrap();
6088            drop(append);
6089
6090            // Tear the shorter checksum after the physical resize has completed.
6091            let (blob, size) = context
6092                .open("test_partition", b"full_page_to_partial_interrupted")
6093                .await
6094                .unwrap();
6095            let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
6096            let write_count = faulty_blob.write_count();
6097            let failed_write_len = faulty_blob.failed_write_len();
6098            let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
6099                .await
6100                .unwrap();
6101
6102            assert!(
6103                append.truncate(target).await.is_err(),
6104                "phase-1 partial write should fail"
6105            );
6106            assert_eq!(write_count.load(Ordering::SeqCst), 1);
6107            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6108
6109            // Recovery must retain the two full pages left by the completed resize.
6110            let (blob, size) = context
6111                .open("test_partition", b"full_page_to_partial_interrupted")
6112                .await
6113                .unwrap();
6114            let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6115                .await
6116                .unwrap();
6117            assert_eq!(append.size(), page_size * 2);
6118            let read = append
6119                .read_at(0, (page_size * 2) as usize)
6120                .await
6121                .unwrap()
6122                .coalesce();
6123            assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
6124        });
6125    }
6126
6127    #[test]
6128    fn test_recovery_truncate_same_page_shrink_survives_interrupted_length_invalidation() {
6129        let executor = deterministic::Runner::default();
6130        executor.start(|context| async move {
6131            // Use lengths above 255 to expose a one-byte tear in old-slot invalidation.
6132            const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
6133            const LARGE_BUFFER_SIZE: usize = 1_200;
6134
6135            let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
6136            let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
6137
6138            let (blob, size) = context
6139                .open(
6140                    "test_partition",
6141                    b"same_page_shrink_interrupted_len_invalidation",
6142                )
6143                .await
6144                .unwrap();
6145            let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
6146                .await
6147                .unwrap();
6148
6149            // Put the old authoritative CRC in slot 1, so the shorter CRC will be staged in slot 0.
6150            (append, _) = append.append(&data[..255]).await.unwrap();
6151            append = append.sync().await.unwrap();
6152            (append, _) = append.append(&data[255..]).await.unwrap();
6153            append = append.sync().await.unwrap();
6154            drop(append);
6155
6156            // Tear invalidation of the old slot after the shorter slot is durable.
6157            let (blob, size) = context
6158                .open(
6159                    "test_partition",
6160                    b"same_page_shrink_interrupted_len_invalidation",
6161                )
6162                .await
6163                .unwrap();
6164            let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
6165            let write_count = faulty_blob.write_count();
6166            let failed_write_len = faulty_blob.failed_write_len();
6167            let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
6168                .await
6169                .unwrap();
6170
6171            assert!(
6172                append.truncate(40).await.is_err(),
6173                "old-slot length invalidation should fail"
6174            );
6175            assert_eq!(write_count.load(Ordering::SeqCst), 3);
6176            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6177
6178            // Recovery must select the durable shorter slot despite the torn invalidation.
6179            let (blob, size) = context
6180                .open(
6181                    "test_partition",
6182                    b"same_page_shrink_interrupted_len_invalidation",
6183                )
6184                .await
6185                .unwrap();
6186            let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
6187                .await
6188                .unwrap();
6189            assert_eq!(append.size(), 40);
6190            let read = append.read_at(0, 40).await.unwrap().coalesce();
6191            assert_eq!(read.as_ref(), &data[..40]);
6192        });
6193    }
6194
6195    #[test_traced("DEBUG")]
6196    fn test_recovery_truncate_partial_shrink_without_physical_resize_uses_range_sync() {
6197        let executor = deterministic::Runner::default();
6198        executor.start(|_: deterministic::Context| async move {
6199            let blob = SyncTrackingBlob::new();
6200            let blob = Arc::new(blob);
6201            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6202            let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6203                .await
6204                .unwrap();
6205            append = append.sync().await.unwrap();
6206
6207            let data = vec![5u8; PAGE_SIZE.get() as usize];
6208            (append, _) = append.append(&data).await.unwrap();
6209            append = append.sync().await.unwrap();
6210
6211            // Shrinking within the same physical page only rewrites CRC metadata.
6212            append = append.truncate(50).await.unwrap();
6213            append.sync().await.unwrap();
6214
6215            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6216            assert_eq!(writes, 4);
6217            assert_eq!(full_syncs, 1);
6218            assert_eq!(range_syncs, 4);
6219        });
6220    }
6221
6222    #[test_traced("DEBUG")]
6223    fn test_recovery_truncate_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
6224        let executor = deterministic::Runner::default();
6225        executor.start(|_: deterministic::Context| async move {
6226            let blob = SyncTrackingBlob::new();
6227            let blob = Arc::new(blob);
6228            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6229            let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6230                .await
6231                .unwrap();
6232            append = append.sync().await.unwrap();
6233
6234            let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
6235            (append, _) = append.append(&data).await.unwrap();
6236            append = append.sync().await.unwrap();
6237
6238            // Shrinking from two physical pages to one partial page must also make the resize
6239            // durable.
6240            append = append.truncate(50).await.unwrap();
6241            append = append.sync().await.unwrap();
6242
6243            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6244            assert_eq!(writes, 4);
6245            assert_eq!(full_syncs, 2);
6246            assert_eq!(range_syncs, 3);
6247
6248            // Once the resize barrier is cleared, the next single flush can use range sync again.
6249            (append, _) = append.append(b"x").await.unwrap();
6250            append = append.sync().await.unwrap();
6251
6252            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6253            assert_eq!(writes, 5);
6254            assert_eq!(full_syncs, 2);
6255            assert_eq!(range_syncs, 4);
6256
6257            let mut expected = data[..50].to_vec();
6258            expected.push(b'x');
6259            let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
6260            assert_eq!(read.as_ref(), expected.as_slice());
6261        });
6262    }
6263
6264    #[test_traced("DEBUG")]
6265    fn test_recovery_truncate_page_boundary_shrink_uses_full_sync() {
6266        let executor = deterministic::Runner::default();
6267        executor.start(|_: deterministic::Context| async move {
6268            let blob = SyncTrackingBlob::new();
6269            let blob = Arc::new(blob);
6270            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6271            let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6272                .await
6273                .unwrap();
6274            append = append.sync().await.unwrap();
6275
6276            // Start with two durable full pages. After clearing the wrapper barrier, the data sync
6277            // can persist them with one range-sync write.
6278            let page_size = PAGE_SIZE.get() as usize;
6279            let data = vec![11u8; page_size * 2];
6280            (append, _) = append.append(&data).await.unwrap();
6281            append = append.sync().await.unwrap();
6282
6283            // Shrinking to a page boundary resizes the blob but does not rewrite CRC metadata.
6284            // Only the resize needs a full sync, which the truncation issues itself.
6285            append = append.truncate(PAGE_SIZE.get() as u64).await.unwrap();
6286            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6287            assert_eq!(writes, 1);
6288            assert_eq!(full_syncs, 2);
6289            assert_eq!(range_syncs, 1);
6290
6291            // Nothing is left pending, so a later sync adds no barrier.
6292            append.sync().await.unwrap();
6293            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6294            assert_eq!(writes, 1);
6295            assert_eq!(full_syncs, 2);
6296            assert_eq!(range_syncs, 1);
6297
6298            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6299            let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
6300                .await
6301                .unwrap();
6302            assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
6303            let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
6304            assert_eq!(read.as_ref(), &data[..page_size]);
6305        });
6306    }
6307
6308    #[test]
6309    fn test_recovery_partial_tail_append_and_truncate() {
6310        let executor = deterministic::Runner::default();
6311        executor.start(|context| async move {
6312            const PAGE_SIZE: NonZeroU16 = NZU16!(64);
6313            const BUFFER_SIZE: usize = 256;
6314
6315            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(4));
6316
6317            let (blob, size) = context
6318                .open("test_partition", b"partial_tail_test")
6319                .await
6320                .unwrap();
6321
6322            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6323                .await
6324                .unwrap();
6325
6326            // Write some initial data.
6327            (append, _) = append.append(&[1, 2, 3, 4, 5]).await.unwrap();
6328            append = append.sync().await.unwrap();
6329            assert_eq!(append.size(), 5);
6330            drop(append);
6331
6332            let (blob, size) = context
6333                .open("test_partition", b"partial_tail_test")
6334                .await
6335                .unwrap();
6336
6337            let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6338                .await
6339                .unwrap();
6340            assert_eq!(append.size(), 5);
6341
6342            (append, _) = append.append(&[6, 7, 8]).await.unwrap();
6343            append = append.truncate(6).await.unwrap();
6344            append = append.sync().await.unwrap();
6345
6346            let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
6347            assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
6348        });
6349    }
6350
6351    #[test]
6352    fn test_corrupted_crc_len_too_large() {
6353        let executor = deterministic::Runner::default();
6354        executor.start(|context| async move {
6355            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6356            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
6357
6358            // Step 1: Create blob with valid data
6359            let (blob, size) = context
6360                .open("test_partition", b"crc_len_test")
6361                .await
6362                .unwrap();
6363
6364            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
6365                .await
6366                .unwrap();
6367
6368            (append, _) = append.append(&[0x42; 50]).await.unwrap();
6369            append = append.sync().await.unwrap();
6370            drop(append);
6371
6372            // Step 2: Corrupt the CRC record to have len > page_size
6373            let (blob, size) = context
6374                .open("test_partition", b"crc_len_test")
6375                .await
6376                .unwrap();
6377            assert_eq!(size as usize, physical_page_size);
6378
6379            // CRC record is at the end of the physical page
6380            let crc_offset = PAGE_SIZE.get() as u64;
6381
6382            // Create a CRC record with len1 = 0xFFFF (65535), which is >> page_size (103)
6383            // Format: [len1_hi, len1_lo, crc1 (4 bytes), len2_hi, len2_lo, crc2 (4 bytes)]
6384            let bad_crc_record: [u8; 12] = [
6385                0xFF, 0xFF, // len1 = 65535 (way too large)
6386                0xDE, 0xAD, 0xBE, 0xEF, // crc1 (garbage)
6387                0x00, 0x00, // len2 = 0
6388                0x00, 0x00, 0x00, 0x00, // crc2 = 0
6389            ];
6390            blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
6391                .await
6392                .unwrap();
6393            blob.sync().await.unwrap();
6394
6395            // Step 3: Opening the blob should NOT panic
6396            let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
6397
6398            // Either returns InvalidChecksum error OR truncates the corrupted data
6399            // (both are acceptable behaviors, but panicking is NOT acceptable)
6400            match result {
6401                Ok(append) => {
6402                    // If it opens successfully, the corrupted page should have been truncated
6403                    let recovered_size = append.size();
6404                    assert_eq!(
6405                        recovered_size, 0,
6406                        "Corrupted page should be truncated, size should be 0"
6407                    );
6408                }
6409                Err(e) => {
6410                    // Error is also acceptable
6411                    assert!(
6412                        matches!(e, crate::Error::InvalidChecksum),
6413                        "Expected InvalidChecksum error, got: {:?}",
6414                        e
6415                    );
6416                }
6417            }
6418        });
6419    }
6420
6421    #[test]
6422    fn test_corrupted_crc_both_slots_len_too_large() {
6423        let executor = deterministic::Runner::default();
6424        executor.start(|context| async move {
6425            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6426
6427            // Step 1: Create blob with valid data
6428            let (blob, size) = context
6429                .open("test_partition", b"crc_both_bad")
6430                .await
6431                .unwrap();
6432
6433            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
6434                .await
6435                .unwrap();
6436
6437            (append, _) = append.append(&[0x42; 50]).await.unwrap();
6438            append = append.sync().await.unwrap();
6439            drop(append);
6440
6441            // Step 2: Corrupt BOTH CRC slots to have len > page_size
6442            let (blob, size) = context
6443                .open("test_partition", b"crc_both_bad")
6444                .await
6445                .unwrap();
6446
6447            let crc_offset = PAGE_SIZE.get() as u64;
6448
6449            // Both slots have len > page_size
6450            let bad_crc_record: [u8; 12] = [
6451                0x01, 0x00, // len1 = 256 (> 103)
6452                0xDE, 0xAD, 0xBE, 0xEF, // crc1 (garbage)
6453                0x02, 0x00, // len2 = 512 (> 103)
6454                0xCA, 0xFE, 0xBA, 0xBE, // crc2 (garbage)
6455            ];
6456            blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
6457                .await
6458                .unwrap();
6459            blob.sync().await.unwrap();
6460
6461            // Step 3: Opening should NOT panic
6462            let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
6463
6464            match result {
6465                Ok(append) => {
6466                    // Corrupted page truncated
6467                    assert_eq!(append.size(), 0);
6468                }
6469                Err(e) => {
6470                    assert!(
6471                        matches!(e, crate::Error::InvalidChecksum),
6472                        "Expected InvalidChecksum, got: {:?}",
6473                        e
6474                    );
6475                }
6476            }
6477        });
6478    }
6479
6480    /// Readers observe buffered (not yet flushed) bytes through both async and sync read paths.
6481    #[test_traced("DEBUG")]
6482    fn test_reader_sees_buffered_bytes() {
6483        let executor = deterministic::Runner::default();
6484        executor.start(|context: deterministic::Context| async move {
6485            let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
6486            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6487            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
6488                .await
6489                .unwrap();
6490
6491            let data: Vec<u8> = (0u8..50).collect();
6492            (writer, _) = writer.append(&data).await.unwrap();
6493
6494            // No flush or sync has happened; reads must still see the buffered bytes.
6495            assert_eq!(writer.size(), 50);
6496            let read = writer.read_at(0, 50).await.unwrap().coalesce();
6497            assert_eq!(read.as_ref(), data.as_slice());
6498
6499            let mut buf = vec![0u8; 10];
6500            assert!(writer.try_read_sync_into(&mut buf, 20));
6501            assert_eq!(buf, data[20..30]);
6502        });
6503    }
6504
6505    /// Recovery rejects reads past the selected end and preserves the retained bytes.
6506    #[test_traced("DEBUG")]
6507    fn test_recovery_read_past_truncated_end_errors_cleanly() {
6508        let executor = deterministic::Runner::default();
6509        executor.start(|context: deterministic::Context| async move {
6510            let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
6511            let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6512            let mut writer = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
6513                .await
6514                .unwrap();
6515
6516            let page_size = PAGE_SIZE.get() as usize;
6517            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
6518            (writer, _) = writer.append(&data).await.unwrap();
6519            writer = writer.sync().await.unwrap();
6520            assert_eq!(writer.size(), (page_size * 2) as u64);
6521
6522            // Shrink below the last observed size.
6523            let new_size = (page_size / 2) as u64;
6524            writer = writer.truncate(new_size).await.unwrap();
6525
6526            // Reads past the new size fail cleanly.
6527            let err = writer
6528                .read_at(new_size, page_size)
6529                .await
6530                .expect_err("read past truncated end must fail");
6531            assert!(matches!(err, Error::BlobInsufficientLength));
6532
6533            // Reads within the new size return the retained prefix, not stale cached bytes.
6534            let read = writer
6535                .read_at(0, new_size as usize)
6536                .await
6537                .unwrap()
6538                .coalesce();
6539            assert_eq!(read.as_ref(), &data[..new_size as usize]);
6540        });
6541    }
6542}