use crate::page::{self, PageType, CHECKSUM_OFFSET, DATA_PAGE_HEADER_SIZE, PAGE_SIZE};
pub(crate) const SLOT_ENTRY_SIZE: usize = 6; const SLOT_FLAG_LIVE: u16 = 0x0001;
pub struct DataPage;
#[allow(dead_code)]
impl DataPage {
pub fn init_page(buf: &mut [u8; PAGE_SIZE]) {
buf.fill(0);
buf[0] = PageType::Data as u8;
buf[1] = page::current_version(page::PageType::Data);
let free_start = DATA_PAGE_HEADER_SIZE as u16;
buf[4..6].copy_from_slice(&free_start.to_le_bytes());
let free_end = CHECKSUM_OFFSET as u16;
buf[6..8].copy_from_slice(&free_end.to_le_bytes());
}
pub fn slot_count(buf: &[u8; PAGE_SIZE]) -> u16 {
u16::from_le_bytes(buf[2..4].try_into().unwrap())
}
pub fn free_space(buf: &[u8; PAGE_SIZE]) -> usize {
match Self::validate_header(buf) {
Some((free_start, free_end, _)) => free_end - free_start,
None => 0,
}
}
fn validate_header(buf: &[u8; PAGE_SIZE]) -> Option<(usize, usize, u16)> {
if buf[0] != PageType::Data as u8 {
return None;
}
let slot_count = u16::from_le_bytes(buf[2..4].try_into().unwrap());
let free_start = u16::from_le_bytes(buf[4..6].try_into().unwrap()) as usize;
let free_end = u16::from_le_bytes(buf[6..8].try_into().unwrap()) as usize;
if free_start < DATA_PAGE_HEADER_SIZE {
return None;
}
if free_end > CHECKSUM_OFFSET {
return None;
}
if free_start > free_end {
return None;
}
let dir_end = DATA_PAGE_HEADER_SIZE
.checked_add((slot_count as usize).checked_mul(SLOT_ENTRY_SIZE)?)?;
if dir_end > free_start {
return None;
}
Some((free_start, free_end, slot_count))
}
pub fn insert(buf: &mut [u8; PAGE_SIZE], value: &[u8]) -> Option<u16> {
let (free_start, free_end, slot_count) = Self::validate_header(buf)?;
let needed = SLOT_ENTRY_SIZE + value.len();
if free_end - free_start < needed {
return None;
}
let data_offset = free_end - value.len();
buf[data_offset..data_offset + value.len()].copy_from_slice(value);
let slot_offset = free_start;
buf[slot_offset..slot_offset + 2].copy_from_slice(&(data_offset as u16).to_le_bytes());
buf[slot_offset + 2..slot_offset + 4].copy_from_slice(&(value.len() as u16).to_le_bytes());
buf[slot_offset + 4..slot_offset + 6].copy_from_slice(&SLOT_FLAG_LIVE.to_le_bytes());
let new_slot_count = slot_count + 1;
buf[2..4].copy_from_slice(&new_slot_count.to_le_bytes());
let new_free_start = (free_start + SLOT_ENTRY_SIZE) as u16;
buf[4..6].copy_from_slice(&new_free_start.to_le_bytes());
let new_free_end = data_offset as u16;
buf[6..8].copy_from_slice(&new_free_end.to_le_bytes());
Some(slot_count) }
pub fn read(buf: &[u8; PAGE_SIZE], slot: u16) -> Option<&[u8]> {
let (_, _, slot_count) = Self::validate_header(buf)?;
if slot >= slot_count {
return None;
}
let (offset, length, flags) = Self::read_slot_entry(buf, slot)?;
if flags != SLOT_FLAG_LIVE {
return None;
}
Some(&buf[offset..offset + length])
}
pub fn used_space(buf: &[u8; PAGE_SIZE]) -> usize {
let Some((_, _, count)) = Self::validate_header(buf) else {
return 0;
};
let mut data_bytes = 0usize;
let mut live_slots = 0usize;
for i in 0..count {
let Some((_, length, flags)) = Self::read_slot_entry(buf, i) else {
return 0;
};
if flags == SLOT_FLAG_LIVE {
data_bytes += length;
live_slots += 1;
}
}
live_slots * SLOT_ENTRY_SIZE + data_bytes
}
pub fn iter_live(buf: &[u8; PAGE_SIZE]) -> Vec<(u16, &[u8])> {
let Some((_, _, count)) = Self::validate_header(buf) else {
return Vec::new();
};
let mut result = Vec::new();
for i in 0..count {
let Some((offset, length, flags)) = Self::read_slot_entry(buf, i) else {
return Vec::new();
};
if flags == SLOT_FLAG_LIVE {
result.push((i, &buf[offset..offset + length]));
}
}
result
}
fn read_slot_entry(buf: &[u8; PAGE_SIZE], slot: u16) -> Option<(usize, usize, u16)> {
let base =
DATA_PAGE_HEADER_SIZE.checked_add((slot as usize).checked_mul(SLOT_ENTRY_SIZE)?)?;
if base.checked_add(SLOT_ENTRY_SIZE)? > CHECKSUM_OFFSET {
return None;
}
let offset = u16::from_le_bytes(buf[base..base + 2].try_into().unwrap()) as usize;
let length = u16::from_le_bytes(buf[base + 2..base + 4].try_into().unwrap()) as usize;
let flags = u16::from_le_bytes(buf[base + 4..base + 6].try_into().unwrap());
if offset < DATA_PAGE_HEADER_SIZE {
return None;
}
let end = offset.checked_add(length)?;
if end > CHECKSUM_OFFSET {
return None;
}
Some((offset, length, flags))
}
}
#[cfg(test)]
mod corruption_tests {
use super::*;
fn fresh_page() -> [u8; PAGE_SIZE] {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
buf
}
#[test]
fn reserved_common_header_bytes_stay_zero() {
let mut buf = fresh_page();
assert_eq!(
&buf[8..16],
&[0u8; 8],
"init_page must zero the reserved 8..16"
);
DataPage::insert(&mut buf, b"hello world").unwrap();
assert_eq!(
&buf[8..16],
&[0u8; 8],
"insert must not write into the reserved 8..16"
);
assert_eq!(
DataPage::slot_count(&buf),
1,
"the value should have landed in slot 0"
);
}
#[test]
fn read_returns_none_on_corrupt_slot_count() {
let mut buf = fresh_page();
buf[2..4].copy_from_slice(&0xFFFFu16.to_le_bytes());
assert!(DataPage::read(&buf, 0).is_none());
assert!(DataPage::read(&buf, 1365).is_none());
assert!(DataPage::read(&buf, 0xFFFE).is_none());
}
#[test]
fn read_returns_none_on_out_of_range_offset_length() {
let mut buf = fresh_page();
buf[2..4].copy_from_slice(&1u16.to_le_bytes());
let free_start = (DATA_PAGE_HEADER_SIZE + SLOT_ENTRY_SIZE) as u16;
buf[4..6].copy_from_slice(&free_start.to_le_bytes());
let base = DATA_PAGE_HEADER_SIZE;
buf[base..base + 2].copy_from_slice(&8000u16.to_le_bytes());
buf[base + 2..base + 4].copy_from_slice(&500u16.to_le_bytes());
buf[base + 4..base + 6].copy_from_slice(&SLOT_FLAG_LIVE.to_le_bytes());
assert!(DataPage::read(&buf, 0).is_none());
}
#[test]
fn read_returns_none_on_offset_below_header() {
let mut buf = fresh_page();
buf[2..4].copy_from_slice(&1u16.to_le_bytes());
buf[4..6]
.copy_from_slice(&((DATA_PAGE_HEADER_SIZE + SLOT_ENTRY_SIZE) as u16).to_le_bytes());
let base = DATA_PAGE_HEADER_SIZE;
buf[base..base + 2].copy_from_slice(&4u16.to_le_bytes());
buf[base + 2..base + 4].copy_from_slice(&1u16.to_le_bytes());
buf[base + 4..base + 6].copy_from_slice(&SLOT_FLAG_LIVE.to_le_bytes());
assert!(DataPage::read(&buf, 0).is_none());
}
#[test]
fn read_returns_none_on_free_end_past_page_body() {
let mut buf = fresh_page();
buf[6..8].copy_from_slice(&(CHECKSUM_OFFSET as u16 + 100).to_le_bytes());
assert!(DataPage::read(&buf, 0).is_none());
assert_eq!(DataPage::free_space(&buf), 0);
}
#[test]
fn read_returns_none_on_free_start_less_than_header() {
let mut buf = fresh_page();
buf[4..6].copy_from_slice(&4u16.to_le_bytes());
assert!(DataPage::read(&buf, 0).is_none());
}
#[test]
fn read_returns_none_on_wrong_page_type() {
let mut buf = fresh_page();
buf[0] = 0xFF; assert!(DataPage::read(&buf, 0).is_none());
assert_eq!(DataPage::used_space(&buf), 0);
assert_eq!(DataPage::iter_live(&buf).len(), 0);
}
#[test]
fn insert_refuses_corrupt_header() {
let mut buf = fresh_page();
buf[4..6].copy_from_slice(&(CHECKSUM_OFFSET as u16 + 1).to_le_bytes());
assert!(DataPage::insert(&mut buf, b"x").is_none());
}
#[test]
fn iter_live_and_used_space_survive_oversized_slot_count() {
let mut buf = fresh_page();
DataPage::insert(&mut buf, b"abc").unwrap();
buf[2..4].copy_from_slice(&2000u16.to_le_bytes());
assert_eq!(DataPage::iter_live(&buf).len(), 0);
assert_eq!(DataPage::used_space(&buf), 0);
}
#[test]
fn test_data_page_insert_and_read() {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
let slot = DataPage::insert(&mut buf, b"hello world").unwrap();
let data = DataPage::read(&buf, slot).unwrap();
assert_eq!(data, b"hello world");
}
#[test]
fn test_data_page_multiple_inserts() {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
let s0 = DataPage::insert(&mut buf, b"aaa").unwrap();
let s1 = DataPage::insert(&mut buf, b"bbb").unwrap();
let s2 = DataPage::insert(&mut buf, b"ccc").unwrap();
assert_eq!(DataPage::read(&buf, s0).unwrap(), b"aaa");
assert_eq!(DataPage::read(&buf, s1).unwrap(), b"bbb");
assert_eq!(DataPage::read(&buf, s2).unwrap(), b"ccc");
assert_eq!(DataPage::slot_count(&buf), 3);
}
#[test]
fn test_data_page_full() {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
let big = vec![0xABu8; 2000];
let mut count = 0;
while DataPage::insert(&mut buf, &big).is_some() {
count += 1;
}
assert!(count >= 3);
assert!(count <= 4);
}
#[test]
fn test_data_page_max_value() {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
let max_val = vec![0xCD; 8162];
let slot = DataPage::insert(&mut buf, &max_val);
assert!(slot.is_some());
assert_eq!(DataPage::read(&buf, slot.unwrap()).unwrap().len(), 8162);
}
proptest::proptest! {
#[test]
fn prop_insert_read_roundtrip(value in proptest::collection::vec(0u8..=255, 0..=8162)) {
let mut buf = [0u8; PAGE_SIZE];
DataPage::init_page(&mut buf);
let slot = DataPage::insert(&mut buf, &value)
.expect("insert must succeed for value sized within the inline max");
let read_back = DataPage::read(&buf, slot)
.expect("read must return the value at its own slot");
assert_eq!(read_back, value.as_slice(),
"insert then read must preserve bytes exactly");
}
}
}