1use std::fs::{File, OpenOptions};
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::path::Path;
11
12use mentedb_core::error::{MenteError, MenteResult};
13use tracing::{debug, info, trace};
14
15pub const PAGE_SIZE: usize = 32 * 1024;
17
18const MAGIC: u64 = 0x4D454E_5445444231;
20
21const VERSION: u32 = 1;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct PageId(pub u64);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum PageType {
32 Free = 0,
33 Data = 1,
34 Index = 2,
35 Overflow = 3,
36}
37
38impl From<u8> for PageType {
39 fn from(v: u8) -> Self {
40 match v {
41 1 => PageType::Data,
42 2 => PageType::Index,
43 3 => PageType::Overflow,
44 _ => PageType::Free,
45 }
46 }
47}
48
49#[repr(C)]
51#[derive(Debug, Clone, Copy)]
52pub struct PageHeader {
53 pub page_id: u64,
55 pub lsn: u64,
57 pub checksum: u32,
59 pub free_space: u16,
61 pub num_slots: u16,
63 pub page_type: u8,
65 pub _padding: [u8; 7],
67}
68
69pub const HEADER_SIZE: usize = std::mem::size_of::<PageHeader>();
71
72pub const PAGE_DATA_SIZE: usize = PAGE_SIZE - HEADER_SIZE;
74
75#[repr(C, align(4096))]
77pub struct Page {
78 pub header: PageHeader,
80 pub data: [u8; PAGE_DATA_SIZE],
82}
83
84impl std::fmt::Debug for Page {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("Page")
87 .field("header", &self.header)
88 .field("data_len", &self.data.len())
89 .finish()
90 }
91}
92
93impl Clone for Page {
94 fn clone(&self) -> Self {
95 let mut new_page = Page::zeroed();
96 new_page.header = self.header;
97 new_page.data.copy_from_slice(&self.data);
98 new_page
99 }
100}
101
102impl Page {
103 pub fn zeroed() -> Self {
105 unsafe { std::mem::zeroed() }
107 }
108
109 fn as_bytes(&self) -> &[u8; PAGE_SIZE] {
111 unsafe { &*(self as *const Page as *const [u8; PAGE_SIZE]) }
113 }
114
115 fn from_bytes(bytes: &[u8; PAGE_SIZE]) -> Self {
117 unsafe { std::ptr::read(bytes.as_ptr() as *const Page) }
119 }
120
121 pub fn compute_checksum(&self) -> u32 {
123 let mut h = crc32fast::Hasher::new();
124 h.update(&self.header.page_id.to_le_bytes());
125 h.update(&self.header.lsn.to_le_bytes());
126 h.update(&self.header.free_space.to_le_bytes());
127 h.update(&self.header.num_slots.to_le_bytes());
128 h.update(&[self.header.page_type]);
129 h.update(&self.data);
130 h.finalize()
131 }
132}
133
134#[repr(C)]
136struct FileHeader {
137 magic: u64,
138 version: u32,
139 _pad: u32,
140 page_count: u64,
141 free_list_head: u64,
142}
143
144pub struct PageManager {
146 file: File,
147 page_count: u64,
148 free_list_head: u64,
149}
150
151impl PageManager {
152 pub fn open(dir_path: &Path) -> MenteResult<Self> {
154 let file_path = dir_path.join("pages.db");
155 let exists = file_path.exists()
156 && std::fs::metadata(&file_path)
157 .map(|m| m.len() > 0)
158 .unwrap_or(false);
159
160 let mut file = OpenOptions::new()
161 .read(true)
162 .write(true)
163 .create(true)
164 .truncate(false)
165 .open(&file_path)?;
166
167 if exists {
168 let mut buf = [0u8; std::mem::size_of::<FileHeader>()];
169 file.seek(SeekFrom::Start(0))?;
170 file.read_exact(&mut buf)?;
171 let hdr: FileHeader = unsafe { std::ptr::read(buf.as_ptr() as *const FileHeader) };
172
173 if hdr.magic != MAGIC {
174 return Err(MenteError::Storage("invalid page file magic number".into()));
175 }
176 if hdr.version != VERSION {
177 return Err(MenteError::Storage(format!(
178 "unsupported page file version: {}",
179 hdr.version
180 )));
181 }
182
183 info!(page_count = hdr.page_count, "opened existing page file");
184 Ok(Self {
185 file,
186 page_count: hdr.page_count,
187 free_list_head: hdr.free_list_head,
188 })
189 } else {
190 let mut pm = Self {
191 file,
192 page_count: 1,
193 free_list_head: 0,
194 };
195 let mut header_page = Page::zeroed();
197 header_page.header.page_id = 0;
198 pm.write_page_raw(PageId(0), &header_page)?;
199 pm.write_file_header()?;
200 info!("created new page file");
201 Ok(pm)
202 }
203 }
204
205 fn write_file_header(&mut self) -> MenteResult<()> {
207 let hdr = FileHeader {
208 magic: MAGIC,
209 version: VERSION,
210 _pad: 0,
211 page_count: self.page_count,
212 free_list_head: self.free_list_head,
213 };
214 let bytes = unsafe {
215 std::slice::from_raw_parts(
216 &hdr as *const FileHeader as *const u8,
217 std::mem::size_of::<FileHeader>(),
218 )
219 };
220 self.file.seek(SeekFrom::Start(0))?;
221 self.file.write_all(bytes)?;
222 self.file.flush()?;
223 Ok(())
224 }
225
226 pub fn allocate_page(&mut self) -> MenteResult<PageId> {
228 if self.free_list_head != 0 {
229 let page_id = PageId(self.free_list_head);
230 let page = self.read_page(page_id)?;
231 let next_free = u64::from_le_bytes(page.data[..8].try_into().unwrap());
232 self.free_list_head = next_free;
233 self.write_file_header()?;
234 debug!(page_id = page_id.0, "allocated page from free list");
235 return Ok(page_id);
236 }
237
238 let page_id = PageId(self.page_count);
239 self.page_count += 1;
240
241 let mut page = Page::zeroed();
242 page.header.page_id = page_id.0;
243 page.header.page_type = PageType::Data as u8;
244 page.header.free_space = PAGE_DATA_SIZE as u16;
245 self.write_page_raw(page_id, &page)?;
246 self.write_file_header()?;
247
248 debug!(page_id = page_id.0, "allocated new page");
249 Ok(page_id)
250 }
251
252 pub fn read_page(&mut self, page_id: PageId) -> MenteResult<Box<Page>> {
254 if page_id.0 >= self.page_count {
255 return Err(MenteError::Storage(format!(
256 "page {} out of range (count={})",
257 page_id.0, self.page_count
258 )));
259 }
260
261 let offset = page_id.0 * PAGE_SIZE as u64;
262 let mut buf = [0u8; PAGE_SIZE];
263 self.file.seek(SeekFrom::Start(offset))?;
264 self.file.read_exact(&mut buf)?;
265
266 trace!(page_id = page_id.0, "read page from disk");
267 Ok(Box::new(Page::from_bytes(&buf)))
268 }
269
270 pub fn write_page(&mut self, page_id: PageId, page: &Page) -> MenteResult<()> {
272 self.write_page_raw(page_id, page)
273 }
274
275 fn write_page_raw(&mut self, page_id: PageId, page: &Page) -> MenteResult<()> {
276 let offset = page_id.0 * PAGE_SIZE as u64;
277 self.file.seek(SeekFrom::Start(offset))?;
278 self.file.write_all(page.as_bytes())?;
279 trace!(page_id = page_id.0, "wrote page to disk");
280 Ok(())
281 }
282
283 pub fn free_page(&mut self, page_id: PageId) -> MenteResult<()> {
285 let mut page = Page::zeroed();
286 page.header.page_id = page_id.0;
287 page.header.page_type = PageType::Free as u8;
288 page.data[..8].copy_from_slice(&self.free_list_head.to_le_bytes());
290
291 self.write_page(page_id, &page)?;
292 self.free_list_head = page_id.0;
293 self.write_file_header()?;
294
295 debug!(page_id = page_id.0, "freed page");
296 Ok(())
297 }
298
299 pub fn page_count(&self) -> u64 {
301 self.page_count
302 }
303
304 pub fn sync(&mut self) -> MenteResult<()> {
306 self.file.sync_data()?;
307 Ok(())
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn setup() -> (tempfile::TempDir, PageManager) {
316 let dir = tempfile::tempdir().unwrap();
317 let pm = PageManager::open(dir.path()).unwrap();
318 (dir, pm)
319 }
320
321 #[test]
322 fn test_allocate_and_read_write() {
323 let (_dir, mut pm) = setup();
324
325 let pid = pm.allocate_page().unwrap();
326 assert_eq!(pid.0, 1); let mut page = Page::zeroed();
329 page.header.page_id = pid.0;
330 page.header.page_type = PageType::Data as u8;
331 page.data[0..5].copy_from_slice(b"hello");
332 pm.write_page(pid, &page).unwrap();
333
334 let loaded = pm.read_page(pid).unwrap();
335 assert_eq!(&loaded.data[0..5], b"hello");
336 }
337
338 #[test]
339 fn test_free_and_reuse() {
340 let (_dir, mut pm) = setup();
341
342 let p1 = pm.allocate_page().unwrap();
343 let p2 = pm.allocate_page().unwrap();
344 assert_eq!(p1.0, 1);
345 assert_eq!(p2.0, 2);
346
347 pm.free_page(p1).unwrap();
349 let p3 = pm.allocate_page().unwrap();
350 assert_eq!(p3.0, p1.0);
351
352 let p4 = pm.allocate_page().unwrap();
354 assert_eq!(p4.0, 3);
355 }
356
357 #[test]
358 fn test_multiple_free_reuse() {
359 let (_dir, mut pm) = setup();
360
361 let p1 = pm.allocate_page().unwrap();
362 let p2 = pm.allocate_page().unwrap();
363 let _p3 = pm.allocate_page().unwrap();
364
365 pm.free_page(p1).unwrap();
367 pm.free_page(p2).unwrap();
368
369 let a1 = pm.allocate_page().unwrap();
371 let a2 = pm.allocate_page().unwrap();
372 assert_eq!(a1.0, p2.0);
373 assert_eq!(a2.0, p1.0);
374
375 let a3 = pm.allocate_page().unwrap();
377 assert_eq!(a3.0, 4);
378 }
379
380 #[test]
381 fn test_reopen() {
382 let dir = tempfile::tempdir().unwrap();
383 let pid;
384 {
385 let mut pm = PageManager::open(dir.path()).unwrap();
386 pid = pm.allocate_page().unwrap();
387 let mut page = Page::zeroed();
388 page.header.page_id = pid.0;
389 page.data[0..4].copy_from_slice(b"test");
390 pm.write_page(pid, &page).unwrap();
391 pm.sync().unwrap();
392 }
393 {
394 let mut pm = PageManager::open(dir.path()).unwrap();
395 let page = pm.read_page(pid).unwrap();
396 assert_eq!(&page.data[0..4], b"test");
397 }
398 }
399
400 #[test]
401 fn test_out_of_range() {
402 let (_dir, mut pm) = setup();
403 assert!(pm.read_page(PageId(999)).is_err());
404 }
405
406 #[test]
407 fn test_checksum() {
408 let mut page = Page::zeroed();
409 page.header.page_id = 42;
410 page.data[0] = 0xFF;
411 let c1 = page.compute_checksum();
412 page.data[0] = 0x00;
413 let c2 = page.compute_checksum();
414 assert_ne!(c1, c2);
415 }
416}