use std::fs::File;
use std::path::Path;
use crate::error::{PageError, PageResult};
use crate::page::{DEFAULT_PAGE_SIZE, Page, PageId, PageSize};
use crate::sys;
#[derive(Debug, Clone)]
#[must_use = "PageFileOptions does nothing until `open` is called"]
pub struct PageFileOptions {
page_size: PageSize,
direct_io: bool,
create: bool,
}
impl Default for PageFileOptions {
fn default() -> Self {
Self {
page_size: DEFAULT_PAGE_SIZE,
direct_io: true,
create: true,
}
}
}
impl PageFileOptions {
pub fn new() -> Self {
Self::default()
}
pub fn page_size(mut self, page_size: PageSize) -> Self {
self.page_size = page_size;
self
}
pub fn direct_io(mut self, enabled: bool) -> Self {
self.direct_io = enabled;
self
}
pub fn create(mut self, create: bool) -> Self {
self.create = create;
self
}
pub fn open<P: AsRef<Path>>(self, path: P) -> PageResult<PageFile> {
let file = sys::open(path.as_ref(), self.direct_io, self.create)?;
Ok(PageFile {
file,
page_size: self.page_size,
})
}
}
#[derive(Debug)]
pub struct PageFile {
file: File,
page_size: PageSize,
}
impl PageFile {
pub fn open<P: AsRef<Path>>(path: P, page_size: PageSize) -> PageResult<Self> {
PageFileOptions::new().page_size(page_size).open(path)
}
#[inline]
#[must_use]
pub fn page_size(&self) -> usize {
self.page_size.get()
}
pub fn page_count(&self) -> PageResult<u64> {
let len = self.file.metadata()?.len();
Ok(len / self.page_size.get() as u64)
}
#[must_use]
pub fn allocate_page(&self) -> Page {
Page::new(self.page_size)
}
pub fn read_page(&self, id: PageId) -> PageResult<Page> {
let mut page = Page::new(self.page_size);
let offset = id.byte_offset(self.page_size.get());
let got = sys::read_at_full(&self.file, page.as_bytes_mut(), offset)?;
if got != self.page_size.get() {
return Err(PageError::ShortRead {
page_id: id.get(),
got,
page_size: self.page_size.get(),
});
}
page.verify(Some(id))?;
Ok(page)
}
pub fn write_page(&self, id: PageId, page: &mut Page) -> PageResult<()> {
if page.page_size() != self.page_size.get() {
return Err(PageError::InvalidPageSize {
size: page.page_size(),
});
}
page.stamp(id);
let offset = id.byte_offset(self.page_size.get());
sys::write_all_at(&self.file, page.as_bytes(), offset)?;
Ok(())
}
pub fn sync(&self) -> PageResult<()> {
sys::sync_data(&self.file)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::page::Lsn;
fn temp_file() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("test.pages");
(dir, path)
}
fn open_buffered(path: &Path) -> PageFile {
PageFileOptions::new()
.direct_io(false)
.open(path)
.expect("open")
}
#[test]
fn test_write_read_roundtrip() {
let (_dir, path) = temp_file();
let file = open_buffered(&path);
let mut page = file.allocate_page();
page.set_lsn(Lsn::new(11));
page.payload_mut()[..5].copy_from_slice(b"world");
file.write_page(PageId::new(2), &mut page).expect("write");
file.sync().expect("sync");
let got = file.read_page(PageId::new(2)).expect("read");
assert_eq!(got.id(), PageId::new(2));
assert_eq!(got.lsn(), Lsn::new(11));
assert_eq!(&got.payload()[..5], b"world");
}
#[test]
fn test_read_past_end_is_short_read() {
let (_dir, path) = temp_file();
let file = open_buffered(&path);
assert!(matches!(
file.read_page(PageId::new(0)),
Err(PageError::ShortRead { .. })
));
}
#[test]
fn test_page_count_tracks_writes() {
let (_dir, path) = temp_file();
let file = open_buffered(&path);
assert_eq!(file.page_count().expect("count"), 0);
let mut page = file.allocate_page();
file.write_page(PageId::new(0), &mut page).expect("write");
assert_eq!(file.page_count().expect("count"), 1);
file.write_page(PageId::new(4), &mut page).expect("write");
assert_eq!(file.page_count().expect("count"), 5);
}
#[test]
fn test_corruption_on_disk_is_detected() {
let (_dir, path) = temp_file();
{
let file = open_buffered(&path);
let mut page = file.allocate_page();
page.payload_mut()[0] = 0x42;
file.write_page(PageId::new(0), &mut page).expect("write");
file.sync().expect("sync");
}
{
use std::io::{Read, Seek, SeekFrom, Write};
let mut raw = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("reopen");
let _ = raw.seek(SeekFrom::Start(40)).expect("seek");
let mut b = [0u8; 1];
let _ = raw.read_exact(&mut b);
b[0] ^= 0xFF;
let _ = raw.seek(SeekFrom::Start(40)).expect("seek");
raw.write_all(&b).expect("write");
raw.sync_all().expect("sync");
}
let file = open_buffered(&path);
assert!(matches!(
file.read_page(PageId::new(0)),
Err(PageError::ChecksumMismatch { .. })
));
}
#[test]
fn test_write_rejects_wrong_page_size() {
let (_dir, path) = temp_file();
let file = open_buffered(&path);
let mut wrong = Page::new(PageSize::new(8192).expect("valid"));
assert!(matches!(
file.write_page(PageId::new(0), &mut wrong),
Err(PageError::InvalidPageSize { size: 8192 })
));
}
}