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 = 64 * 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 reads: u64,
151}
152
153impl PageManager {
154 pub fn open(dir_path: &Path) -> MenteResult<Self> {
156 let file_path = dir_path.join("pages.db");
157 let exists = file_path.exists()
158 && std::fs::metadata(&file_path)
159 .map(|m| m.len() > 0)
160 .unwrap_or(false);
161
162 let mut file = OpenOptions::new()
163 .read(true)
164 .write(true)
165 .create(true)
166 .truncate(false)
167 .open(&file_path)?;
168
169 if exists {
170 let mut buf = [0u8; std::mem::size_of::<FileHeader>()];
171 file.seek(SeekFrom::Start(0))?;
172 file.read_exact(&mut buf)?;
173 let hdr: FileHeader = unsafe { std::ptr::read(buf.as_ptr() as *const FileHeader) };
174
175 if hdr.magic != MAGIC {
176 return Err(MenteError::Storage("invalid page file magic number".into()));
177 }
178 if hdr.version != VERSION {
179 return Err(MenteError::Storage(format!(
180 "unsupported page file version: {}",
181 hdr.version
182 )));
183 }
184
185 info!(page_count = hdr.page_count, "opened existing page file");
186 Ok(Self {
187 file,
188 page_count: hdr.page_count,
189 free_list_head: hdr.free_list_head,
190 reads: 0,
191 })
192 } else {
193 let mut pm = Self {
194 file,
195 page_count: 1,
196 free_list_head: 0,
197 reads: 0,
198 };
199 let mut header_page = Page::zeroed();
201 header_page.header.page_id = 0;
202 pm.write_page_raw(PageId(0), &header_page)?;
203 pm.write_file_header()?;
204 info!("created new page file");
205 Ok(pm)
206 }
207 }
208
209 pub fn reload_header(&mut self) -> MenteResult<()> {
211 let mut buf = [0u8; std::mem::size_of::<FileHeader>()];
212 self.file.seek(SeekFrom::Start(0))?;
213 self.file.read_exact(&mut buf)?;
214 let hdr: FileHeader = unsafe { std::ptr::read(buf.as_ptr() as *const FileHeader) };
215 if hdr.magic != MAGIC {
216 return Err(MenteError::Storage(
217 "invalid page file magic on reload".into(),
218 ));
219 }
220 self.page_count = hdr.page_count;
221 self.free_list_head = hdr.free_list_head;
222 debug!(page_count = self.page_count, "reloaded page file header");
223 Ok(())
224 }
225
226 fn write_file_header(&mut self) -> MenteResult<()> {
228 let hdr = FileHeader {
229 magic: MAGIC,
230 version: VERSION,
231 _pad: 0,
232 page_count: self.page_count,
233 free_list_head: self.free_list_head,
234 };
235 let bytes = unsafe {
236 std::slice::from_raw_parts(
237 &hdr as *const FileHeader as *const u8,
238 std::mem::size_of::<FileHeader>(),
239 )
240 };
241 self.file.seek(SeekFrom::Start(0))?;
242 self.file.write_all(bytes)?;
243 self.file.flush()?;
244 Ok(())
245 }
246
247 pub fn allocate_page(&mut self) -> MenteResult<PageId> {
249 if self.free_list_head != 0 {
250 let page_id = PageId(self.free_list_head);
251 let page = self.read_page(page_id)?;
252 let next_free = u64::from_le_bytes(page.data[..8].try_into().unwrap());
253 self.free_list_head = next_free;
254 self.write_file_header()?;
255 debug!(page_id = page_id.0, "allocated page from free list");
256 return Ok(page_id);
257 }
258
259 let page_id = PageId(self.page_count);
260 self.page_count += 1;
261
262 let mut page = Page::zeroed();
263 page.header.page_id = page_id.0;
264 page.header.page_type = PageType::Data as u8;
265 page.header.free_space = PAGE_DATA_SIZE as u16;
266 self.write_page_raw(page_id, &page)?;
267 self.write_file_header()?;
268
269 debug!(page_id = page_id.0, "allocated new page");
270 Ok(page_id)
271 }
272
273 pub fn read_page(&mut self, page_id: PageId) -> MenteResult<Box<Page>> {
275 if page_id.0 >= self.page_count {
276 return Err(MenteError::Storage(format!(
277 "page {} out of range (count={})",
278 page_id.0, self.page_count
279 )));
280 }
281
282 let offset = page_id.0 * PAGE_SIZE as u64;
283 let mut buf = [0u8; PAGE_SIZE];
284 self.file.seek(SeekFrom::Start(offset))?;
285 self.file.read_exact(&mut buf)?;
286 self.reads += 1;
287
288 let page = Page::from_bytes(&buf);
289 if page.header.checksum != 0 {
290 let expected = page.compute_checksum();
291 if page.header.checksum != expected {
292 return Err(MenteError::Storage(format!(
293 "page {} checksum mismatch (stored={:#x}, computed={:#x})",
294 page_id.0, page.header.checksum, expected
295 )));
296 }
297 }
298
299 trace!(page_id = page_id.0, "read page from disk");
300 Ok(Box::new(page))
301 }
302
303 pub fn write_page(&mut self, page_id: PageId, page: &Page) -> MenteResult<()> {
305 self.write_page_raw(page_id, page)
306 }
307
308 fn write_page_raw(&mut self, page_id: PageId, page: &Page) -> MenteResult<()> {
309 let offset = page_id.0 * PAGE_SIZE as u64;
310 self.file.seek(SeekFrom::Start(offset))?;
311 self.file.write_all(page.as_bytes())?;
312 trace!(page_id = page_id.0, "wrote page to disk");
313 Ok(())
314 }
315
316 pub fn free_page(&mut self, page_id: PageId) -> MenteResult<()> {
318 let mut page = Page::zeroed();
319 page.header.page_id = page_id.0;
320 page.header.page_type = PageType::Free as u8;
321 page.data[..8].copy_from_slice(&self.free_list_head.to_le_bytes());
323
324 self.write_page(page_id, &page)?;
325 self.free_list_head = page_id.0;
326 self.write_file_header()?;
327
328 debug!(page_id = page_id.0, "freed page");
329 Ok(())
330 }
331
332 pub fn rebuild_free_list(&mut self) -> MenteResult<()> {
338 let mut head = 0u64;
339 let mut freed = 0usize;
340 for i in (1..self.page_count).rev() {
341 let page = self.read_page(PageId(i))?;
342 if PageType::from(page.header.page_type) == PageType::Free {
343 let mut fresh = Page::zeroed();
344 fresh.header.page_id = i;
345 fresh.header.page_type = PageType::Free as u8;
346 fresh.data[..8].copy_from_slice(&head.to_le_bytes());
347 self.write_page_raw(PageId(i), &fresh)?;
348 head = i;
349 freed += 1;
350 }
351 }
352 self.free_list_head = head;
353 self.write_file_header()?;
354 if freed > 0 {
355 debug!(freed, head, "rebuilt free list from page types");
356 }
357 Ok(())
358 }
359
360 pub fn rebuild_free_list_from(
366 &mut self,
367 pages: &std::collections::BTreeSet<u64>,
368 ) -> MenteResult<bool> {
369 for &i in pages {
370 if i == 0 || i >= self.page_count {
371 return Ok(false);
372 }
373 let page = self.read_page(PageId(i))?;
374 if PageType::from(page.header.page_type) != PageType::Free {
375 return Ok(false);
376 }
377 }
378 let mut head = 0u64;
379 for &i in pages.iter().rev() {
380 let mut fresh = Page::zeroed();
381 fresh.header.page_id = i;
382 fresh.header.page_type = PageType::Free as u8;
383 fresh.data[..8].copy_from_slice(&head.to_le_bytes());
384 self.write_page_raw(PageId(i), &fresh)?;
385 head = i;
386 }
387 self.free_list_head = head;
388 self.write_file_header()?;
389 Ok(true)
390 }
391
392 pub fn reads(&self) -> u64 {
394 self.reads
395 }
396
397 pub fn page_count(&self) -> u64 {
399 self.page_count
400 }
401
402 pub fn sync(&mut self) -> MenteResult<()> {
404 self.file.sync_data()?;
405 Ok(())
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn setup() -> (tempfile::TempDir, PageManager) {
414 let dir = tempfile::tempdir().unwrap();
415 let pm = PageManager::open(dir.path()).unwrap();
416 (dir, pm)
417 }
418
419 #[test]
420 fn test_allocate_and_read_write() {
421 let (_dir, mut pm) = setup();
422
423 let pid = pm.allocate_page().unwrap();
424 assert_eq!(pid.0, 1); let mut page = Page::zeroed();
427 page.header.page_id = pid.0;
428 page.header.page_type = PageType::Data as u8;
429 page.data[0..5].copy_from_slice(b"hello");
430 pm.write_page(pid, &page).unwrap();
431
432 let loaded = pm.read_page(pid).unwrap();
433 assert_eq!(&loaded.data[0..5], b"hello");
434 }
435
436 #[test]
437 fn test_free_and_reuse() {
438 let (_dir, mut pm) = setup();
439
440 let p1 = pm.allocate_page().unwrap();
441 let p2 = pm.allocate_page().unwrap();
442 assert_eq!(p1.0, 1);
443 assert_eq!(p2.0, 2);
444
445 pm.free_page(p1).unwrap();
447 let p3 = pm.allocate_page().unwrap();
448 assert_eq!(p3.0, p1.0);
449
450 let p4 = pm.allocate_page().unwrap();
452 assert_eq!(p4.0, 3);
453 }
454
455 #[test]
456 fn test_multiple_free_reuse() {
457 let (_dir, mut pm) = setup();
458
459 let p1 = pm.allocate_page().unwrap();
460 let p2 = pm.allocate_page().unwrap();
461 let _p3 = pm.allocate_page().unwrap();
462
463 pm.free_page(p1).unwrap();
465 pm.free_page(p2).unwrap();
466
467 let a1 = pm.allocate_page().unwrap();
469 let a2 = pm.allocate_page().unwrap();
470 assert_eq!(a1.0, p2.0);
471 assert_eq!(a2.0, p1.0);
472
473 let a3 = pm.allocate_page().unwrap();
475 assert_eq!(a3.0, 4);
476 }
477
478 #[test]
479 fn test_reopen() {
480 let dir = tempfile::tempdir().unwrap();
481 let pid;
482 {
483 let mut pm = PageManager::open(dir.path()).unwrap();
484 pid = pm.allocate_page().unwrap();
485 let mut page = Page::zeroed();
486 page.header.page_id = pid.0;
487 page.data[0..4].copy_from_slice(b"test");
488 pm.write_page(pid, &page).unwrap();
489 pm.sync().unwrap();
490 }
491 {
492 let mut pm = PageManager::open(dir.path()).unwrap();
493 let page = pm.read_page(pid).unwrap();
494 assert_eq!(&page.data[0..4], b"test");
495 }
496 }
497
498 #[test]
499 fn test_out_of_range() {
500 let (_dir, mut pm) = setup();
501 assert!(pm.read_page(PageId(999)).is_err());
502 }
503
504 #[test]
505 fn test_checksum() {
506 let mut page = Page::zeroed();
507 page.header.page_id = 42;
508 page.data[0] = 0xFF;
509 let c1 = page.compute_checksum();
510 page.data[0] = 0x00;
511 let c2 = page.compute_checksum();
512 assert_ne!(c1, c2);
513 }
514
515 #[test]
516 fn test_checksum_verified_on_read() {
517 let dir = tempfile::tempdir().unwrap();
518 let pid;
519 {
520 let mut pm = PageManager::open(dir.path()).unwrap();
521 pid = pm.allocate_page().unwrap();
522 let mut page = Page::zeroed();
523 page.header.page_id = pid.0;
524 page.header.page_type = PageType::Data as u8;
525 page.data[0..5].copy_from_slice(b"valid");
526 page.header.checksum = page.compute_checksum();
527 pm.write_page(pid, &page).unwrap();
528 pm.sync().unwrap();
529 }
530 {
531 let mut pm = PageManager::open(dir.path()).unwrap();
533 let page = pm.read_page(pid).unwrap();
534 assert_eq!(&page.data[0..5], b"valid");
535 }
536 {
537 let data_path = dir.path().join("pages.db");
539 let mut raw = std::fs::read(&data_path).unwrap();
540 let offset = pid.0 as usize * PAGE_SIZE;
541 raw[offset + std::mem::size_of::<PageHeader>()] ^= 0xFF;
543 std::fs::write(&data_path, &raw).unwrap();
544
545 let mut pm = PageManager::open(dir.path()).unwrap();
546 let result = pm.read_page(pid);
547 assert!(result.is_err(), "corrupted page should fail checksum");
548 }
549 }
550}