use crate::error::{corrupt, too_big, DbResult};
use crate::ids::PageId;
pub const MIN_PAGE_SIZE: u32 = 512;
pub const MAX_PAGE_SIZE: u32 = 65_536;
pub const MIN_USABLE_SIZE: u32 = 480;
pub const HEADER_SIZE: u32 = 100;
pub const ENCODED_MAX_PAGE_SIZE: u16 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PageSize(u32);
impl PageSize {
pub const DEFAULT: PageSize = PageSize(4096);
pub fn new(bytes: u32) -> DbResult<PageSize> {
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&bytes) {
return Err(corrupt("page size is outside the 512..65536 range"));
}
if !bytes.is_power_of_two() {
return Err(corrupt("page size is not a power of two"));
}
Ok(PageSize(bytes))
}
pub fn from_encoded(encoded: u16) -> DbResult<PageSize> {
match encoded {
ENCODED_MAX_PAGE_SIZE => Ok(PageSize(MAX_PAGE_SIZE)),
other => PageSize::new(u32::from(other)),
}
}
pub fn to_encoded(self) -> u16 {
if self.0 == MAX_PAGE_SIZE {
return ENCODED_MAX_PAGE_SIZE;
}
self.0 as u16
}
pub fn bytes(self) -> u32 {
self.0
}
pub fn as_usize(self) -> usize {
self.0 as usize
}
pub fn usable(self, reserved: u8) -> DbResult<u32> {
let usable = self
.0
.checked_sub(u32::from(reserved))
.ok_or_else(|| corrupt("reserved region is larger than the page"))?;
if usable < MIN_USABLE_SIZE {
return Err(corrupt("usable page size is below the 480-byte minimum"));
}
Ok(usable)
}
}
pub fn page_offset(size: PageSize, page: PageId) -> DbResult<u64> {
u64::from(page.index())
.checked_mul(u64::from(size.bytes()))
.ok_or_else(|| too_big("page offset does not fit in a 64-bit file offset"))
}
pub fn offset_within_page(size: PageSize, page: PageId, offset: u32) -> DbResult<u64> {
if offset >= size.bytes() {
return Err(corrupt("offset reaches past the end of the page"));
}
page_offset(size, page)?
.checked_add(u64::from(offset))
.ok_or_else(|| too_big("offset within page does not fit in a file offset"))
}
pub fn page_count(size: PageSize, bytes: u64) -> u32 {
let pages = bytes.checked_div(u64::from(size.bytes())).unwrap_or(0);
pages.min(u64::from(u32::MAX)) as u32
}
pub fn file_size(size: PageSize, pages: u32) -> DbResult<u64> {
u64::from(pages)
.checked_mul(u64::from(size.bytes()))
.ok_or_else(|| too_big("database size does not fit in a 64-bit file offset"))
}
pub fn is_page_aligned(size: PageSize, bytes: u64) -> bool {
bytes.is_multiple_of(u64::from(size.bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::PrimaryCode;
#[test]
fn page_sizes_follow_the_file_format_rules() {
for shift in 9..=16 {
let bytes = 1u32 << shift;
assert_eq!(PageSize::new(bytes).unwrap().bytes(), bytes);
}
for bytes in [0, 1, 256, 511, 513, 1000, 65_535, 131_072] {
assert_eq!(
PageSize::new(bytes).unwrap_err().code(),
PrimaryCode::Corrupt
);
}
}
#[test]
fn the_largest_page_size_round_trips_through_its_encoded_form() {
let largest = PageSize::new(MAX_PAGE_SIZE).unwrap();
assert_eq!(largest.to_encoded(), 1);
assert_eq!(PageSize::from_encoded(1).unwrap(), largest);
let ordinary = PageSize::new(4096).unwrap();
assert_eq!(ordinary.to_encoded(), 4096);
assert_eq!(PageSize::from_encoded(4096).unwrap(), ordinary);
assert_eq!(
PageSize::from_encoded(0).unwrap_err().code(),
PrimaryCode::Corrupt
);
}
#[test]
fn usable_size_respects_the_reserved_region_and_its_floor() {
let small = PageSize::new(512).unwrap();
assert_eq!(small.usable(0).unwrap(), 512);
assert_eq!(small.usable(32).unwrap(), 480);
assert_eq!(small.usable(33).unwrap_err().code(), PrimaryCode::Corrupt);
let large = PageSize::new(65_536).unwrap();
assert_eq!(large.usable(255).unwrap(), 65_281);
}
#[test]
fn page_offsets_are_one_based() {
let size = PageSize::new(4096).unwrap();
assert_eq!(page_offset(size, PageId::FIRST).unwrap(), 0);
assert_eq!(page_offset(size, PageId::new(2).unwrap()).unwrap(), 4096);
assert_eq!(
offset_within_page(size, PageId::new(3).unwrap(), 100).unwrap(),
8292
);
assert_eq!(
offset_within_page(size, PageId::FIRST, 4096)
.unwrap_err()
.code(),
PrimaryCode::Corrupt
);
}
#[test]
fn the_largest_addressable_page_does_not_overflow() {
let size = PageSize::new(MAX_PAGE_SIZE).unwrap();
let last = PageId::new(u32::MAX).unwrap();
let expected = u64::from(u32::MAX - 1) * u64::from(MAX_PAGE_SIZE);
assert_eq!(page_offset(size, last).unwrap(), expected);
assert_eq!(
file_size(size, u32::MAX).unwrap(),
u64::from(u32::MAX) * 65_536
);
}
#[test]
fn page_count_ignores_a_trailing_partial_page() {
let size = PageSize::new(1024).unwrap();
assert_eq!(page_count(size, 0), 0);
assert_eq!(page_count(size, 1023), 0);
assert_eq!(page_count(size, 1024), 1);
assert_eq!(page_count(size, 2047), 1);
assert!(is_page_aligned(size, 2048));
assert!(!is_page_aligned(size, 2049));
}
}