#![deny(
clippy::unwrap_used,
clippy::too_many_lines,
clippy::cast_possible_truncation
)]
use super::page::{Page, PageError, PageType, PAGE_SIZE};
use reddb_file::{VfsFile, PAGED_SUPERBLOCK_SLOT_SIZE, PAGED_SUPERBLOCK_ZONE_SIZE};
use std::io::{self, SeekFrom};
pub const DWB_ZONE_START_PAGE: u32 = 3;
pub const DWB_ZONE_PAGES: u32 = 64;
pub const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
pub const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
pub const DWB_MAX_PAGES_PER_FRAME: usize =
(DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
#[derive(Debug)]
pub enum PagerIoError {
Io(io::Error),
Page(PageError),
InvalidDatabase(String),
}
impl std::fmt::Display for PagerIoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(f, "I/O error: {error}"),
Self::Page(error) => write!(f, "Page error: {error}"),
Self::InvalidDatabase(message) => write!(f, "Invalid database: {message}"),
}
}
}
impl std::error::Error for PagerIoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Page(error) => Some(error),
Self::InvalidDatabase(_) => None,
}
}
}
impl From<io::Error> for PagerIoError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<PageError> for PagerIoError {
fn from(error: PageError) -> Self {
Self::Page(error)
}
}
pub fn read_page<F: VfsFile>(
file: &mut F,
page_id: u32,
verify_checksum: bool,
) -> Result<Page, PagerIoError> {
file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
let mut buffer = [0u8; PAGE_SIZE];
file.read_exact(&mut buffer)?;
let page = Page::from_bytes(buffer);
if verify_checksum && page_id != 0 {
page.verify_checksum()?;
}
Ok(page)
}
pub fn write_page<F: VfsFile>(file: &mut F, page_id: u32, page: &Page) -> Result<(), PagerIoError> {
file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
file.write_all(page.as_bytes())?;
Ok(())
}
pub fn read_superblock_zone<F: VfsFile>(
file: &mut F,
) -> Result<[u8; PAGED_SUPERBLOCK_ZONE_SIZE], PagerIoError> {
let len = file.file_len()?;
let mut zone = [0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
let readable = usize::try_from(len.min(PAGED_SUPERBLOCK_ZONE_SIZE as u64))
.expect("readable prefix is bounded by the superblock zone");
if readable > 0 {
file.seek(SeekFrom::Start(0))?;
file.read_exact(&mut zone[..readable])?;
}
Ok(zone)
}
pub fn write_superblock_slot<F: VfsFile>(
file: &mut F,
slot: &mut [u8; PAGED_SUPERBLOCK_SLOT_SIZE],
copy_index: usize,
generation: u64,
) -> Result<(), PagerIoError> {
reddb_file::seal_paged_superblock_slot(slot, copy_index, generation)
.map_err(|error| PagerIoError::InvalidDatabase(error.to_string()))?;
file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
copy_index,
)))?;
file.write_all(slot)?;
file.sync_all()?;
Ok(())
}
pub fn dwb_zone_offset() -> u64 {
u64::from(DWB_ZONE_START_PAGE) * PAGE_SIZE as u64
}
pub fn clear_dwb_zone<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
for page_id in DWB_ZONE_START_PAGE..FIRST_ALLOCATABLE_PAGE {
let mut page = Page::new(PageType::Header, page_id);
page.update_checksum();
write_page(file, page_id, &page)?;
}
file.sync_all()?;
Ok(())
}
pub fn dwb_zone_is_clear(buffer: &[u8]) -> bool {
if buffer.len() != DWB_ZONE_BYTES {
return false;
}
for (index, chunk) in buffer.chunks_exact(PAGE_SIZE).enumerate() {
let Ok(page) = Page::from_slice(chunk) else {
return false;
};
if page.verify_checksum().is_err()
|| page.page_id()
!= DWB_ZONE_START_PAGE
+ u32::try_from(index).expect("DWB page index is bounded by its zone")
|| page.page_type().ok() != Some(PageType::Header)
{
return false;
}
}
true
}
pub fn write_pages<F: VfsFile>(
file: &mut F,
pages: &[(u32, Page)],
double_write: bool,
) -> Result<(), PagerIoError> {
if !double_write {
for (page_id, page) in pages {
write_page(file, *page_id, page)?;
}
return Ok(());
}
for chunk in pages.chunks(DWB_MAX_PAGES_PER_FRAME.max(1)) {
let buffer = reddb_file::encode_paged_dwb_frame(
chunk
.iter()
.map(|(page_id, page)| (*page_id, page.as_bytes())),
);
assert!(buffer.len() <= DWB_ZONE_BYTES, "DWB frame fits its zone");
file.seek(SeekFrom::Start(dwb_zone_offset()))?;
file.write_all(&buffer)?;
file.sync_all()?;
for (page_id, page) in chunk {
write_page(file, *page_id, page)?;
}
file.sync_all()?;
clear_dwb_zone(file)?;
}
Ok(())
}
pub fn recover_from_dwb<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
if file.file_len()? < dwb_zone_offset() + DWB_ZONE_BYTES as u64 {
return Ok(());
}
let mut buffer = vec![0u8; DWB_ZONE_BYTES];
file.seek(SeekFrom::Start(dwb_zone_offset()))?;
file.read_exact(&mut buffer)?;
if buffer.get(..4) != Some(reddb_file::DWB_MAGIC.as_slice()) {
if dwb_zone_is_clear(&buffer) {
return Ok(());
}
return clear_dwb_zone(file);
}
let entries = match reddb_file::decode_paged_dwb_frame(&buffer) {
Ok(entries) => entries,
Err(_) => return clear_dwb_zone(file),
};
for entry in entries {
write_page(file, entry.page_id, &Page::from_bytes(entry.page))?;
}
file.sync_all()?;
clear_dwb_zone(file)
}