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
13use super::page::{Page, PageError, PageType, PAGE_SIZE};
14use reddb_file::{VfsFile, PAGED_SUPERBLOCK_SLOT_SIZE, PAGED_SUPERBLOCK_ZONE_SIZE};
15use std::io::{self, SeekFrom};
16
17pub const DWB_ZONE_START_PAGE: u32 = 3;
18pub const DWB_ZONE_PAGES: u32 = 64;
19pub const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
20pub const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
21pub const DWB_MAX_PAGES_PER_FRAME: usize =
22    (DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
23
24#[derive(Debug)]
25pub enum PagerIoError {
26    Io(io::Error),
27    Page(PageError),
28    InvalidDatabase(String),
29}
30
31impl std::fmt::Display for PagerIoError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Io(error) => write!(f, "I/O error: {error}"),
35            Self::Page(error) => write!(f, "Page error: {error}"),
36            Self::InvalidDatabase(message) => write!(f, "Invalid database: {message}"),
37        }
38    }
39}
40
41impl std::error::Error for PagerIoError {
42    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43        match self {
44            Self::Io(error) => Some(error),
45            Self::Page(error) => Some(error),
46            Self::InvalidDatabase(_) => None,
47        }
48    }
49}
50
51impl From<io::Error> for PagerIoError {
52    fn from(error: io::Error) -> Self {
53        Self::Io(error)
54    }
55}
56
57impl From<PageError> for PagerIoError {
58    fn from(error: PageError) -> Self {
59        Self::Page(error)
60    }
61}
62
63pub fn read_page<F: VfsFile>(
64    file: &mut F,
65    page_id: u32,
66    verify_checksum: bool,
67) -> Result<Page, PagerIoError> {
68    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
69    let mut buffer = [0u8; PAGE_SIZE];
70    file.read_exact(&mut buffer)?;
71    let page = Page::from_bytes(buffer);
72    // Page 0 contains independently sealed superblocks, not a page checksum.
73    if verify_checksum && page_id != 0 {
74        page.verify_checksum()?;
75    }
76    Ok(page)
77}
78
79/// Raw write, without a durability barrier. The owner controls publication.
80pub fn write_page<F: VfsFile>(file: &mut F, page_id: u32, page: &Page) -> Result<(), PagerIoError> {
81    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
82    file.write_all(page.as_bytes())?;
83    Ok(())
84}
85
86pub fn read_superblock_zone<F: VfsFile>(
87    file: &mut F,
88) -> Result<[u8; PAGED_SUPERBLOCK_ZONE_SIZE], PagerIoError> {
89    let len = file.file_len()?;
90    let mut zone = [0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
91    let readable = usize::try_from(len.min(PAGED_SUPERBLOCK_ZONE_SIZE as u64))
92        .expect("readable prefix is bounded by the superblock zone");
93    if readable > 0 {
94        file.seek(SeekFrom::Start(0))?;
95        file.read_exact(&mut zone[..readable])?;
96    }
97    Ok(zone)
98}
99
100/// Seal and sync only the stale slot; the owner selects it and its generation.
101pub fn write_superblock_slot<F: VfsFile>(
102    file: &mut F,
103    slot: &mut [u8; PAGED_SUPERBLOCK_SLOT_SIZE],
104    copy_index: usize,
105    generation: u64,
106) -> Result<(), PagerIoError> {
107    reddb_file::seal_paged_superblock_slot(slot, copy_index, generation)
108        .map_err(|error| PagerIoError::InvalidDatabase(error.to_string()))?;
109    file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
110        copy_index,
111    )))?;
112    file.write_all(slot)?;
113    file.sync_all()?;
114    Ok(())
115}
116
117pub fn dwb_zone_offset() -> u64 {
118    u64::from(DWB_ZONE_START_PAGE) * PAGE_SIZE as u64
119}
120
121/// Clear the in-file DWB zone with valid reserved pages and sync it.
122pub fn clear_dwb_zone<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
123    for page_id in DWB_ZONE_START_PAGE..FIRST_ALLOCATABLE_PAGE {
124        let mut page = Page::new(PageType::Header, page_id);
125        page.update_checksum();
126        write_page(file, page_id, &page)?;
127    }
128    file.sync_all()?;
129    Ok(())
130}
131
132pub fn dwb_zone_is_clear(buffer: &[u8]) -> bool {
133    if buffer.len() != DWB_ZONE_BYTES {
134        return false;
135    }
136    for (index, chunk) in buffer.chunks_exact(PAGE_SIZE).enumerate() {
137        let Ok(page) = Page::from_slice(chunk) else {
138            return false;
139        };
140        if page.verify_checksum().is_err()
141            || page.page_id()
142                != DWB_ZONE_START_PAGE
143                    + u32::try_from(index).expect("DWB page index is bounded by its zone")
144            || page.page_type().ok() != Some(PageType::Header)
145        {
146            return false;
147        }
148    }
149    true
150}
151
152/// The caller must make the batch's WAL durable before entering this function.
153/// With DWB enabled: stage+sync, home pages+sync, clear+sync, for each bounded
154/// frame. With DWB disabled, the caller's proven atomic-write policy applies.
155pub fn write_pages<F: VfsFile>(
156    file: &mut F,
157    pages: &[(u32, Page)],
158    double_write: bool,
159) -> Result<(), PagerIoError> {
160    if !double_write {
161        for (page_id, page) in pages {
162            write_page(file, *page_id, page)?;
163        }
164        return Ok(());
165    }
166
167    for chunk in pages.chunks(DWB_MAX_PAGES_PER_FRAME.max(1)) {
168        let buffer = reddb_file::encode_paged_dwb_frame(
169            chunk
170                .iter()
171                .map(|(page_id, page)| (*page_id, page.as_bytes())),
172        );
173        assert!(buffer.len() <= DWB_ZONE_BYTES, "DWB frame fits its zone");
174        file.seek(SeekFrom::Start(dwb_zone_offset()))?;
175        file.write_all(&buffer)?;
176        file.sync_all()?;
177        for (page_id, page) in chunk {
178            write_page(file, *page_id, page)?;
179        }
180        file.sync_all()?;
181        clear_dwb_zone(file)?;
182    }
183    Ok(())
184}
185
186/// Replay a complete staged DWB frame before allowing page access. Incomplete
187/// staging is discarded as on the native path: home writes start only after
188/// the complete frame crossed its barrier. No database root is recreated.
189pub fn recover_from_dwb<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
190    if file.file_len()? < dwb_zone_offset() + DWB_ZONE_BYTES as u64 {
191        return Ok(());
192    }
193    let mut buffer = vec![0u8; DWB_ZONE_BYTES];
194    file.seek(SeekFrom::Start(dwb_zone_offset()))?;
195    file.read_exact(&mut buffer)?;
196    if buffer.get(..4) != Some(reddb_file::DWB_MAGIC.as_slice()) {
197        if dwb_zone_is_clear(&buffer) {
198            return Ok(());
199        }
200        return clear_dwb_zone(file);
201    }
202    let entries = match reddb_file::decode_paged_dwb_frame(&buffer) {
203        Ok(entries) => entries,
204        Err(_) => return clear_dwb_zone(file),
205    };
206    for entry in entries {
207        write_page(file, entry.page_id, &Page::from_bytes(entry.page))?;
208    }
209    file.sync_all()?;
210    clear_dwb_zone(file)
211}