Skip to main content

reddb_engine/storage/
pager_io.rs

1//! Byte I/O shared by the native pager and exclusively owned browser handles.
2//!
3//! The owner supplies locking, read-only policy, WAL-first ordering, allocation
4//! fences and encryption. It must stop using a handle after failed I/O and
5//! reopen/recover before resuming. These functions do not form a database API.
6
7#![deny(
8    clippy::unwrap_used,
9    clippy::too_many_lines,
10    clippy::cast_possible_truncation
11)]
12
13mod staging;
14pub(crate) use staging::StagedPageBatch;
15
16use super::page::{Page, PageError, PageType, PAGE_SIZE};
17use reddb_file::{VfsFile, PAGED_SUPERBLOCK_SLOT_SIZE, PAGED_SUPERBLOCK_ZONE_SIZE};
18use std::io::{self, SeekFrom};
19
20pub const DWB_ZONE_START_PAGE: u32 = 3;
21pub const DWB_ZONE_PAGES: u32 = 64;
22pub const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
23pub const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
24pub const DWB_MAX_PAGES_PER_FRAME: usize =
25    (DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
26
27#[derive(Debug)]
28pub enum PagerIoError {
29    Io(io::Error),
30    Page(PageError),
31    InvalidDatabase(String),
32}
33
34impl std::fmt::Display for PagerIoError {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Io(error) => write!(f, "I/O error: {error}"),
38            Self::Page(error) => write!(f, "Page error: {error}"),
39            Self::InvalidDatabase(message) => write!(f, "Invalid database: {message}"),
40        }
41    }
42}
43
44impl std::error::Error for PagerIoError {
45    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46        match self {
47            Self::Io(error) => Some(error),
48            Self::Page(error) => Some(error),
49            Self::InvalidDatabase(_) => None,
50        }
51    }
52}
53
54impl From<io::Error> for PagerIoError {
55    fn from(error: io::Error) -> Self {
56        Self::Io(error)
57    }
58}
59
60impl From<PageError> for PagerIoError {
61    fn from(error: PageError) -> Self {
62        Self::Page(error)
63    }
64}
65
66pub fn read_page<F: VfsFile>(
67    file: &mut F,
68    page_id: u32,
69    verify_checksum: bool,
70) -> Result<Page, PagerIoError> {
71    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
72    let mut buffer = [0u8; PAGE_SIZE];
73    file.read_exact(&mut buffer)?;
74    let page = Page::from_bytes(buffer);
75    // Page 0 contains independently sealed superblocks, not a page checksum.
76    if verify_checksum && page_id != 0 {
77        page.verify_checksum()?;
78        check_page_identity(&page, page_id)?;
79    }
80    Ok(page)
81}
82
83/// Check plaintext identity only; encrypted envelopes are authenticated by the host.
84pub fn check_page_identity(page: &Page, expected: u32) -> Result<(), PagerIoError> {
85    if expected != 0 && page.page_id() != expected {
86        return Err(PagerIoError::InvalidDatabase(format!(
87            "page identity mismatch: requested {expected}, found {}",
88            page.page_id()
89        )));
90    }
91    Ok(())
92}
93
94/// Raw write, without a durability barrier. The owner controls publication.
95pub fn write_page<F: VfsFile>(file: &mut F, page_id: u32, page: &Page) -> Result<(), PagerIoError> {
96    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
97    file.write_all(page.as_bytes())?;
98    Ok(())
99}
100
101pub fn read_superblock_zone<F: VfsFile>(
102    file: &mut F,
103) -> Result<[u8; PAGED_SUPERBLOCK_ZONE_SIZE], PagerIoError> {
104    let len = file.file_len()?;
105    let mut zone = [0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
106    let readable = usize::try_from(len.min(PAGED_SUPERBLOCK_ZONE_SIZE as u64))
107        .expect("readable prefix is bounded by the superblock zone");
108    if readable > 0 {
109        file.seek(SeekFrom::Start(0))?;
110        file.read_exact(&mut zone[..readable])?;
111    }
112    Ok(zone)
113}
114
115/// Seal and sync only the stale slot; the owner selects it and its generation.
116pub fn write_superblock_slot<F: VfsFile>(
117    file: &mut F,
118    slot: &mut [u8; PAGED_SUPERBLOCK_SLOT_SIZE],
119    copy_index: usize,
120    generation: u64,
121) -> Result<(), PagerIoError> {
122    reddb_file::seal_paged_superblock_slot(slot, copy_index, generation)
123        .map_err(|error| PagerIoError::InvalidDatabase(error.to_string()))?;
124    file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
125        copy_index,
126    )))?;
127    file.write_all(slot)?;
128    file.sync_all()?;
129    Ok(())
130}
131
132pub fn dwb_zone_offset() -> u64 {
133    u64::from(DWB_ZONE_START_PAGE) * PAGE_SIZE as u64
134}
135
136/// Clear the in-file DWB zone with valid reserved pages and sync it.
137pub fn clear_dwb_zone<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
138    for page_id in DWB_ZONE_START_PAGE..FIRST_ALLOCATABLE_PAGE {
139        let mut page = Page::new(PageType::Header, page_id);
140        page.update_checksum();
141        write_page(file, page_id, &page)?;
142    }
143    file.sync_all()?;
144    Ok(())
145}
146
147pub fn dwb_zone_is_clear(buffer: &[u8]) -> bool {
148    if buffer.len() != DWB_ZONE_BYTES {
149        return false;
150    }
151    for (index, chunk) in buffer.chunks_exact(PAGE_SIZE).enumerate() {
152        let Ok(page) = Page::from_slice(chunk) else {
153            return false;
154        };
155        if page.verify_checksum().is_err()
156            || page.page_id()
157                != DWB_ZONE_START_PAGE
158                    + u32::try_from(index).expect("DWB page index is bounded by its zone")
159            || page.page_type().ok() != Some(PageType::Header)
160        {
161            return false;
162        }
163    }
164    true
165}
166
167/// The caller must make the batch's WAL durable before entering this function.
168/// With DWB enabled: stage+sync and home pages+sync for each bounded frame,
169/// then clear+sync once. With DWB disabled, the caller's proven atomic-write
170/// policy applies.
171pub fn write_pages<F: VfsFile>(
172    file: &mut F,
173    pages: &[(u32, Page)],
174    double_write: bool,
175) -> Result<(), PagerIoError> {
176    if !double_write {
177        for (page_id, page) in pages {
178            write_page(file, *page_id, page)?;
179        }
180        return Ok(());
181    }
182
183    for chunk in pages.chunks(DWB_MAX_PAGES_PER_FRAME.max(1)) {
184        let buffer = reddb_file::encode_paged_dwb_frame(
185            chunk
186                .iter()
187                .map(|(page_id, page)| (*page_id, page.as_bytes())),
188        );
189        assert!(buffer.len() <= DWB_ZONE_BYTES, "DWB frame fits its zone");
190        file.seek(SeekFrom::Start(dwb_zone_offset()))?;
191        file.write_all(&buffer)?;
192        file.sync_all()?;
193        for (page_id, page) in chunk {
194            write_page(file, *page_id, page)?;
195        }
196        file.sync_all()?;
197    }
198    // Each frame's home pages are durable before the next frame replaces its
199    // staging image. An interrupted replacement can replay the old frame or
200    // discard an incomplete new one: neither can lose those durable homes.
201    // Only the final frame needs clearing before returning to the owner.
202    if !pages.is_empty() {
203        clear_dwb_zone(file)?;
204    }
205    Ok(())
206}
207
208/// Replay a complete staged DWB frame before allowing page access. Incomplete
209/// staging is discarded as on the native path: home writes start only after
210/// the complete frame crossed its barrier. No database root is recreated.
211pub fn recover_from_dwb<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
212    staging::recover(file)
213}
214
215/// Read a complete staged frame without consuming or repairing the source.
216/// Incomplete staging cannot have reached home pages and is ignored, just as
217/// in writer recovery. The frame decoder bounds the number of pages.
218pub fn read_dwb_pages<F: VfsFile>(file: &mut F) -> Result<Vec<(u32, Page)>, PagerIoError> {
219    staging::read_pages(file)
220}