Skip to main content

ic_sqlite_vfs/sqlite_vfs/
stable_blob.rs

1//! Logical `/main.db` access backed by an in-place stable-memory image.
2//!
3//! SQLite sees a contiguous file. Internally, logical page `n` lives at
4//! `db_base_offset + n * SQLITE_PAGE_SIZE`.
5
6use crate::config::{SQLITE_PAGE_SIZE, STABLE_PAGE_SIZE, SUPERBLOCK_SIZE};
7use crate::sqlite_vfs::overlay::{self, Overlay};
8#[cfg(test)]
9use crate::stable::memory::ContextId;
10use crate::stable::memory::{self, StableMemoryError};
11use crate::stable::meta::{
12    fnv1a64, Superblock, ZeroExtent, CURRENT_LAYOUT_VERSION, FLAG_CHECKSUM_REFRESHING,
13    FLAG_CHECKSUM_STALE, FLAG_IMPORTING, MAX_ZERO_EXTENTS,
14};
15#[cfg(test)]
16use std::cell::RefCell;
17#[cfg(test)]
18use std::collections::BTreeMap;
19
20const CHECKSUM_CHUNK_LEN: u64 = 16 * 1024;
21#[cfg(test)]
22const FAR_PAGE_NO: u64 = 257;
23const FILE_PAGE_OFFSET_CACHE_CAPACITY: usize = 64;
24const FILE_PAGE_DATA_CACHE_CAPACITY: usize = 8;
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct ChecksumRefresh {
28    pub complete: bool,
29    pub checksum: u64,
30    pub scanned_bytes: u64,
31    pub db_size: u64,
32}
33
34#[allow(dead_code)]
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct StorageStats {
37    pub layout_version: u64,
38    pub page_count: u64,
39    pub page_table_bytes: u64,
40    pub zero_extent_count: u64,
41    pub active_bytes: u64,
42    pub allocated_bytes: u64,
43    pub orphan_bytes_estimate: u64,
44    pub orphan_ratio_basis_points: u64,
45    pub compact_recommended: bool,
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub(crate) enum StableBlobFailpoint {
50    OverlayWrite,
51    OverlayTruncate,
52    CommitCapacity,
53    CommitChunkWrite,
54    CommitSuperblockStore,
55}
56
57thread_local! {
58    #[cfg(test)]
59    static FAILPOINTS: RefCell<BTreeMap<ContextId, StableBlobFailpoint>> = const { RefCell::new(BTreeMap::new()) };
60}
61
62#[derive(Debug)]
63pub(crate) struct PageOffsetCache {
64    entries: Vec<(u64, u64)>,
65    pages: Vec<(u64, Vec<u8>)>,
66}
67
68impl PageOffsetCache {
69    pub(crate) fn new() -> Self {
70        Self {
71            entries: Vec::with_capacity(FILE_PAGE_OFFSET_CACHE_CAPACITY),
72            pages: Vec::new(),
73        }
74    }
75
76    fn get(&self, page_no: u64) -> Option<u64> {
77        match self.entries.as_slice() {
78            [] => None,
79            [(cached_page, physical)] => (*cached_page == page_no).then_some(*physical),
80            entries => {
81                for (cached_page, physical) in entries {
82                    if *cached_page == page_no {
83                        return Some(*physical);
84                    }
85                }
86                None
87            }
88        }
89    }
90
91    fn insert(&mut self, page_no: u64, physical: u64) {
92        if self.entries.len() == FILE_PAGE_OFFSET_CACHE_CAPACITY {
93            self.entries.remove(0);
94        }
95        self.entries.push((page_no, physical));
96    }
97
98    #[inline(always)]
99    fn copy_page_slice(&self, page_no: u64, in_page: usize, dst: &mut [u8]) -> bool {
100        if self.pages.is_empty() {
101            return false;
102        }
103        if self.pages.len() == 1 {
104            let (cached_page, page) = &self.pages[0];
105            if *cached_page == page_no {
106                let end = in_page + dst.len();
107                dst.copy_from_slice(&page[in_page..end]);
108                return true;
109            }
110            return false;
111        }
112        for (cached_page, page) in &self.pages {
113            if *cached_page == page_no {
114                let end = in_page + dst.len();
115                dst.copy_from_slice(&page[in_page..end]);
116                return true;
117            }
118        }
119        false
120    }
121
122    fn insert_page(&mut self, page_no: u64, page: Vec<u8>) {
123        if self.pages.len() == FILE_PAGE_DATA_CACHE_CAPACITY {
124            self.pages.remove(0);
125        }
126        self.pages.push((page_no, page));
127    }
128}
129
130#[cfg(test)]
131pub(crate) fn set_failpoint(failpoint: StableBlobFailpoint) {
132    if let Ok(context) = memory::active_context_id() {
133        FAILPOINTS.with(|slot| {
134            slot.borrow_mut().insert(context, failpoint);
135        });
136    }
137}
138
139#[cfg(test)]
140pub(crate) fn clear_failpoint() {
141    FAILPOINTS.with(|slot| slot.borrow_mut().clear());
142}
143
144pub(crate) fn ensure_current_layout() -> Result<(), StableMemoryError> {
145    let block = Superblock::load()?;
146    if block.layout_version == CURRENT_LAYOUT_VERSION {
147        return Ok(());
148    }
149    Err(StableMemoryError::UnsupportedLayoutVersion(
150        block.layout_version,
151    ))
152}
153
154pub(crate) fn begin_update() -> Result<u64, StableMemoryError> {
155    let block = Superblock::load()?;
156    if block.layout_version != CURRENT_LAYOUT_VERSION {
157        return Err(StableMemoryError::UnsupportedLayoutVersion(
158            block.layout_version,
159        ));
160    }
161    if block.is_importing() {
162        return Err(StableMemoryError::ImportAlreadyStarted);
163    }
164    overlay::begin(block.db_size)?;
165    Ok(block.db_size)
166}
167
168pub(crate) fn rollback_update() {
169    overlay::rollback();
170}
171
172pub(crate) fn commit_update() -> Result<(), StableMemoryError> {
173    let Some(overlay) = overlay::take() else {
174        return Ok(());
175    };
176    if overlay.is_empty() {
177        return Ok(());
178    }
179    commit_overlay(overlay, true)
180}
181
182pub(crate) fn read_at(offset: u64, dst: &mut [u8]) -> Result<bool, StableMemoryError> {
183    if let Some(result) = overlay::read_at(offset, dst) {
184        return result;
185    }
186    read_base_at(offset, dst)
187}
188
189pub(crate) fn read_base_at(offset: u64, dst: &mut [u8]) -> Result<bool, StableMemoryError> {
190    if dst.is_empty() {
191        return Ok(true);
192    }
193    let block = Superblock::load()?;
194    read_base_at_with_block(&block, offset, dst)
195}
196
197pub(crate) fn read_base_at_with_block(
198    block: &Superblock,
199    offset: u64,
200    dst: &mut [u8],
201) -> Result<bool, StableMemoryError> {
202    if dst.is_empty() {
203        return Ok(true);
204    }
205    if offset >= block.db_size {
206        dst.fill(0);
207        return Ok(false);
208    }
209    let requested = u64::try_from(dst.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
210    if requested <= block.db_size - offset {
211        read_logical_range(block, offset, dst)?;
212        return Ok(true);
213    }
214    let copied = requested.min(block.db_size - offset);
215    let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
216    read_logical_range(block, offset, &mut dst[..copied_len])?;
217    dst[copied_len..].fill(0);
218    Ok(copied == requested)
219}
220
221#[inline(always)]
222pub(crate) fn read_base_at_with_page_cache(
223    block: &Superblock,
224    offset: u64,
225    dst: &mut [u8],
226    page_offsets: &mut PageOffsetCache,
227) -> Result<bool, StableMemoryError> {
228    if dst.is_empty() {
229        return Ok(true);
230    }
231    if offset >= block.db_size {
232        dst.fill(0);
233        return Ok(false);
234    }
235    let requested = u64::try_from(dst.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
236    if requested <= block.db_size - offset {
237        read_logical_range_with_page_cache(block, offset, dst, page_offsets)?;
238        return Ok(true);
239    }
240    let copied = requested.min(block.db_size - offset);
241    let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
242    read_logical_range_with_page_cache(block, offset, &mut dst[..copied_len], page_offsets)?;
243    dst[copied_len..].fill(0);
244    Ok(copied == requested)
245}
246
247pub(crate) fn read_base_page(page_no: u64) -> Result<Vec<u8>, StableMemoryError> {
248    let block = Superblock::load()?;
249    let mut page = zero_page();
250    read_base_at_with_block(
251        &block,
252        page_no
253            .checked_mul(page_size())
254            .ok_or(StableMemoryError::OffsetOverflow)?,
255        &mut page,
256    )?;
257    Ok(page)
258}
259
260pub(crate) fn write_at(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
261    if let Some(result) = overlay::write_at(offset, bytes) {
262        hit_failpoint(StableBlobFailpoint::OverlayWrite)?;
263        return result;
264    }
265    if bytes.is_empty() {
266        return Ok(());
267    }
268    ensure_current_layout()?;
269    let mut direct = Overlay::new(Superblock::load()?.db_size);
270    direct.write_at(offset, bytes)?;
271    commit_overlay(direct, false)
272}
273
274pub(crate) fn truncate(size: u64) -> Result<(), StableMemoryError> {
275    if let Some(result) = overlay::truncate(size) {
276        hit_failpoint(StableBlobFailpoint::OverlayTruncate)?;
277        return result;
278    }
279    ensure_current_layout()?;
280    let mut direct = Overlay::new(Superblock::load()?.db_size);
281    direct.truncate(size)?;
282    if direct.is_empty() {
283        return Ok(());
284    }
285    commit_overlay(direct, false)
286}
287
288pub(crate) fn file_size() -> Result<u64, StableMemoryError> {
289    if let Some(size) = overlay::file_size() {
290        return Ok(size);
291    }
292    Ok(Superblock::load()?.db_size)
293}
294
295#[allow(dead_code)]
296pub fn export_chunk(offset: u64, len: u64) -> Result<Vec<u8>, StableMemoryError> {
297    reject_during_update()?;
298    let block = Superblock::load()?;
299    if offset >= block.db_size {
300        return Ok(Vec::new());
301    }
302    let copied = len.min(block.db_size - offset);
303    let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
304    let mut out = vec![0_u8; copied_len];
305    read_logical_range(&block, offset, &mut out)?;
306    Ok(out)
307}
308
309#[allow(dead_code)]
310pub fn import_chunk(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
311    reject_during_update()?;
312    let mut block = Superblock::load()?;
313    if !block.is_importing() {
314        return Err(StableMemoryError::ImportNotStarted);
315    }
316    let len = u64::try_from(bytes.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
317    if offset != block.import_written_until {
318        return Err(StableMemoryError::ImportOutOfOrder {
319            offset,
320            expected: block.import_written_until,
321        });
322    }
323    let end = checked_add(offset, len)?;
324    if end > block.import_total_size {
325        return Err(StableMemoryError::ImportOutOfBounds {
326            offset,
327            len,
328            db_size: block.import_total_size,
329        });
330    }
331    memory::write(import_offset(&block, offset)?, bytes)?;
332    block.import_written_until = end;
333    block.store()?;
334    Ok(())
335}
336
337#[allow(dead_code)]
338pub fn begin_import(total_size: u64, expected_checksum: u64) -> Result<(), StableMemoryError> {
339    reject_during_update()?;
340    let mut block = Superblock::load()?;
341    if block.is_importing() {
342        return Err(StableMemoryError::ImportAlreadyStarted);
343    }
344    let import_base_offset = append_base()?;
345    checked_add(import_base_offset, total_size)?;
346    block.flags |= FLAG_IMPORTING;
347    block.clear_checksum_refresh();
348    block.import_expected_checksum = expected_checksum;
349    block.import_written_until = 0;
350    block.import_total_size = total_size;
351    block.import_base_offset = import_base_offset;
352    block.store()?;
353    Ok(())
354}
355
356#[allow(dead_code)]
357pub fn finish_import() -> Result<(), StableMemoryError> {
358    reject_during_update()?;
359    let mut block = Superblock::load()?;
360    if !block.is_importing() {
361        return Err(StableMemoryError::ImportNotStarted);
362    }
363    if block.import_written_until != block.import_total_size {
364        return Err(StableMemoryError::ImportIncomplete {
365            written_until: block.import_written_until,
366            db_size: block.import_total_size,
367        });
368    }
369    let checksum = checksum_physical_range(block.import_base_offset, block.import_total_size)?;
370    if checksum != block.import_expected_checksum {
371        let expected = block.import_expected_checksum;
372        clear_import(&mut block)?;
373        return Err(StableMemoryError::ChecksumMismatch {
374            expected,
375            actual: checksum,
376        });
377    }
378    block.db_size = block.import_total_size;
379    block.db_base_offset = block.import_base_offset;
380    block.page_table_offset = 0;
381    block.page_count = page_count_for_size(block.db_size)?;
382    block.layout_version = CURRENT_LAYOUT_VERSION;
383    block.schema_version = 0;
384    block.flags &= !FLAG_IMPORTING;
385    block.flags &= !FLAG_CHECKSUM_STALE;
386    block.clear_zero_extents();
387    block.clear_checksum_refresh();
388    block.checksum = checksum;
389    block.import_expected_checksum = 0;
390    block.import_written_until = 0;
391    block.import_total_size = 0;
392    block.import_base_offset = 0;
393    block.store()?;
394    Ok(())
395}
396
397#[allow(dead_code)]
398pub fn cancel_import() -> Result<(), StableMemoryError> {
399    reject_during_update()?;
400    let mut block = Superblock::load()?;
401    if !block.is_importing() {
402        return Err(StableMemoryError::ImportNotStarted);
403    }
404    clear_import(&mut block)
405}
406
407pub fn refresh_checksum() -> Result<u64, StableMemoryError> {
408    reject_during_update()?;
409    let checksum = checksum()?;
410    let mut block = Superblock::load()?;
411    block.checksum = checksum;
412    block.flags &= !FLAG_CHECKSUM_STALE;
413    block.clear_checksum_refresh();
414    block.store()?;
415    Ok(checksum)
416}
417
418pub fn refresh_checksum_chunk(max_bytes: u64) -> Result<ChecksumRefresh, StableMemoryError> {
419    reject_during_update()?;
420    if max_bytes == 0 {
421        return Err(StableMemoryError::ChecksumRefreshChunkEmpty);
422    }
423
424    let mut block = Superblock::load()?;
425    if block.is_importing() {
426        return Err(StableMemoryError::ImportAlreadyStarted);
427    }
428    if !block.is_checksum_refreshing() {
429        block.flags |= FLAG_CHECKSUM_REFRESHING;
430        block.checksum_refresh_offset = 0;
431        block.checksum_refresh_hash = fnv1a64(&[]);
432        block.checksum_refresh_tx_id = block.last_tx_id;
433    }
434    if block.checksum_refresh_tx_id != block.last_tx_id {
435        block.clear_checksum_refresh();
436        block.store()?;
437        return refresh_checksum_chunk(max_bytes);
438    }
439
440    let start = block.checksum_refresh_offset;
441    let end = block.db_size.min(start.saturating_add(max_bytes));
442    let mut offset = start;
443    let mut hash = block.checksum_refresh_hash;
444    while offset < end {
445        let len = (end - offset).min(CHECKSUM_CHUNK_LEN);
446        let copied_len = usize::try_from(len).map_err(|_| StableMemoryError::OffsetOverflow)?;
447        let mut bytes = vec![0_u8; copied_len];
448        read_logical_range(&block, offset, &mut bytes)?;
449        hash = fold_fnv1a64(hash, &bytes);
450        offset += len;
451    }
452
453    block.checksum_refresh_offset = offset;
454    block.checksum_refresh_hash = hash;
455    if offset == block.db_size {
456        block.checksum = hash;
457        block.flags &= !FLAG_CHECKSUM_STALE;
458        block.clear_checksum_refresh();
459    }
460    let out = ChecksumRefresh {
461        complete: offset == block.db_size,
462        checksum: hash,
463        scanned_bytes: offset,
464        db_size: block.db_size,
465    };
466    block.store()?;
467    Ok(out)
468}
469
470pub fn checksum() -> Result<u64, StableMemoryError> {
471    reject_during_update()?;
472    let block = Superblock::load()?;
473    checksum_logical_range(&block, block.db_size)
474}
475
476#[allow(dead_code)]
477pub fn compact() -> Result<(), StableMemoryError> {
478    reject_during_update()?;
479    ensure_current_layout()?;
480    Ok(())
481}
482
483#[allow(dead_code)]
484pub fn storage_stats() -> Result<StorageStats, StableMemoryError> {
485    let block = Superblock::load()?;
486    ensure_current_layout()?;
487    let page_table_bytes = 0;
488    let zero_extent_count =
489        u64::try_from(block.zero_extent_count()).map_err(|_| StableMemoryError::OffsetOverflow)?;
490    let active_bytes = SUPERBLOCK_SIZE
491        .checked_add(block.db_size)
492        .ok_or(StableMemoryError::OffsetOverflow)?;
493    let allocated_bytes = memory::size_pages()
494        .checked_mul(STABLE_PAGE_SIZE)
495        .ok_or(StableMemoryError::OffsetOverflow)?;
496    let orphan_bytes_estimate = allocated_bytes.saturating_sub(active_bytes);
497    let orphan_ratio_basis_points = orphan_bytes_estimate
498        .saturating_mul(10_000)
499        .checked_div(active_bytes)
500        .unwrap_or(0);
501    Ok(StorageStats {
502        layout_version: block.layout_version,
503        page_count: active_page_count(&block)?,
504        page_table_bytes,
505        zero_extent_count,
506        active_bytes,
507        allocated_bytes,
508        orphan_bytes_estimate,
509        orphan_ratio_basis_points,
510        compact_recommended: false,
511    })
512}
513
514pub(crate) fn page_count_for_size(size: u64) -> Result<u64, StableMemoryError> {
515    Ok(size.div_ceil(page_size()))
516}
517
518fn commit_overlay(overlay: Overlay, advance_tx: bool) -> Result<(), StableMemoryError> {
519    hit_failpoint(StableBlobFailpoint::CommitCapacity)?;
520    let profile_enabled = commit_profile_enabled();
521    let profile_start = commit_profile_start(profile_enabled);
522    let block = Superblock::load()?;
523    commit_profile_record_load(profile_start);
524    commit_overlay_in_place(&block, overlay, advance_tx, profile_enabled)
525}
526
527fn commit_overlay_in_place(
528    block: &Superblock,
529    overlay: Overlay,
530    advance_tx: bool,
531    profile_enabled: bool,
532) -> Result<(), StableMemoryError> {
533    let overlay_size = overlay.size();
534    let final_page_count = page_count_for_size(overlay_size)?;
535    debug_assert!(overlay
536        .dirty_pages()
537        .iter()
538        .all(|(page_no, _)| *page_no < final_page_count));
539    let dirty_pages = overlay.dirty_pages();
540    let zero_extents = zero_extents_after_commit(block, &overlay, final_page_count)?;
541
542    let mut required_end = checked_add(block.db_base_offset, overlay_size)?;
543    for (page_no, _) in dirty_pages {
544        if *page_no >= final_page_count {
545            continue;
546        }
547        required_end = required_end.max(checked_add(
548            page_physical_offset(block, *page_no)?,
549            page_size(),
550        )?);
551    }
552    let profile_start = commit_profile_start(profile_enabled);
553    memory::ensure_capacity(required_end)?;
554    commit_profile_record_capacity(profile_start);
555
556    let profile_start = commit_profile_start(profile_enabled);
557    for (page_no, page) in dirty_pages {
558        if *page_no >= final_page_count {
559            continue;
560        }
561        hit_failpoint(StableBlobFailpoint::CommitChunkWrite)?;
562        write_commit_page(
563            page_physical_offset(block, *page_no)?,
564            page,
565            profile_enabled,
566        )?;
567    }
568    commit_profile_record_page_write(profile_start);
569
570    if let Err(error) = hit_failpoint(StableBlobFailpoint::CommitSuperblockStore) {
571        panic!("failed to publish in-place commit after page writes: {error}");
572    }
573    let profile_start = commit_profile_start(profile_enabled);
574    let result =
575        store_commit_db_image(advance_tx, block.db_base_offset, overlay_size, zero_extents);
576    commit_profile_record_superblock_store(profile_start);
577    if let Err(error) = result {
578        panic!("failed to publish in-place commit after page writes: {error}");
579    }
580    Ok(())
581}
582
583#[cfg(any(test, debug_assertions, feature = "bench-profile"))]
584#[inline(always)]
585fn commit_profile_enabled() -> bool {
586    crate::read_metrics::metrics_enabled()
587}
588
589#[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
590#[inline(always)]
591fn commit_profile_enabled() -> bool {
592    false
593}
594
595#[cfg(any(test, debug_assertions, feature = "bench-profile"))]
596#[inline(always)]
597fn commit_profile_start(enabled: bool) -> Option<u64> {
598    if enabled {
599        Some(crate::read_metrics::instruction_counter())
600    } else {
601        None
602    }
603}
604
605#[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
606#[inline(always)]
607fn commit_profile_start(_enabled: bool) -> Option<u64> {
608    None
609}
610
611macro_rules! commit_profile_recorder {
612    ($name:ident, $record:ident) => {
613        #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
614        #[inline(always)]
615        fn $name(start: Option<u64>) {
616            if let Some(start) = start {
617                crate::read_metrics::$record(
618                    crate::read_metrics::instruction_counter().saturating_sub(start),
619                );
620            }
621        }
622
623        #[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
624        #[inline(always)]
625        fn $name(_start: Option<u64>) {}
626    };
627}
628
629commit_profile_recorder!(commit_profile_record_capacity, record_commit_capacity);
630commit_profile_recorder!(commit_profile_record_load, record_commit_load);
631commit_profile_recorder!(commit_profile_record_page_write, record_commit_page_write);
632commit_profile_recorder!(
633    commit_profile_record_superblock_store,
634    record_commit_superblock_store
635);
636
637#[inline(always)]
638fn write_commit_page(
639    offset: u64,
640    page: &[u8],
641    profile_enabled: bool,
642) -> Result<(), StableMemoryError> {
643    if profile_enabled {
644        memory::write_prechecked(offset, page)
645    } else {
646        memory::write_prechecked_unmetered(offset, page)
647    }
648}
649
650fn store_commit_db_image(
651    advance_tx: bool,
652    db_base_offset: u64,
653    overlay_size: u64,
654    zero_extents: Vec<ZeroExtent>,
655) -> Result<(), StableMemoryError> {
656    match advance_tx {
657        true => Superblock::commit_db_image(db_base_offset, overlay_size, zero_extents),
658        false => Superblock::store_db_image_without_tx(db_base_offset, overlay_size, zero_extents),
659    }
660}
661
662fn zero_extents_after_commit(
663    block: &Superblock,
664    overlay: &Overlay,
665    final_page_count: u64,
666) -> Result<Vec<ZeroExtent>, StableMemoryError> {
667    let mut extents = block.zero_extents().to_vec();
668    let old_page_count = page_count_for_size(block.db_size)?;
669    let dirty_page_slack = overlay
670        .dirty_pages()
671        .iter()
672        .filter(|(page_no, _)| *page_no < final_page_count)
673        .count();
674    let temporary_limit = MAX_ZERO_EXTENTS.checked_add(dirty_page_slack).ok_or(
675        StableMemoryError::ZeroExtentLimitExceeded {
676            limit: MAX_ZERO_EXTENTS,
677        },
678    )?;
679    // A commit can temporarily exceed MAX_ZERO_EXTENTS while combining growth,
680    // truncate masks, and dirty-page materialization. Bound that temporary
681    // slack to pages that can actually remove zero-mask metadata later.
682    if overlay.size() > block.db_size {
683        let first_new_page = page_count_for_size(block.db_size)?;
684        add_zero_extent_without_limit(&mut extents, first_new_page, final_page_count);
685        enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
686    }
687    if overlay.size() < block.db_size {
688        let first_zero_page = page_count_for_size(overlay.size())?;
689        add_zero_extent_without_limit(&mut extents, first_zero_page, old_page_count);
690        enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
691    }
692    for extent in overlay.zero_extents() {
693        add_zero_extent_without_limit(&mut extents, extent.start_page, extent.end_page);
694        enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
695    }
696    for (page_no, _) in overlay.dirty_pages() {
697        if *page_no < final_page_count {
698            subtract_zero_extent_without_limit(&mut extents, *page_no, page_no.saturating_add(1));
699        }
700    }
701    enforce_zero_extent_limit(&extents)?;
702    Ok(extents)
703}
704
705fn enforce_temporary_zero_extent_limit(
706    extents: &[ZeroExtent],
707    temporary_limit: usize,
708) -> Result<(), StableMemoryError> {
709    if extents.len() > temporary_limit {
710        return Err(StableMemoryError::ZeroExtentLimitExceeded {
711            limit: MAX_ZERO_EXTENTS,
712        });
713    }
714    Ok(())
715}
716
717#[cfg(test)]
718fn add_zero_extent(
719    extents: &mut Vec<ZeroExtent>,
720    start_page: u64,
721    end_page: u64,
722) -> Result<(), StableMemoryError> {
723    if start_page >= end_page {
724        return Ok(());
725    }
726    extents.push(ZeroExtent {
727        start_page,
728        end_page,
729    });
730    normalize_zero_extents(extents)
731}
732
733fn add_zero_extent_without_limit(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
734    if start_page >= end_page {
735        return;
736    }
737    extents.push(ZeroExtent {
738        start_page,
739        end_page,
740    });
741    normalize_zero_extents_without_limit(extents);
742}
743
744#[cfg(test)]
745fn subtract_zero_extent(
746    extents: &mut Vec<ZeroExtent>,
747    start_page: u64,
748    end_page: u64,
749) -> Result<(), StableMemoryError> {
750    if start_page >= end_page || extents.is_empty() {
751        return Ok(());
752    }
753    let mut next = Vec::with_capacity(extents.len() + 1);
754    for extent in extents.iter() {
755        if end_page <= extent.start_page || start_page >= extent.end_page {
756            next.push(extent.clone());
757            continue;
758        }
759        if extent.start_page < start_page {
760            next.push(ZeroExtent {
761                start_page: extent.start_page,
762                end_page: start_page,
763            });
764        }
765        if end_page < extent.end_page {
766            next.push(ZeroExtent {
767                start_page: end_page,
768                end_page: extent.end_page,
769            });
770        }
771    }
772    *extents = next;
773    normalize_zero_extents(extents)
774}
775
776fn subtract_zero_extent_without_limit(
777    extents: &mut Vec<ZeroExtent>,
778    start_page: u64,
779    end_page: u64,
780) {
781    if start_page >= end_page || extents.is_empty() {
782        return;
783    }
784    let mut next = Vec::with_capacity(extents.len() + 1);
785    for extent in extents.iter() {
786        if end_page <= extent.start_page || start_page >= extent.end_page {
787            next.push(extent.clone());
788            continue;
789        }
790        if extent.start_page < start_page {
791            next.push(ZeroExtent {
792                start_page: extent.start_page,
793                end_page: start_page,
794            });
795        }
796        if end_page < extent.end_page {
797            next.push(ZeroExtent {
798                start_page: end_page,
799                end_page: extent.end_page,
800            });
801        }
802    }
803    *extents = next;
804    normalize_zero_extents_without_limit(extents);
805}
806
807#[cfg(test)]
808fn normalize_zero_extents(extents: &mut Vec<ZeroExtent>) -> Result<(), StableMemoryError> {
809    normalize_zero_extents_without_limit(extents);
810    enforce_zero_extent_limit(extents)
811}
812
813fn normalize_zero_extents_without_limit(extents: &mut Vec<ZeroExtent>) {
814    extents.retain(|extent| extent.start_page < extent.end_page);
815    extents.sort_by_key(|extent| extent.start_page);
816    let mut normalized: Vec<ZeroExtent> = Vec::with_capacity(extents.len());
817    for extent in extents.drain(..) {
818        if let Some(last) = normalized.last_mut() {
819            if extent.start_page <= last.end_page {
820                last.end_page = last.end_page.max(extent.end_page);
821                continue;
822            }
823        }
824        normalized.push(extent);
825    }
826    *extents = normalized;
827}
828
829fn enforce_zero_extent_limit(extents: &[ZeroExtent]) -> Result<(), StableMemoryError> {
830    if extents.len() > MAX_ZERO_EXTENTS {
831        return Err(StableMemoryError::ZeroExtentLimitExceeded {
832            limit: MAX_ZERO_EXTENTS,
833        });
834    }
835    Ok(())
836}
837
838fn reject_during_update() -> Result<(), StableMemoryError> {
839    if overlay::is_active() {
840        Err(StableMemoryError::UpdateInProgress)
841    } else {
842        Ok(())
843    }
844}
845
846fn read_logical_range(
847    block: &Superblock,
848    offset: u64,
849    dst: &mut [u8],
850) -> Result<(), StableMemoryError> {
851    if dst.is_empty() {
852        return Ok(());
853    }
854    let in_page =
855        usize::try_from(offset % page_size()).map_err(|_| StableMemoryError::OffsetOverflow)?;
856    if dst.len() <= page_len() - in_page {
857        return read_logical_page_slice(block, offset / page_size(), in_page, dst);
858    }
859
860    let mut copied_total = 0_usize;
861    while copied_total < dst.len() {
862        let absolute = checked_add(
863            offset,
864            u64::try_from(copied_total).map_err(|_| StableMemoryError::OffsetOverflow)?,
865        )?;
866        let page_no = absolute / page_size();
867        let in_page = usize::try_from(absolute % page_size())
868            .map_err(|_| StableMemoryError::OffsetOverflow)?;
869        let copied = (page_len() - in_page).min(dst.len() - copied_total);
870        read_logical_page_slice(
871            block,
872            page_no,
873            in_page,
874            &mut dst[copied_total..copied_total + copied],
875        )?;
876        copied_total += copied;
877    }
878    Ok(())
879}
880
881fn read_logical_range_with_page_cache(
882    block: &Superblock,
883    offset: u64,
884    dst: &mut [u8],
885    page_offsets: &mut PageOffsetCache,
886) -> Result<(), StableMemoryError> {
887    let in_page =
888        usize::try_from(offset % page_size()).map_err(|_| StableMemoryError::OffsetOverflow)?;
889    if dst.len() <= page_len() - in_page {
890        return read_logical_page_slice_with_page_cache(
891            block,
892            offset / page_size(),
893            in_page,
894            dst,
895            page_offsets,
896        );
897    }
898
899    let mut copied_total = 0_usize;
900    while copied_total < dst.len() {
901        let absolute = checked_add(
902            offset,
903            u64::try_from(copied_total).map_err(|_| StableMemoryError::OffsetOverflow)?,
904        )?;
905        let page_no = absolute / page_size();
906        let in_page = usize::try_from(absolute % page_size())
907            .map_err(|_| StableMemoryError::OffsetOverflow)?;
908        let copied = (page_len() - in_page).min(dst.len() - copied_total);
909        read_logical_page_slice_with_page_cache(
910            block,
911            page_no,
912            in_page,
913            &mut dst[copied_total..copied_total + copied],
914            page_offsets,
915        )?;
916        copied_total += copied;
917    }
918    Ok(())
919}
920
921fn read_logical_page_slice(
922    block: &Superblock,
923    page_no: u64,
924    in_page: usize,
925    dst: &mut [u8],
926) -> Result<(), StableMemoryError> {
927    if page_is_zero_masked(block, page_no) {
928        dst.fill(0);
929        return Ok(());
930    }
931    let physical = page_offset_for(block, page_no)?;
932    if physical == 0 {
933        dst.fill(0);
934        return Ok(());
935    }
936    let stable_offset = checked_add(
937        physical,
938        u64::try_from(in_page).map_err(|_| StableMemoryError::OffsetOverflow)?,
939    )?;
940    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
941    crate::read_metrics::record_stable_data_read(dst.len());
942    memory::read_preallocated(stable_offset, dst)
943}
944
945#[inline(always)]
946fn read_logical_page_slice_with_page_cache(
947    block: &Superblock,
948    page_no: u64,
949    in_page: usize,
950    dst: &mut [u8],
951    page_offsets: &mut PageOffsetCache,
952) -> Result<(), StableMemoryError> {
953    if page_is_zero_masked(block, page_no) {
954        dst.fill(0);
955        return Ok(());
956    }
957    if dst.len() < page_len() && page_offsets.copy_page_slice(page_no, in_page, dst) {
958        return Ok(());
959    }
960    let physical = match page_offsets.get(page_no) {
961        Some(physical) => physical,
962        None => {
963            let physical = page_offset_for(block, page_no)?;
964            page_offsets.insert(page_no, physical);
965            physical
966        }
967    };
968    if physical == 0 {
969        dst.fill(0);
970        return Ok(());
971    }
972    if in_page == 0 && dst.len() == page_len() {
973        #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
974        crate::read_metrics::record_stable_data_read(dst.len());
975        return memory::read_preallocated(physical, dst);
976    }
977    if dst.len() < page_len() {
978        let mut page = zero_page();
979        #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
980        crate::read_metrics::record_stable_data_read(page.len());
981        memory::read_preallocated(physical, &mut page)?;
982        let end = in_page + dst.len();
983        dst.copy_from_slice(&page[in_page..end]);
984        page_offsets.insert_page(page_no, page);
985        return Ok(());
986    }
987    let stable_offset = checked_add(
988        physical,
989        u64::try_from(in_page).map_err(|_| StableMemoryError::OffsetOverflow)?,
990    )?;
991    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
992    crate::read_metrics::record_stable_data_read(dst.len());
993    memory::read_preallocated(stable_offset, dst)
994}
995
996fn page_offset_for(block: &Superblock, page_no: u64) -> Result<u64, StableMemoryError> {
997    if page_no >= active_page_count(block)? {
998        return Ok(0);
999    }
1000    page_physical_offset(block, page_no)
1001}
1002
1003fn page_is_zero_masked(block: &Superblock, page_no: u64) -> bool {
1004    block
1005        .zero_extents()
1006        .iter()
1007        .any(|extent| page_no >= extent.start_page && page_no < extent.end_page)
1008}
1009
1010fn page_physical_offset(block: &Superblock, page_no: u64) -> Result<u64, StableMemoryError> {
1011    checked_add(
1012        block.db_base_offset,
1013        page_no
1014            .checked_mul(page_size())
1015            .ok_or(StableMemoryError::OffsetOverflow)?,
1016    )
1017}
1018
1019fn checksum_logical_range(block: &Superblock, len: u64) -> Result<u64, StableMemoryError> {
1020    let mut offset = 0_u64;
1021    let mut hash = fnv1a64(&[]);
1022    while offset < len {
1023        let chunk_len = (len - offset).min(CHECKSUM_CHUNK_LEN);
1024        let copied_len =
1025            usize::try_from(chunk_len).map_err(|_| StableMemoryError::OffsetOverflow)?;
1026        let mut bytes = vec![0_u8; copied_len];
1027        read_logical_range(block, offset, &mut bytes)?;
1028        hash = fold_fnv1a64(hash, &bytes);
1029        offset += chunk_len;
1030    }
1031    Ok(hash)
1032}
1033
1034#[allow(dead_code)]
1035fn checksum_physical_range(base_offset: u64, len: u64) -> Result<u64, StableMemoryError> {
1036    let mut offset = 0_u64;
1037    let mut hash = fnv1a64(&[]);
1038    while offset < len {
1039        let chunk_len = (len - offset).min(CHECKSUM_CHUNK_LEN);
1040        let copied_len =
1041            usize::try_from(chunk_len).map_err(|_| StableMemoryError::OffsetOverflow)?;
1042        let mut bytes = vec![0_u8; copied_len];
1043        memory::read_preallocated(checked_add(base_offset, offset)?, &mut bytes)?;
1044        hash = fold_fnv1a64(hash, &bytes);
1045        offset += chunk_len;
1046    }
1047    Ok(hash)
1048}
1049
1050#[allow(dead_code)]
1051fn clear_import(block: &mut Superblock) -> Result<(), StableMemoryError> {
1052    block.flags &= !FLAG_IMPORTING;
1053    block.import_expected_checksum = 0;
1054    block.import_written_until = 0;
1055    block.import_total_size = 0;
1056    block.import_base_offset = 0;
1057    block.store()?;
1058    Ok(())
1059}
1060
1061#[allow(dead_code)]
1062fn import_offset(block: &Superblock, offset: u64) -> Result<u64, StableMemoryError> {
1063    checked_add(block.import_base_offset, offset)
1064}
1065
1066fn active_page_count(block: &Superblock) -> Result<u64, StableMemoryError> {
1067    page_count_for_size(block.db_size)
1068}
1069
1070#[allow(dead_code)]
1071fn append_base() -> Result<u64, StableMemoryError> {
1072    // Fresh bases must append at the high-water mark; never reuse stale physical gaps.
1073    memory::size_pages()
1074        .checked_mul(STABLE_PAGE_SIZE)
1075        .ok_or(StableMemoryError::OffsetOverflow)
1076}
1077
1078fn page_size() -> u64 {
1079    u64::from(SQLITE_PAGE_SIZE)
1080}
1081
1082fn page_len() -> usize {
1083    usize::try_from(SQLITE_PAGE_SIZE).expect("SQLite page size fits usize")
1084}
1085
1086fn zero_page() -> Vec<u8> {
1087    vec![0_u8; page_len()]
1088}
1089
1090fn checked_add(left: u64, right: u64) -> Result<u64, StableMemoryError> {
1091    left.checked_add(right)
1092        .ok_or(StableMemoryError::OffsetOverflow)
1093}
1094
1095fn fold_fnv1a64(mut hash: u64, bytes: &[u8]) -> u64 {
1096    for byte in bytes {
1097        hash ^= u64::from(*byte);
1098        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1099    }
1100    hash
1101}
1102
1103#[cfg(test)]
1104fn hit_failpoint(failpoint: StableBlobFailpoint) -> Result<(), StableMemoryError> {
1105    let Ok(context) = memory::active_context_id() else {
1106        return Ok(());
1107    };
1108    FAILPOINTS.with(|slot| {
1109        let mut slot = slot.borrow_mut();
1110        if slot.get(&context).copied() == Some(failpoint) {
1111            slot.remove(&context);
1112            Err(StableMemoryError::Failpoint(failpoint.name()))
1113        } else {
1114            Ok(())
1115        }
1116    })
1117}
1118
1119#[cfg(not(test))]
1120fn hit_failpoint(_failpoint: StableBlobFailpoint) -> Result<(), StableMemoryError> {
1121    Ok(())
1122}
1123
1124#[cfg(test)]
1125impl StableBlobFailpoint {
1126    fn name(self) -> &'static str {
1127        match self {
1128            Self::OverlayWrite => "before overlay write",
1129            Self::OverlayTruncate => "before overlay truncate",
1130            Self::CommitCapacity => "before commit capacity",
1131            Self::CommitChunkWrite => "before commit page write",
1132            Self::CommitSuperblockStore => "before commit superblock store",
1133        }
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140    use proptest::prelude::*;
1141    use proptest::test_runner::{Config, TestRunner};
1142    use std::collections::BTreeSet;
1143
1144    #[test]
1145    fn layout_math_matches_expected_boundaries() {
1146        assert_eq!(page_count_for_size(0).unwrap(), 0);
1147        assert_eq!(page_count_for_size(1).unwrap(), 1);
1148        assert_eq!(page_count_for_size(page_size()).unwrap(), 1);
1149        assert_eq!(page_count_for_size(page_size() + 1).unwrap(), 2);
1150    }
1151
1152    #[test]
1153    fn layout_math_rejects_u64_max_overflow_boundaries() {
1154        assert!(matches!(
1155            checked_add(u64::MAX, 1),
1156            Err(StableMemoryError::OffsetOverflow)
1157        ));
1158
1159        let mut block = Superblock::fresh();
1160        block.import_base_offset = u64::MAX;
1161        assert!(matches!(
1162            import_offset(&block, 1),
1163            Err(StableMemoryError::OffsetOverflow)
1164        ));
1165
1166        block.db_base_offset = u64::MAX - page_size() + 1;
1167        assert!(matches!(
1168            page_physical_offset(&block, 1),
1169            Err(StableMemoryError::OffsetOverflow)
1170        ));
1171    }
1172
1173    #[test]
1174    fn pbt_layout_math_matches_verus_model() {
1175        let mut runner = TestRunner::new(Config {
1176            cases: 512,
1177            ..Config::default()
1178        });
1179
1180        runner
1181            .run(
1182                &(boundary_size_strategy(), boundary_page_strategy()),
1183                |(size, page_no)| {
1184                    let page_count = page_count_for_size(size).unwrap();
1185                    let page_size = u128::from(page_size());
1186                    if size == 0 {
1187                        prop_assert_eq!(page_count, 0);
1188                    } else {
1189                        prop_assert!(u128::from(page_count - 1) * page_size < u128::from(size));
1190                        prop_assert!(u128::from(size) <= u128::from(page_count) * page_size);
1191                    }
1192
1193                    let mut block = Superblock::fresh();
1194                    block.db_base_offset = SUPERBLOCK_SIZE;
1195                    let expected = u128::from(SUPERBLOCK_SIZE) + u128::from(page_no) * page_size;
1196                    match page_physical_offset(&block, page_no) {
1197                        Ok(physical) => prop_assert_eq!(u128::from(physical), expected),
1198                        Err(StableMemoryError::OffsetOverflow) => {
1199                            prop_assert!(expected > u128::from(u64::MAX));
1200                        }
1201                        Err(error) => return Err(TestCaseError::fail(error.to_string())),
1202                    }
1203                    Ok(())
1204                },
1205            )
1206            .unwrap();
1207    }
1208
1209    fn boundary_size_strategy() -> impl Strategy<Value = u64> {
1210        let page = page_size();
1211        let far_page_bytes = FAR_PAGE_NO * page;
1212        prop_oneof![
1213            any::<u64>(),
1214            prop::sample::select(boundary_values(&[
1215                0,
1216                1,
1217                page - 1,
1218                page,
1219                page + 1,
1220                far_page_bytes - 1,
1221                far_page_bytes,
1222                far_page_bytes + 1,
1223                u64::MAX,
1224            ])),
1225        ]
1226    }
1227
1228    fn boundary_page_strategy() -> impl Strategy<Value = u64> {
1229        prop_oneof![
1230            any::<u64>(),
1231            prop::sample::select(boundary_values(&[
1232                0,
1233                1,
1234                FAR_PAGE_NO - 1,
1235                FAR_PAGE_NO,
1236                FAR_PAGE_NO + 1,
1237                u64::MAX,
1238            ])),
1239        ]
1240    }
1241
1242    fn boundary_values(values: &[u64]) -> Vec<u64> {
1243        values
1244            .iter()
1245            .flat_map(|value| [value.saturating_sub(1), *value, value.saturating_add(1)])
1246            .collect()
1247    }
1248
1249    #[test]
1250    fn fnv_fold_matches_one_pass_for_multiple_partitions() {
1251        let bytes: Vec<u8> = (0..97)
1252            .map(|index| (index as u8).wrapping_mul(37).wrapping_add(11))
1253            .collect();
1254        let expected = fnv1a64(&bytes);
1255
1256        for split in [0_usize, 1, 2, 7, 31, 64, bytes.len()] {
1257            let split = split.min(bytes.len());
1258            let mut hash = fnv1a64(&[]);
1259            hash = fold_fnv1a64(hash, &bytes[..split]);
1260            hash = fold_fnv1a64(hash, &bytes[split..]);
1261            assert_eq!(hash, expected);
1262        }
1263
1264        let mut hash = fnv1a64(&[]);
1265        for chunk in bytes.chunks(13) {
1266            hash = fold_fnv1a64(hash, chunk);
1267        }
1268        assert_eq!(hash, expected);
1269    }
1270
1271    #[test]
1272    #[serial_test::serial]
1273    fn in_place_commit_keeps_dirty_page_offsets_stable() {
1274        crate::stable::memory::reset_for_tests();
1275        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1276
1277        let page_zero = vec![1_u8; page_len()];
1278        let page_later = vec![2_u8; page_len()];
1279        let later_page_no = FAR_PAGE_NO;
1280        write_at(0, &page_zero).unwrap();
1281        write_at(later_page_no * page_size(), &page_later).unwrap();
1282
1283        let block = Superblock::load().unwrap();
1284        let old_page_zero_offset = page_physical_offset(&block, 0).unwrap();
1285        let old_later_offset = page_physical_offset(&block, later_page_no).unwrap();
1286
1287        let updated_page_zero = vec![3_u8; page_len()];
1288        write_at(0, &updated_page_zero).unwrap();
1289        let updated_block = Superblock::load().unwrap();
1290        let mut out = vec![0_u8; page_len()];
1291        read_base_at(0, &mut out).unwrap();
1292
1293        assert_eq!(
1294            page_physical_offset(&updated_block, 0).unwrap(),
1295            old_page_zero_offset
1296        );
1297        assert_eq!(
1298            page_physical_offset(&updated_block, later_page_no).unwrap(),
1299            old_later_offset
1300        );
1301        assert_eq!(out, updated_page_zero);
1302    }
1303
1304    #[test]
1305    #[serial_test::serial]
1306    fn in_place_commit_tracks_multi_segment_dirty_and_clean_pages() {
1307        crate::stable::memory::reset_for_tests();
1308        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1309
1310        let clean_page_no = 1;
1311        let later_page_no = FAR_PAGE_NO;
1312        write_at(0, &vec![1_u8; page_len()]).unwrap();
1313        write_at(clean_page_no * page_size(), &vec![2_u8; page_len()]).unwrap();
1314        write_at(later_page_no * page_size(), &vec![3_u8; page_len()]).unwrap();
1315
1316        let before = Superblock::load().unwrap();
1317        let before_first = page_physical_offset(&before, 0).unwrap();
1318        let before_clean = page_physical_offset(&before, clean_page_no).unwrap();
1319        let before_later = page_physical_offset(&before, later_page_no).unwrap();
1320
1321        begin_update().unwrap();
1322        write_at(0, &vec![4_u8; page_len()]).unwrap();
1323        write_at(later_page_no * page_size(), &vec![5_u8; page_len()]).unwrap();
1324        commit_update().unwrap();
1325
1326        let after = Superblock::load().unwrap();
1327
1328        assert_eq!(after.page_count, active_page_count(&after).unwrap());
1329        assert_eq!(page_physical_offset(&after, 0).unwrap(), before_first);
1330        assert_eq!(
1331            page_physical_offset(&after, clean_page_no).unwrap(),
1332            before_clean
1333        );
1334        assert_eq!(
1335            page_physical_offset(&after, later_page_no).unwrap(),
1336            before_later
1337        );
1338    }
1339
1340    #[test]
1341    #[serial_test::serial]
1342    fn in_place_truncate_hides_old_tail_pages() {
1343        crate::stable::memory::reset_for_tests();
1344        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1345
1346        write_at(0, &vec![1_u8; page_len()]).unwrap();
1347        write_at(page_size(), &vec![2_u8; page_len()]).unwrap();
1348        write_at(2 * page_size(), &vec![3_u8; page_len()]).unwrap();
1349        truncate(page_size()).unwrap();
1350
1351        let block = Superblock::load().unwrap();
1352        let mut hidden = vec![1_u8; page_len()];
1353        read_base_at(page_size(), &mut hidden).unwrap();
1354
1355        assert_eq!(block.db_size, page_size());
1356        assert_eq!(block.page_count, 1);
1357        assert_eq!(block.zero_extent_count(), 1);
1358        assert_eq!(hidden, vec![0_u8; page_len()]);
1359    }
1360
1361    #[test]
1362    #[serial_test::serial]
1363    fn same_overlay_truncate_reextend_zeroes_active_gap() {
1364        crate::stable::memory::reset_for_tests();
1365        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1366
1367        write_at(0, &vec![1_u8; page_len()]).unwrap();
1368        write_at(page_size(), &vec![0xAA; page_len()]).unwrap();
1369        write_at(2 * page_size(), &vec![3_u8; page_len()]).unwrap();
1370
1371        begin_update().unwrap();
1372        truncate(page_size()).unwrap();
1373        truncate(2 * page_size()).unwrap();
1374        let mut overlay_gap = vec![1_u8; page_len()];
1375        read_at(page_size(), &mut overlay_gap).unwrap();
1376        commit_update().unwrap();
1377
1378        let committed_gap = export_chunk(page_size(), page_size()).unwrap();
1379
1380        assert_eq!(overlay_gap, vec![0_u8; page_len()]);
1381        assert_eq!(committed_gap, vec![0_u8; page_len()]);
1382    }
1383
1384    #[test]
1385    #[serial_test::serial]
1386    fn compact_is_noop_for_in_place_layout() {
1387        crate::stable::memory::reset_for_tests();
1388        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1389
1390        let later_page_no = FAR_PAGE_NO;
1391        let first_page = vec![7_u8; page_len()];
1392        let later_page = vec![9_u8; page_len()];
1393        write_at(0, &first_page).unwrap();
1394        write_at(later_page_no * page_size(), &later_page).unwrap();
1395
1396        compact().unwrap();
1397
1398        let block = Superblock::load().unwrap();
1399        let mut first_out = vec![0_u8; page_len()];
1400        let mut later_out = vec![0_u8; page_len()];
1401
1402        read_base_at(0, &mut first_out).unwrap();
1403        read_base_at(later_page_no * page_size(), &mut later_out).unwrap();
1404
1405        assert_eq!(
1406            page_physical_offset(&block, later_page_no).unwrap(),
1407            block.db_base_offset + later_page_no * page_size()
1408        );
1409        assert_eq!(first_out, first_page);
1410        assert_eq!(later_out, later_page);
1411    }
1412
1413    #[test]
1414    #[serial_test::serial]
1415    fn in_place_expand_only_commit_zeroes_new_logical_tail() {
1416        crate::stable::memory::reset_for_tests();
1417        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1418
1419        write_at(0, &[0]).unwrap();
1420        truncate(page_size() * 4).unwrap();
1421        truncate(page_size() * 4 + 1).unwrap();
1422
1423        let block = Superblock::load().unwrap();
1424        let mut first = [1_u8; 1];
1425        let mut expanded_tail = [1_u8; 1];
1426
1427        read_base_at(0, &mut first).unwrap();
1428        read_base_at(page_size() * 4, &mut expanded_tail).unwrap();
1429
1430        assert_eq!(block.db_size, page_size() * 4 + 1);
1431        assert_eq!(
1432            page_physical_offset(&block, 0).unwrap(),
1433            block.db_base_offset
1434        );
1435        assert_eq!(
1436            page_physical_offset(&block, 4).unwrap(),
1437            block.db_base_offset + 4 * page_size()
1438        );
1439        assert_eq!(first, [0]);
1440        assert_eq!(expanded_tail, [0]);
1441    }
1442
1443    #[test]
1444    #[serial_test::serial]
1445    fn truncate_grow_zeroes_stale_physical_gap() {
1446        crate::stable::memory::reset_for_tests();
1447        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1448
1449        let old_size = page_size() + 17;
1450        let grow_len = page_size() * 2 + 111;
1451        let grow_len_usize = usize::try_from(grow_len).unwrap();
1452        write_at(old_size - 1, b"a").unwrap();
1453        let block = Superblock::load().unwrap();
1454        crate::stable::memory::write(block.db_base_offset + old_size, &vec![0xAA; grow_len_usize])
1455            .unwrap();
1456
1457        truncate(old_size + grow_len).unwrap();
1458        let gap = export_chunk(old_size, grow_len).unwrap();
1459
1460        assert!(gap.iter().all(|byte| *byte == 0));
1461    }
1462
1463    #[test]
1464    #[serial_test::serial]
1465    fn sparse_write_zeroes_stale_physical_gap() {
1466        crate::stable::memory::reset_for_tests();
1467        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1468
1469        let old_size = page_size() + 17;
1470        let write_offset = old_size + page_size() * 2 + 33;
1471        let gap_len = write_offset - old_size;
1472        let gap_len_usize = usize::try_from(gap_len).unwrap();
1473        write_at(old_size - 1, b"a").unwrap();
1474        let block = Superblock::load().unwrap();
1475        crate::stable::memory::write(block.db_base_offset + old_size, &vec![0xAA; gap_len_usize])
1476            .unwrap();
1477
1478        write_at(write_offset, b"z").unwrap();
1479        let gap = export_chunk(old_size, gap_len).unwrap();
1480        let written = export_chunk(write_offset, 1).unwrap();
1481
1482        assert!(gap.iter().all(|byte| *byte == 0));
1483        assert_eq!(written, b"z");
1484    }
1485
1486    #[test]
1487    #[serial_test::serial]
1488    fn failed_import_slack_stays_hidden_after_normal_grow() {
1489        crate::stable::memory::reset_for_tests();
1490        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1491
1492        write_at(0, b"a").unwrap();
1493        begin_import(page_size(), 0).unwrap();
1494        let importing = Superblock::load().unwrap();
1495        import_chunk(0, &vec![0xAA; page_len()]).unwrap();
1496        assert!(finish_import().is_err());
1497
1498        let block = Superblock::load().unwrap();
1499        let logical_import_offset = importing.import_base_offset - block.db_base_offset;
1500        truncate(logical_import_offset + page_size()).unwrap();
1501        let grown_over_import_slack = export_chunk(logical_import_offset, page_size()).unwrap();
1502
1503        assert!(grown_over_import_slack.iter().all(|byte| *byte == 0));
1504    }
1505
1506    #[test]
1507    #[serial_test::serial]
1508    fn zero_mask_hides_truncated_tail_without_moving_base() {
1509        crate::stable::memory::reset_for_tests();
1510        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1511
1512        write_at(0, &vec![b'a'; page_len()]).unwrap();
1513        write_at(page_size(), &vec![b'b'; page_len()]).unwrap();
1514        let old_base = Superblock::load().unwrap().db_base_offset;
1515
1516        truncate(1).unwrap();
1517        let mut stale_tail = [0_u8; 1];
1518        crate::stable::memory::read_preallocated(old_base + page_size(), &mut stale_tail).unwrap();
1519        assert_eq!(stale_tail, [b'b']);
1520
1521        let grow_offset = 2 * page_size() + 2;
1522        write_at(grow_offset, b"z").unwrap();
1523
1524        let block = Superblock::load().unwrap();
1525        let image = export_chunk(0, block.db_size).unwrap();
1526        let z_index = usize::try_from(grow_offset).unwrap();
1527
1528        assert_eq!(block.db_base_offset, old_base);
1529        assert_eq!(block.db_size, grow_offset + 1);
1530        assert_eq!(block.zero_extent_count(), 1);
1531        assert_eq!(image[0], b'a');
1532        assert!(image[1..z_index].iter().all(|byte| *byte == 0));
1533        assert_eq!(image[z_index], b'z');
1534    }
1535
1536    #[test]
1537    fn zero_extent_add_subtract_merges_and_splits_ranges() {
1538        let mut extents = Vec::new();
1539        add_zero_extent(&mut extents, 10, 20).unwrap();
1540        add_zero_extent(&mut extents, 30, 40).unwrap();
1541        add_zero_extent(&mut extents, 20, 30).unwrap();
1542        assert_eq!(
1543            extents,
1544            vec![ZeroExtent {
1545                start_page: 10,
1546                end_page: 40
1547            }]
1548        );
1549
1550        subtract_zero_extent(&mut extents, 15, 17).unwrap();
1551        assert_eq!(
1552            extents,
1553            vec![
1554                ZeroExtent {
1555                    start_page: 10,
1556                    end_page: 15
1557                },
1558                ZeroExtent {
1559                    start_page: 17,
1560                    end_page: 40
1561                }
1562            ]
1563        );
1564
1565        subtract_zero_extent(&mut extents, 8, 18).unwrap();
1566        assert_eq!(
1567            extents,
1568            vec![ZeroExtent {
1569                start_page: 18,
1570                end_page: 40
1571            }]
1572        );
1573    }
1574
1575    #[test]
1576    fn zero_extent_truncate_tail_merges_with_existing_range() {
1577        let mut extents = vec![ZeroExtent {
1578            start_page: 10,
1579            end_page: 20,
1580        }];
1581
1582        add_zero_extent(&mut extents, 10, 40).unwrap();
1583
1584        assert_eq!(
1585            extents,
1586            vec![ZeroExtent {
1587                start_page: 10,
1588                end_page: 40
1589            }]
1590        );
1591    }
1592
1593    #[test]
1594    fn zero_extent_commit_allows_temporary_limit_excess_removed_by_dirty_page() {
1595        let mut block = Superblock::fresh();
1596        block.db_size = (MAX_ZERO_EXTENTS as u64) * 2 * page_size();
1597        block.zero_extents = (0..MAX_ZERO_EXTENTS)
1598            .map(|index| ZeroExtent {
1599                start_page: (index as u64) * 2,
1600                end_page: (index as u64) * 2 + 1,
1601            })
1602            .collect();
1603
1604        let mut overlay = Overlay::new(block.db_size);
1605        overlay
1606            .write_at(block.db_size, &vec![0xAA; page_len()])
1607            .unwrap();
1608        let final_page_count = page_count_for_size(overlay.size()).unwrap();
1609
1610        let extents = zero_extents_after_commit(&block, &overlay, final_page_count).unwrap();
1611
1612        assert_eq!(extents.len(), MAX_ZERO_EXTENTS);
1613        assert_eq!(extents, block.zero_extents);
1614    }
1615
1616    #[test]
1617    fn zero_extent_commit_rejects_final_limit_excess() {
1618        let mut block = Superblock::fresh();
1619        block.db_size = (MAX_ZERO_EXTENTS as u64) * 2 * page_size();
1620        block.zero_extents = (0..MAX_ZERO_EXTENTS)
1621            .map(|index| ZeroExtent {
1622                start_page: (index as u64) * 2,
1623                end_page: (index as u64) * 2 + 1,
1624            })
1625            .collect();
1626
1627        let mut overlay = Overlay::new(block.db_size);
1628        overlay.truncate(block.db_size + page_size()).unwrap();
1629        let final_page_count = page_count_for_size(overlay.size()).unwrap();
1630
1631        assert!(matches!(
1632            zero_extents_after_commit(&block, &overlay, final_page_count),
1633            Err(StableMemoryError::ZeroExtentLimitExceeded {
1634                limit: MAX_ZERO_EXTENTS
1635            })
1636        ));
1637    }
1638
1639    #[test]
1640    #[serial_test::serial]
1641    fn zero_extent_limit_error_keeps_active_image_and_metadata() {
1642        crate::stable::memory::reset_for_tests();
1643        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1644
1645        let mut block = Superblock::fresh();
1646        let max_zero_extents = u64::try_from(MAX_ZERO_EXTENTS).unwrap();
1647        block.db_size = max_zero_extents * 2 * page_size();
1648        block.page_count = page_count_for_size(block.db_size).unwrap();
1649        block.last_tx_id = 41;
1650        block.flags = FLAG_CHECKSUM_STALE | FLAG_CHECKSUM_REFRESHING;
1651        block.checksum = 0xA5A5;
1652        block.checksum_refresh_offset = 17;
1653        block.checksum_refresh_hash = 0x5A5A;
1654        block.checksum_refresh_tx_id = 40;
1655        block.zero_extents = (0..MAX_ZERO_EXTENTS)
1656            .map(|index| ZeroExtent {
1657                start_page: u64::try_from(index).unwrap() * 2,
1658                end_page: u64::try_from(index).unwrap() * 2 + 1,
1659            })
1660            .collect();
1661
1662        let sample_page_no = 1_u64;
1663        let sample_offset = page_physical_offset(&block, sample_page_no).unwrap();
1664        let sample = b"active-before";
1665        let sample_len = u64::try_from(sample.len()).unwrap();
1666        crate::stable::memory::ensure_capacity(checked_add(sample_offset, sample_len).unwrap())
1667            .unwrap();
1668        crate::stable::memory::write(sample_offset, sample).unwrap();
1669        block.store().unwrap();
1670
1671        let before_block = Superblock::load().unwrap();
1672        let mut before_sample = vec![0_u8; sample.len()];
1673        read_base_at(sample_page_no * page_size(), &mut before_sample).unwrap();
1674        assert_eq!(before_sample, sample);
1675
1676        begin_update().unwrap();
1677        truncate(block.db_size + page_size()).unwrap();
1678        let result = commit_update();
1679
1680        assert!(matches!(
1681            result,
1682            Err(StableMemoryError::ZeroExtentLimitExceeded {
1683                limit: MAX_ZERO_EXTENTS
1684            })
1685        ));
1686        assert_eq!(Superblock::load().unwrap(), before_block);
1687
1688        let mut after_sample = vec![0_u8; sample.len()];
1689        read_base_at(sample_page_no * page_size(), &mut after_sample).unwrap();
1690        assert_eq!(after_sample, before_sample);
1691    }
1692
1693    #[test]
1694    fn zero_extent_temporary_limit_allows_only_dirty_page_slack() {
1695        let extents = (0..=MAX_ZERO_EXTENTS)
1696            .map(|index| ZeroExtent {
1697                start_page: index as u64,
1698                end_page: index as u64 + 1,
1699            })
1700            .collect::<Vec<_>>();
1701
1702        enforce_temporary_zero_extent_limit(&extents, MAX_ZERO_EXTENTS + 1).unwrap();
1703        assert!(matches!(
1704            enforce_temporary_zero_extent_limit(&extents, MAX_ZERO_EXTENTS),
1705            Err(StableMemoryError::ZeroExtentLimitExceeded {
1706                limit: MAX_ZERO_EXTENTS
1707            })
1708        ));
1709    }
1710
1711    #[test]
1712    fn zero_extent_limit_counts_normalized_ranges() {
1713        let mut extents = (0..MAX_ZERO_EXTENTS)
1714            .map(|index| ZeroExtent {
1715                start_page: (index as u64) * 2,
1716                end_page: (index as u64) * 2 + 1,
1717            })
1718            .collect::<Vec<_>>();
1719
1720        add_zero_extent(
1721            &mut extents,
1722            (MAX_ZERO_EXTENTS as u64) * 2 - 2,
1723            (MAX_ZERO_EXTENTS as u64) * 2,
1724        )
1725        .unwrap();
1726
1727        assert_eq!(extents.len(), MAX_ZERO_EXTENTS);
1728        assert_eq!(
1729            extents[MAX_ZERO_EXTENTS - 1],
1730            ZeroExtent {
1731                start_page: (MAX_ZERO_EXTENTS as u64) * 2 - 2,
1732                end_page: (MAX_ZERO_EXTENTS as u64) * 2
1733            }
1734        );
1735    }
1736
1737    #[test]
1738    fn zero_extent_limit_is_rejected_without_fallback() {
1739        let mut extents = (0..MAX_ZERO_EXTENTS)
1740            .map(|index| ZeroExtent {
1741                start_page: (index as u64) * 2,
1742                end_page: (index as u64) * 2 + 1,
1743            })
1744            .collect::<Vec<_>>();
1745        let result = add_zero_extent(
1746            &mut extents,
1747            (MAX_ZERO_EXTENTS as u64) * 2,
1748            (MAX_ZERO_EXTENTS as u64) * 2 + 1,
1749        );
1750        assert!(matches!(
1751            result,
1752            Err(StableMemoryError::ZeroExtentLimitExceeded {
1753                limit: MAX_ZERO_EXTENTS
1754            })
1755        ));
1756    }
1757
1758    #[test]
1759    fn pbt_zero_extent_normalizer_matches_independent_model() {
1760        let mut runner = TestRunner::new(Config {
1761            cases: 512,
1762            ..Config::default()
1763        });
1764
1765        runner
1766            .run(&zero_extent_vec_strategy(), |mut extents| {
1767                let expected = model_normalize_zero_extents(&extents);
1768                let result = normalize_zero_extents(&mut extents);
1769
1770                if expected.len() > MAX_ZERO_EXTENTS {
1771                    let rejected = matches!(
1772                        result,
1773                        Err(StableMemoryError::ZeroExtentLimitExceeded {
1774                            limit: MAX_ZERO_EXTENTS
1775                        })
1776                    );
1777                    prop_assert!(rejected);
1778                } else {
1779                    result.map_err(|error| TestCaseError::fail(error.to_string()))?;
1780                    prop_assert_eq!(extents, expected);
1781                }
1782                Ok(())
1783            })
1784            .unwrap();
1785    }
1786
1787    #[test]
1788    fn pbt_zero_extent_add_subtract_matches_independent_model() {
1789        let mut runner = TestRunner::new(Config {
1790            cases: 256,
1791            ..Config::default()
1792        });
1793
1794        runner
1795            .run(&zero_extent_operation_sequence(), |operations| {
1796                let mut production = Vec::new();
1797                let mut model = Vec::new();
1798
1799                for operation in operations {
1800                    let result = match operation {
1801                        ZeroExtentOp::Add {
1802                            start_page,
1803                            end_page,
1804                        } => {
1805                            model_add_zero_extent(&mut model, start_page, end_page);
1806                            add_zero_extent(&mut production, start_page, end_page)
1807                        }
1808                        ZeroExtentOp::Subtract {
1809                            start_page,
1810                            end_page,
1811                        } => {
1812                            model_subtract_zero_extent(&mut model, start_page, end_page);
1813                            subtract_zero_extent(&mut production, start_page, end_page)
1814                        }
1815                    };
1816
1817                    let expected = model_normalize_zero_extents(&model);
1818                    if expected.len() > MAX_ZERO_EXTENTS {
1819                        let rejected = matches!(
1820                            result,
1821                            Err(StableMemoryError::ZeroExtentLimitExceeded {
1822                                limit: MAX_ZERO_EXTENTS
1823                            })
1824                        );
1825                        prop_assert!(rejected);
1826                        return Ok(());
1827                    }
1828
1829                    result.map_err(|error| TestCaseError::fail(error.to_string()))?;
1830                    prop_assert_eq!(&production, &expected);
1831                    model = expected;
1832                }
1833                Ok(())
1834            })
1835            .unwrap();
1836    }
1837
1838    #[test]
1839    #[serial_test::serial]
1840    fn pbt_noop_compact_preserves_sparse_page_model() {
1841        let mut runner = TestRunner::new(Config {
1842            cases: 32,
1843            ..Config::default()
1844        });
1845
1846        runner
1847            .run(
1848                &proptest::collection::vec(prop::option::of(any::<u8>()), 0..=300),
1849                |pages| {
1850                    crate::stable::memory::reset_for_tests();
1851                    crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1852
1853                    let active_len = pages
1854                        .iter()
1855                        .rposition(Option::is_some)
1856                        .map(|index| index + 1)
1857                        .unwrap_or(0);
1858                    for (page_no, byte) in pages.iter().take(active_len).enumerate() {
1859                        if let Some(byte) = byte {
1860                            write_at(
1861                                u64::try_from(page_no).unwrap() * page_size(),
1862                                &vec![*byte; page_len()],
1863                            )
1864                            .unwrap();
1865                        }
1866                    }
1867
1868                    compact().unwrap();
1869                    let block = Superblock::load().unwrap();
1870                    prop_assert_eq!(
1871                        block.db_size,
1872                        u64::try_from(active_len).unwrap() * page_size()
1873                    );
1874                    prop_assert_eq!(block.page_count, active_page_count(&block).unwrap());
1875
1876                    for (page_no, byte) in pages.iter().take(active_len).enumerate() {
1877                        let mut page = vec![0_u8; page_len()];
1878                        read_base_at(u64::try_from(page_no).unwrap() * page_size(), &mut page)
1879                            .unwrap();
1880
1881                        if let Some(byte) = byte {
1882                            prop_assert_eq!(page, vec![*byte; page_len()]);
1883                        } else {
1884                            prop_assert_eq!(page, vec![0_u8; page_len()]);
1885                        }
1886                    }
1887                    Ok(())
1888                },
1889            )
1890            .unwrap();
1891    }
1892
1893    #[derive(Clone, Debug)]
1894    enum BlobOp {
1895        Write { offset: u64, len: usize, byte: u8 },
1896        Truncate { size: u64 },
1897        Compact,
1898    }
1899
1900    #[derive(Clone, Debug)]
1901    enum ZeroExtentOp {
1902        Add { start_page: u64, end_page: u64 },
1903        Subtract { start_page: u64, end_page: u64 },
1904    }
1905
1906    fn zero_extent_vec_strategy() -> impl Strategy<Value = Vec<ZeroExtent>> {
1907        let page_limit = zero_extent_model_page_limit();
1908        proptest::collection::vec(
1909            (0_u64..=page_limit, 0_u64..=page_limit).prop_map(|(left, right)| ZeroExtent {
1910                start_page: left.min(right),
1911                end_page: left.max(right),
1912            }),
1913            0..=MAX_ZERO_EXTENTS + 8,
1914        )
1915    }
1916
1917    fn zero_extent_operation_sequence() -> impl Strategy<Value = Vec<ZeroExtentOp>> {
1918        let page_limit = zero_extent_model_page_limit();
1919        let range = (0_u64..=page_limit, 0_u64..=page_limit)
1920            .prop_map(|(left, right)| (left.min(right), left.max(right)));
1921        proptest::collection::vec(
1922            prop_oneof![
1923                range
1924                    .clone()
1925                    .prop_map(|(start_page, end_page)| ZeroExtentOp::Add {
1926                        start_page,
1927                        end_page,
1928                    }),
1929                range.prop_map(|(start_page, end_page)| ZeroExtentOp::Subtract {
1930                    start_page,
1931                    end_page,
1932                }),
1933            ],
1934            0..=96,
1935        )
1936    }
1937
1938    fn zero_extent_model_page_limit() -> u64 {
1939        (u64::try_from(MAX_ZERO_EXTENTS).unwrap() + 8) * 2
1940    }
1941
1942    fn model_add_zero_extent(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
1943        if start_page < end_page {
1944            extents.push(ZeroExtent {
1945                start_page,
1946                end_page,
1947            });
1948        }
1949    }
1950
1951    fn model_subtract_zero_extent(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
1952        if start_page >= end_page {
1953            return;
1954        }
1955
1956        let mut next = Vec::new();
1957        for extent in extents.iter() {
1958            if end_page <= extent.start_page || start_page >= extent.end_page {
1959                next.push(extent.clone());
1960                continue;
1961            }
1962            if extent.start_page < start_page {
1963                next.push(ZeroExtent {
1964                    start_page: extent.start_page,
1965                    end_page: start_page,
1966                });
1967            }
1968            if end_page < extent.end_page {
1969                next.push(ZeroExtent {
1970                    start_page: end_page,
1971                    end_page: extent.end_page,
1972                });
1973            }
1974        }
1975        *extents = next;
1976    }
1977
1978    fn model_normalize_zero_extents(extents: &[ZeroExtent]) -> Vec<ZeroExtent> {
1979        let page_limit = usize::try_from(zero_extent_model_page_limit()).unwrap();
1980        let mut mask = vec![false; page_limit + 1];
1981        for extent in extents {
1982            let start = usize::try_from(extent.start_page).unwrap().min(mask.len());
1983            let end = usize::try_from(extent.end_page).unwrap().min(mask.len());
1984            for page in &mut mask[start..end] {
1985                *page = true;
1986            }
1987        }
1988
1989        let mut normalized = Vec::new();
1990        let mut page = 0_usize;
1991        while page < mask.len() {
1992            if !mask[page] {
1993                page += 1;
1994                continue;
1995            }
1996            let start = page;
1997            while page < mask.len() && mask[page] {
1998                page += 1;
1999            }
2000            normalized.push(ZeroExtent {
2001                start_page: u64::try_from(start).unwrap(),
2002                end_page: u64::try_from(page).unwrap(),
2003            });
2004        }
2005        normalized
2006    }
2007
2008    #[test]
2009    #[serial_test::serial]
2010    fn pbt_blob_operations_match_logical_model_across_noop_compact() {
2011        let mut runner = TestRunner::new(Config {
2012            cases: 48,
2013            ..Config::default()
2014        });
2015
2016        runner
2017            .run(&blob_operation_sequence(), |operations| {
2018                crate::stable::memory::reset_for_tests();
2019                crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2020
2021                let mut model = Vec::new();
2022                let mut materialized = BTreeSet::new();
2023                assert_blob_model(&model, &materialized, false)?;
2024
2025                for operation in operations {
2026                    let compacted = apply_blob_op(operation, &mut model, &mut materialized)?;
2027                    assert_blob_model(&model, &materialized, compacted)?;
2028                }
2029                Ok(())
2030            })
2031            .unwrap();
2032    }
2033
2034    fn blob_operation_sequence() -> impl Strategy<Value = Vec<BlobOp>> {
2035        let write = (blob_offset_strategy(), blob_len_strategy(), any::<u8>())
2036            .prop_map(|(offset, len, byte)| BlobOp::Write { offset, len, byte });
2037        let truncate = blob_offset_strategy().prop_map(|size| BlobOp::Truncate { size });
2038        proptest::collection::vec(prop_oneof![write, truncate, Just(BlobOp::Compact)], 0..=48)
2039    }
2040
2041    fn blob_offset_strategy() -> impl Strategy<Value = u64> {
2042        let limit = blob_model_limit();
2043        let page = page_size();
2044        let far_page = FAR_PAGE_NO * page;
2045        prop_oneof![
2046            0_u64..=limit,
2047            prop::sample::select(boundary_values(&[
2048                0,
2049                1,
2050                page - 1,
2051                page,
2052                page + 1,
2053                far_page - 1,
2054                far_page,
2055                far_page + 1,
2056                limit - 1,
2057                limit,
2058            ]))
2059            .prop_map(move |value| value.min(limit)),
2060        ]
2061    }
2062
2063    fn blob_len_strategy() -> impl Strategy<Value = usize> {
2064        prop_oneof![
2065            0_usize..=(page_len() * 2 + 17),
2066            prop::sample::select(vec![
2067                0,
2068                1,
2069                page_len() - 1,
2070                page_len(),
2071                page_len() + 1,
2072                page_len() * 2 + 1,
2073            ]),
2074        ]
2075    }
2076
2077    fn blob_model_limit() -> u64 {
2078        (FAR_PAGE_NO + 3) * page_size()
2079    }
2080
2081    fn apply_blob_op(
2082        operation: BlobOp,
2083        model: &mut Vec<u8>,
2084        materialized: &mut BTreeSet<u64>,
2085    ) -> Result<bool, TestCaseError> {
2086        match operation {
2087            BlobOp::Write { offset, len, byte } => {
2088                let len = len.min(usize::try_from(blob_model_limit() - offset).unwrap());
2089                let bytes = vec![byte; len];
2090                write_at(offset, &bytes).map_err(|error| TestCaseError::fail(error.to_string()))?;
2091                if len == 0 {
2092                    return Ok(false);
2093                }
2094
2095                let start = usize::try_from(offset).unwrap();
2096                let end = start + len;
2097                if model.len() < start {
2098                    model.resize(start, 0);
2099                }
2100                if model.len() < end {
2101                    model.resize(end, 0);
2102                }
2103                model[start..end].copy_from_slice(&bytes);
2104                mark_materialized_range(offset, len, materialized);
2105                Ok(false)
2106            }
2107            BlobOp::Truncate { size } => {
2108                truncate(size).map_err(|error| TestCaseError::fail(error.to_string()))?;
2109                let new_len = usize::try_from(size).unwrap();
2110                model.resize(new_len, 0);
2111                let active_pages = page_count_for_size(size)
2112                    .map_err(|error| TestCaseError::fail(error.to_string()))?;
2113                materialized.retain(|page_no| *page_no < active_pages);
2114                if size > 0 && !size.is_multiple_of(page_size()) {
2115                    materialized.insert(size / page_size());
2116                }
2117                Ok(false)
2118            }
2119            BlobOp::Compact => {
2120                compact().map_err(|error| TestCaseError::fail(error.to_string()))?;
2121                Ok(true)
2122            }
2123        }
2124    }
2125
2126    fn mark_materialized_range(offset: u64, len: usize, materialized: &mut BTreeSet<u64>) {
2127        let end = offset + u64::try_from(len).unwrap();
2128        let first_page = offset / page_size();
2129        let last_page = (end - 1) / page_size();
2130        for page_no in first_page..=last_page {
2131            materialized.insert(page_no);
2132        }
2133    }
2134
2135    fn assert_blob_model(
2136        model: &[u8],
2137        _materialized: &BTreeSet<u64>,
2138        _expect_compacted: bool,
2139    ) -> Result<(), TestCaseError> {
2140        let block = Superblock::load().map_err(|error| TestCaseError::fail(error.to_string()))?;
2141        prop_assert_eq!(block.db_size, u64::try_from(model.len()).unwrap());
2142
2143        if !model.is_empty() {
2144            let mut out = vec![0_u8; model.len()];
2145            read_base_at(0, &mut out).map_err(|error| TestCaseError::fail(error.to_string()))?;
2146            prop_assert_eq!(out, model);
2147        }
2148
2149        let mut tail = vec![1_u8; 32];
2150        read_base_at(u64::try_from(model.len()).unwrap(), &mut tail)
2151            .map_err(|error| TestCaseError::fail(error.to_string()))?;
2152        prop_assert_eq!(tail, vec![0_u8; 32]);
2153
2154        let active_pages = page_count_for_size(u64::try_from(model.len()).unwrap())
2155            .map_err(|error| TestCaseError::fail(error.to_string()))?;
2156        prop_assert_eq!(block.page_count, active_pages);
2157        Ok(())
2158    }
2159
2160    #[test]
2161    #[serial_test::serial]
2162    fn read_metrics_track_in_place_data_reads() {
2163        crate::stable::memory::reset_for_tests();
2164        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2165
2166        let page = vec![7_u8; page_len()];
2167        write_at(0, &page).unwrap();
2168        crate::read_metrics::reset_read_metrics();
2169
2170        let first = read_base_page(0).unwrap();
2171        let second = read_base_page(0).unwrap();
2172        let metrics = crate::read_metrics::read_metrics_snapshot();
2173
2174        assert_eq!(first, page);
2175        assert_eq!(second, page);
2176        assert!(metrics.stable_data_read_calls >= 2);
2177        assert!(metrics.stable_data_read_bytes >= page_size() * 2);
2178        #[cfg(feature = "bench-profile")]
2179        assert!(metrics.superblock_loads <= 1);
2180        #[cfg(not(feature = "bench-profile"))]
2181        assert_eq!(metrics.superblock_loads, 0);
2182    }
2183
2184    #[test]
2185    #[serial_test::serial]
2186    fn page_offset_cache_reuses_page_data_for_small_reads() {
2187        crate::stable::memory::reset_for_tests();
2188        crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2189
2190        let page = vec![9_u8; page_len()];
2191        write_at(0, &page).unwrap();
2192        let block = Superblock::load().unwrap();
2193        let mut cache = PageOffsetCache::new();
2194        let mut first = [0_u8; 16];
2195        let mut second = [0_u8; 16];
2196
2197        crate::read_metrics::reset_read_metrics();
2198        read_base_at_with_page_cache(&block, 0, &mut first, &mut cache).unwrap();
2199        read_base_at_with_page_cache(&block, 8, &mut second, &mut cache).unwrap();
2200        let metrics = crate::read_metrics::read_metrics_snapshot();
2201
2202        assert_eq!(first, [9_u8; 16]);
2203        assert_eq!(second, [9_u8; 16]);
2204        assert_eq!(metrics.stable_data_read_calls, 1);
2205        assert_eq!(metrics.stable_data_read_bytes, page_size());
2206    }
2207}