reddb_engine/storage/
pager_io.rs1#![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 if verify_checksum && page_id != 0 {
77 page.verify_checksum()?;
78 check_page_identity(&page, page_id)?;
79 }
80 Ok(page)
81}
82
83pub 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
94pub 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
115pub 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
136pub 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
167pub 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 if !pages.is_empty() {
203 clear_dwb_zone(file)?;
204 }
205 Ok(())
206}
207
208pub fn recover_from_dwb<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
212 staging::recover(file)
213}
214
215pub fn read_dwb_pages<F: VfsFile>(file: &mut F) -> Result<Vec<(u32, Page)>, PagerIoError> {
219 staging::read_pages(file)
220}