reddb_engine/storage/
pager_io.rs1#![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 if verify_checksum && page_id != 0 {
74 page.verify_checksum()?;
75 }
76 Ok(page)
77}
78
79pub 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
100pub 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
121pub 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
152pub 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
186pub 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}