use crate::error::{ChiselError, Result};
use crate::page::{self, PageType, CHECKSUM_OFFSET};
use crate::page_cache::PageCache;
const OVERFLOW_HEADER_END: usize = 32;
const OVERFLOW_PAYLOAD: usize = CHECKSUM_OFFSET - OVERFLOW_HEADER_END;
pub struct Overflow;
impl Overflow {
pub fn write(cache: &mut PageCache, value: &[u8]) -> Result<u64> {
assert!(
!value.is_empty(),
"Overflow::write requires a non-empty value; empty values must use a data page slot"
);
let total_length = value.len() as u64;
let num_pages = value.len().div_ceil(OVERFLOW_PAYLOAD);
let mut page_ids = Vec::with_capacity(num_pages);
for _ in 0..num_pages {
page_ids.push(cache.new_page()?);
}
for (i, &page_id) in page_ids.iter().enumerate() {
let start = i * OVERFLOW_PAYLOAD;
let end = std::cmp::min(start + OVERFLOW_PAYLOAD, value.len());
let chunk = &value[start..end];
let next_page = if i + 1 < page_ids.len() {
page_ids[i + 1]
} else {
0
};
let buf = cache.get_mut(page_id)?;
buf.fill(0);
buf[0] = PageType::Overflow as u8;
buf[1] = page::current_version(page::PageType::Overflow);
buf[16..24].copy_from_slice(&total_length.to_le_bytes());
buf[24..32].copy_from_slice(&next_page.to_le_bytes());
buf[OVERFLOW_HEADER_END..OVERFLOW_HEADER_END + chunk.len()].copy_from_slice(chunk);
page::stamp_checksum(buf);
}
Ok(page_ids[0])
}
pub fn read(cache: &mut PageCache, first_page: u64) -> Result<Vec<u8>> {
let total_length = {
let buf = cache.get(first_page)?;
if buf[0] != PageType::Overflow as u8 {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
u64::from_le_bytes(buf[16..24].try_into().unwrap()) as usize
};
if total_length == 0 {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
let max_pages = total_length.div_ceil(OVERFLOW_PAYLOAD);
let mut result = Vec::with_capacity(total_length);
let mut current_page = first_page;
let mut pages_visited = 0usize;
loop {
if pages_visited >= max_pages {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
pages_visited += 1;
let buf = cache.get(current_page)?;
if buf[0] != PageType::Overflow as u8 {
return Err(ChiselError::CorruptPage {
page_id: current_page,
});
}
let next_page = u64::from_le_bytes(buf[24..32].try_into().unwrap());
let remaining = total_length.saturating_sub(result.len());
if remaining == 0 {
return Err(ChiselError::CorruptPage {
page_id: current_page,
});
}
let chunk_len = std::cmp::min(remaining, OVERFLOW_PAYLOAD);
result.extend_from_slice(&buf[OVERFLOW_HEADER_END..OVERFLOW_HEADER_END + chunk_len]);
if next_page == 0 {
break;
}
current_page = next_page;
}
if result.len() != total_length {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
Ok(result)
}
pub fn collect_chain_pages(cache: &mut PageCache, first_page: u64) -> Result<Vec<u64>> {
let total_length = {
let buf = cache.get(first_page)?;
if buf[0] != PageType::Overflow as u8 {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
u64::from_le_bytes(buf[16..24].try_into().unwrap()) as usize
};
if total_length == 0 {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
let max_pages = total_length.div_ceil(OVERFLOW_PAYLOAD);
let mut freed = Vec::new();
let mut current_page = first_page;
loop {
if freed.len() >= max_pages {
return Err(ChiselError::CorruptPage {
page_id: first_page,
});
}
let buf = cache.get(current_page)?;
if buf[0] != PageType::Overflow as u8 {
return Err(ChiselError::CorruptPage {
page_id: current_page,
});
}
let next_page = u64::from_le_bytes(buf[24..32].try_into().unwrap());
freed.push(current_page);
if next_page == 0 {
break;
}
current_page = next_page;
}
Ok(freed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page_io::PageIo;
use tempfile::NamedTempFile;
fn make_cache() -> PageCache {
let file = NamedTempFile::new().unwrap();
let io = PageIo::open(file.path(), false).unwrap();
let mut cache = PageCache::new(
io,
64 * crate::page::PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
cache.set_next_page_id(2);
cache
}
fn write_three_page_chain(cache: &mut PageCache) -> (u64, Vec<u8>) {
let value_len = OVERFLOW_PAYLOAD * 2 + 100;
let value: Vec<u8> = (0..value_len).map(|i| (i % 251) as u8).collect();
let first = Overflow::write(cache, &value).unwrap();
(first, value)
}
fn patch_next_page(cache: &mut PageCache, page_id: u64, new_next: u64) {
let buf = cache.get_mut(page_id).unwrap();
buf[24..32].copy_from_slice(&new_next.to_le_bytes());
page::stamp_checksum(buf);
}
fn read_next_page(cache: &mut PageCache, page_id: u64) -> u64 {
let buf = cache.get(page_id).unwrap();
u64::from_le_bytes(buf[24..32].try_into().unwrap())
}
#[test]
#[should_panic(expected = "non-empty value")]
fn write_empty_value_panics() {
let mut cache = make_cache();
let _ = Overflow::write(&mut cache, &[]);
}
#[test]
fn read_detects_cycle_in_chain() {
let mut cache = make_cache();
let (first, _) = write_three_page_chain(&mut cache);
let second = read_next_page(&mut cache, first);
let third = read_next_page(&mut cache, second);
patch_next_page(&mut cache, third, second);
match Overflow::read(&mut cache, first) {
Err(ChiselError::CorruptPage { page_id: _ }) => {}
other => panic!("expected CorruptPage, got {other:?}"),
}
}
#[test]
fn delete_detects_cycle_in_chain() {
let mut cache = make_cache();
let (first, _) = write_three_page_chain(&mut cache);
let second = read_next_page(&mut cache, first);
let third = read_next_page(&mut cache, second);
patch_next_page(&mut cache, third, first);
match Overflow::collect_chain_pages(&mut cache, first) {
Err(ChiselError::CorruptPage { page_id: _ }) => {}
other => panic!("expected CorruptPage, got {other:?}"),
}
}
#[test]
fn read_detects_short_chain() {
let mut cache = make_cache();
let (first, _) = write_three_page_chain(&mut cache);
patch_next_page(&mut cache, first, 0);
match Overflow::read(&mut cache, first) {
Err(ChiselError::CorruptPage { page_id: _ }) => {}
other => panic!("expected CorruptPage, got {other:?}"),
}
}
#[test]
fn read_roundtrip_well_formed_chain() {
let mut cache = make_cache();
let (first, expected) = write_three_page_chain(&mut cache);
let got = Overflow::read(&mut cache, first).unwrap();
assert_eq!(got, expected);
}
#[test]
fn write_and_read_value_smaller_than_threshold() {
let mut cache = make_cache();
let value = vec![0xAB; 4000];
let first_page = Overflow::write(&mut cache, &value).unwrap();
let read_back = Overflow::read(&mut cache, first_page).unwrap();
assert_eq!(read_back, value);
}
#[test]
fn write_and_read_multi_page_value() {
let mut cache = make_cache();
let value = vec![0xCD; 20000];
let first_page = Overflow::write(&mut cache, &value).unwrap();
let read_back = Overflow::read(&mut cache, first_page).unwrap();
assert_eq!(read_back, value);
}
#[test]
fn delete_returns_every_chain_page() {
let mut cache = make_cache();
let value = vec![0xEF; 20000];
let first_page = Overflow::write(&mut cache, &value).unwrap();
let freed = Overflow::collect_chain_pages(&mut cache, first_page).unwrap();
assert!(freed.len() >= 3);
}
#[test]
fn read_and_delete_reject_wrong_page_type_at_first_page() {
let mut cache = make_cache();
let id = cache.new_page().unwrap();
{
let buf = cache.get_mut(id).unwrap();
buf.fill(0);
buf[0] = PageType::Data as u8;
buf[16..24].copy_from_slice(&u64::MAX.to_le_bytes());
page::stamp_checksum(buf);
}
assert!(
matches!(Overflow::read(&mut cache, id), Err(ChiselError::CorruptPage { page_id }) if page_id == id),
"read must reject a wrong-type first page before trusting its total_length"
);
assert!(
matches!(Overflow::collect_chain_pages(&mut cache, id), Err(ChiselError::CorruptPage { page_id }) if page_id == id),
"collect_chain_pages must reject a wrong-type first page"
);
}
#[test]
fn read_rejects_wrong_page_type_mid_chain() {
let mut cache = make_cache();
let (first, _value) = write_three_page_chain(&mut cache);
let second = read_next_page(&mut cache, first);
assert_ne!(second, 0, "a three-page chain must have a second page");
{
let buf = cache.get_mut(second).unwrap();
buf[0] = PageType::Data as u8;
page::stamp_checksum(buf);
}
assert!(
matches!(Overflow::read(&mut cache, first), Err(ChiselError::CorruptPage { page_id }) if page_id == second),
"read must reject a wrong-type page mid-walk"
);
}
}