use fsqlite_error::{FrankenError, Result};
use fsqlite_types::PageNumber;
use fsqlite_types::limits::{
BTREE_INTERIOR_HEADER_SIZE, BTREE_LEAF_HEADER_SIZE, CELL_POINTER_SIZE, DB_HEADER_SIZE,
};
use fsqlite_types::serial_type::read_varint;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum BtreePageType {
InteriorIndex = 0x02,
InteriorTable = 0x05,
LeafIndex = 0x0A,
LeafTable = 0x0D,
}
impl BtreePageType {
pub const fn from_flag(flag: u8) -> Option<Self> {
match flag {
0x02 => Some(Self::InteriorIndex),
0x05 => Some(Self::InteriorTable),
0x0A => Some(Self::LeafIndex),
0x0D => Some(Self::LeafTable),
_ => None,
}
}
#[must_use]
pub const fn is_interior(self) -> bool {
matches!(self, Self::InteriorIndex | Self::InteriorTable)
}
#[must_use]
pub const fn is_leaf(self) -> bool {
!self.is_interior()
}
#[must_use]
pub const fn is_table(self) -> bool {
matches!(self, Self::InteriorTable | Self::LeafTable)
}
#[must_use]
pub const fn is_index(self) -> bool {
!self.is_table()
}
#[must_use]
pub const fn header_size(self) -> u8 {
if self.is_interior() {
BTREE_INTERIOR_HEADER_SIZE
} else {
BTREE_LEAF_HEADER_SIZE
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BtreePageHeader {
pub page_type: BtreePageType,
pub first_freeblock: u16,
pub cell_count: u16,
pub cell_content_offset: u32,
pub fragmented_free_bytes: u8,
pub right_child: Option<PageNumber>,
}
impl BtreePageHeader {
#[must_use]
pub fn content_offset(&self, usable_size: u32) -> usize {
if self.cell_count == 0 || self.cell_content_offset >= 65536 {
usable_size as usize
} else {
self.cell_content_offset as usize
}
}
#[inline]
pub fn parse(page: &[u8], header_offset: usize) -> Result<Self> {
const LEAF: usize = BTREE_LEAF_HEADER_SIZE as usize;
const INTERIOR_TAIL: usize = BTREE_INTERIOR_HEADER_SIZE as usize - LEAF;
let leaf_end = header_offset.saturating_add(LEAF);
let h: &[u8; LEAF] = page
.get(header_offset..leaf_end)
.and_then(|slice| slice.try_into().ok())
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!(
"page too small for B-tree header: {} bytes at offset {}",
page.len().saturating_sub(header_offset),
header_offset
),
})?;
let page_type =
BtreePageType::from_flag(h[0]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid B-tree page type flag: {:#04x}", h[0]),
})?;
let first_freeblock = u16::from_be_bytes([h[1], h[2]]);
let cell_count = u16::from_be_bytes([h[3], h[4]]);
let raw_content_offset = u16::from_be_bytes([h[5], h[6]]);
let cell_content_offset = if raw_content_offset == 0 {
65536
} else {
u32::from(raw_content_offset)
};
let fragmented_free_bytes = h[7];
let right_child = if page_type.is_interior() {
let interior_end = leaf_end + INTERIOR_TAIL;
let tail: &[u8; INTERIOR_TAIL] = page
.get(leaf_end..interior_end)
.and_then(|slice| slice.try_into().ok())
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "page too small for interior B-tree header".to_owned(),
})?;
let pgno = u32::from_be_bytes(*tail);
Some(
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "interior page has zero right-child pointer".to_owned(),
})?,
)
} else {
None
};
Ok(Self {
page_type,
first_freeblock,
cell_count,
cell_content_offset,
fragmented_free_bytes,
right_child,
})
}
pub fn write(&self, page: &mut [u8], header_offset: usize) {
let h = &mut page[header_offset..];
h[0] = self.page_type as u8;
h[1..3].copy_from_slice(&self.first_freeblock.to_be_bytes());
h[3..5].copy_from_slice(&self.cell_count.to_be_bytes());
let content_offset_u16 = if self.cell_content_offset >= 65536 {
0u16
} else {
#[allow(clippy::cast_possible_truncation)]
{
self.cell_content_offset as u16
}
};
h[5..7].copy_from_slice(&content_offset_u16.to_be_bytes());
h[7] = self.fragmented_free_bytes;
if let Some(right_child) = self.right_child {
h[8..12].copy_from_slice(&right_child.get().to_be_bytes());
}
}
}
#[inline]
pub fn parse_page_header(page: &[u8], page_no: PageNumber) -> Result<BtreePageHeader> {
let header_offset = header_offset_for_page(page_no);
BtreePageHeader::parse(page, header_offset).map_err(|error| FrankenError::DatabaseCorrupt {
detail: format!(
"failed to parse B-tree page {} at header offset {}: {}",
page_no.get(),
header_offset,
error
),
})
}
pub fn read_cell_pointers(
page: &[u8],
header: &BtreePageHeader,
header_offset: usize,
) -> Result<Vec<u16>> {
let mut buf = Vec::new();
read_cell_pointers_into(page, header, header_offset, &mut buf)?;
Ok(buf)
}
#[allow(clippy::incompatible_msrv)] pub fn read_cell_pointers_into(
page: &[u8],
header: &BtreePageHeader,
header_offset: usize,
buf: &mut Vec<u16>,
) -> Result<()> {
let ptr_array_start = header_offset + header.page_type.header_size() as usize;
let count = header.cell_count as usize;
let ptr_array_end = ptr_array_start + count * CELL_POINTER_SIZE as usize;
if ptr_array_end > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"cell pointer array extends past page: {} pointers at offset {}",
count, ptr_array_start
),
});
}
let cell_content_offset =
usize::try_from(header.cell_content_offset).map_err(|_| FrankenError::DatabaseCorrupt {
detail: format!(
"cell content offset {} exceeds platform address range",
header.cell_content_offset
),
})?;
if count > 0 && cell_content_offset >= page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"non-empty page cell content offset {cell_content_offset} starts outside page of {} bytes",
page.len()
),
});
}
if ptr_array_end > cell_content_offset {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"cell pointer array overlaps with cell content area: ptr_end={} > content={}",
ptr_array_end, cell_content_offset
),
});
}
buf.clear();
buf.reserve(count);
let ptr_bytes = &page[ptr_array_start..ptr_array_end];
let (chunks, _) = ptr_bytes.as_chunks::<2>();
buf.extend(chunks.iter().map(|c| u16::from_be_bytes(*c)));
Ok(())
}
#[allow(clippy::incompatible_msrv)] pub fn write_cell_pointers(
page: &mut [u8],
header_offset: usize,
header: &BtreePageHeader,
pointers: &[u16],
) {
let ptr_array_start = header_offset + header.page_type.header_size() as usize;
let ptr_array_end = ptr_array_start + pointers.len() * CELL_POINTER_SIZE as usize;
let dst = &mut page[ptr_array_start..ptr_array_end];
let (chunks, _) = dst.as_chunks_mut::<2>();
for (chunk, &ptr) in chunks.iter_mut().zip(pointers) {
*chunk = ptr.to_be_bytes();
}
}
#[must_use]
pub const fn max_local_payload(usable_size: u32, page_type: BtreePageType) -> u32 {
if page_type.is_table() && page_type.is_leaf() {
usable_size.saturating_sub(35)
} else {
(usable_size.saturating_sub(12) * 64 / 255).saturating_sub(23)
}
}
#[must_use]
pub const fn min_local_payload(usable_size: u32) -> u32 {
(usable_size.saturating_sub(12) * 32 / 255).saturating_sub(23)
}
#[must_use]
pub const fn local_payload_size(
payload_size: u32,
usable_size: u32,
page_type: BtreePageType,
) -> u32 {
let max_local = max_local_payload(usable_size, page_type);
if payload_size <= max_local {
return payload_size;
}
let min_local = min_local_payload(usable_size);
let surplus = usable_size.saturating_sub(4);
if surplus == 0 {
return min_local;
}
let local = min_local + (payload_size - min_local) % surplus;
if local > max_local { min_local } else { local }
}
#[must_use]
pub const fn has_overflow(payload_size: u32, usable_size: u32, page_type: BtreePageType) -> bool {
payload_size > max_local_payload(usable_size, page_type)
}
#[derive(Debug, Clone)]
pub struct CellRef {
pub left_child: Option<PageNumber>,
pub rowid: Option<i64>,
pub payload_size: u32,
pub local_size: u32,
pub payload_offset: usize,
pub overflow_page: Option<PageNumber>,
}
impl CellRef {
#[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
pub fn parse(
page: &[u8],
cell_offset: usize,
page_type: BtreePageType,
usable_size: u32,
) -> Result<Self> {
if cell_offset > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"cell offset {cell_offset} extends past page length {}",
page.len()
),
});
}
if page_type == BtreePageType::LeafTable {
return Self::parse_leaf_table(page, cell_offset, usable_size);
}
let mut pos = cell_offset;
let left_child = if page_type.is_interior() {
if pos + 4 > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past page (left child)".to_owned(),
});
}
let pgno = u32::from_be_bytes([page[pos], page[pos + 1], page[pos + 2], page[pos + 3]]);
pos += 4;
Some(
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell has zero left-child pointer".to_owned(),
})?,
)
} else {
None
};
if page_type == BtreePageType::InteriorTable {
let (rowid_raw, rowid_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in interior table cell (rowid)".to_owned(),
})?;
#[allow(clippy::cast_possible_wrap)]
let rowid = rowid_raw as i64;
return Ok(Self {
left_child,
rowid: Some(rowid),
payload_size: 0,
local_size: 0,
payload_offset: pos + rowid_len,
overflow_page: None,
});
}
let (payload_size_raw, ps_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in cell (payload size)".to_owned(),
})?;
let payload_size =
u32::try_from(payload_size_raw).map_err(|_| FrankenError::DatabaseCorrupt {
detail: "cell payload size exceeds 32-bit range".to_owned(),
})?;
pos += ps_len;
let rowid = if page_type.is_table() {
let (rowid_raw, rowid_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in table cell (rowid)".to_owned(),
})?;
pos += rowid_len;
#[allow(clippy::cast_possible_wrap)]
let r = rowid_raw as i64;
Some(r)
} else {
None
};
let payload_offset = pos;
let local_size = local_payload_size(payload_size, usable_size, page_type);
let local_end = payload_offset
.checked_add(local_size as usize)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell payload offset overflow".to_owned(),
})?;
if local_end > page.len() || local_end > usable_size as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (payload bytes)".to_owned(),
});
}
let overflow_page = if local_size < payload_size {
let overflow_ptr_offset = local_end;
if overflow_ptr_offset + 4 > page.len()
|| overflow_ptr_offset + 4 > usable_size as usize
{
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (overflow pointer)".to_owned(),
});
}
let pgno = u32::from_be_bytes([
page[overflow_ptr_offset],
page[overflow_ptr_offset + 1],
page[overflow_ptr_offset + 2],
page[overflow_ptr_offset + 3],
]);
Some(
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell has zero overflow page pointer".to_owned(),
})?,
)
} else {
None
};
Ok(Self {
left_child,
rowid,
payload_size,
local_size,
payload_offset,
overflow_page,
})
}
#[inline]
fn parse_leaf_table(page: &[u8], cell_offset: usize, usable_size: u32) -> Result<Self> {
let (payload_size_raw, ps_len) =
read_varint(&page[cell_offset..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in cell (payload size)".to_owned(),
})?;
let payload_size =
u32::try_from(payload_size_raw).map_err(|_| FrankenError::DatabaseCorrupt {
detail: "cell payload size exceeds 32-bit range".to_owned(),
})?;
let rowid_start =
cell_offset
.checked_add(ps_len)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell offset overflow after payload size".to_owned(),
})?;
let (rowid_raw, rowid_len) =
read_varint(&page[rowid_start..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in table cell (rowid)".to_owned(),
})?;
let payload_offset =
rowid_start
.checked_add(rowid_len)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell payload offset overflow".to_owned(),
})?;
let max_local = usable_size.saturating_sub(35);
let local_size = if payload_size <= max_local {
payload_size
} else {
local_payload_size(payload_size, usable_size, BtreePageType::LeafTable)
};
let local_end = payload_offset
.checked_add(local_size as usize)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell payload offset overflow".to_owned(),
})?;
if local_end > page.len() || local_end > usable_size as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (payload bytes)".to_owned(),
});
}
let overflow_page = if local_size < payload_size {
let overflow_ptr_offset = local_end;
if overflow_ptr_offset + 4 > page.len()
|| overflow_ptr_offset + 4 > usable_size as usize
{
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (overflow pointer)".to_owned(),
});
}
let pgno = u32::from_be_bytes([
page[overflow_ptr_offset],
page[overflow_ptr_offset + 1],
page[overflow_ptr_offset + 2],
page[overflow_ptr_offset + 3],
]);
Some(
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell has zero overflow page pointer".to_owned(),
})?,
)
} else {
None
};
#[allow(clippy::cast_possible_wrap)]
let rowid = rowid_raw as i64;
Ok(Self {
left_child: None,
rowid: Some(rowid),
payload_size,
local_size,
payload_offset,
overflow_page,
})
}
#[inline]
pub fn read_interior_left_child(page: &[u8], cell_offset: usize) -> Result<PageNumber> {
if cell_offset.saturating_add(4) > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: "interior cell extends past page (left child)".to_owned(),
});
}
let pgno = u32::from_be_bytes([
page[cell_offset],
page[cell_offset + 1],
page[cell_offset + 2],
page[cell_offset + 3],
]);
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "interior cell has zero left-child pointer".to_owned(),
})
}
#[inline]
pub fn parse_leaf_table_rowid(page: &[u8], cell_offset: usize) -> Result<i64> {
let cell = page
.get(cell_offset..)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!(
"cell offset {cell_offset} extends past page length {}",
page.len()
),
})?;
let (_, ps_len) = read_varint(cell).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in table cell (payload size)".to_owned(),
})?;
let rowid_start =
cell_offset
.checked_add(ps_len)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell offset overflow after payload size".to_owned(),
})?;
let rowid_slice = page
.get(rowid_start..)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell offset overflow after payload size".to_owned(),
})?;
let (rowid_raw, _) =
read_varint(rowid_slice).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in table cell (rowid)".to_owned(),
})?;
#[allow(clippy::cast_possible_wrap)]
Ok(rowid_raw as i64)
}
pub fn local_payload<'a>(&self, page: &'a [u8]) -> &'a [u8] {
&page[self.payload_offset..self.payload_offset + self.local_size as usize]
}
#[must_use]
pub fn payload_on_page_size(&self) -> usize {
let mut size = self.local_size as usize;
if self.overflow_page.is_some() {
size += 4;
}
size
}
}
#[must_use]
#[inline]
pub const fn header_offset_for_page(page_no: PageNumber) -> usize {
if page_no.get() == 1 {
DB_HEADER_SIZE as usize
} else {
0
}
}
#[must_use]
pub fn read_table_leaf_rowid_at_offset(page: &[u8], cell_offset: usize) -> Option<i64> {
if cell_offset >= page.len() {
return None;
}
let cell = &page[cell_offset..];
let (_, payload_varint_len) = read_varint(cell)?;
if payload_varint_len >= cell.len() {
return None;
}
let (rowid_raw, _) = read_varint(&cell[payload_varint_len..])?;
#[allow(clippy::cast_possible_wrap)]
Some(rowid_raw as i64)
}
pub fn cell_on_page_size_fast(
page: &[u8],
cell_offset: usize,
page_type: BtreePageType,
usable_size: u32,
) -> Result<usize> {
if cell_offset >= page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell offset past end of page".to_owned(),
});
}
let mut pos = cell_offset;
if page_type.is_interior() {
if pos + 4 > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past page (left child)".to_owned(),
});
}
pos += 4;
}
if page_type == BtreePageType::InteriorTable {
let (_rowid, rowid_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in interior table cell (rowid)".to_owned(),
})?;
return Ok(pos + rowid_len - cell_offset);
}
let (payload_size_raw, ps_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in cell (payload size)".to_owned(),
})?;
let payload_size =
u32::try_from(payload_size_raw).map_err(|_| FrankenError::DatabaseCorrupt {
detail: "cell payload size exceeds 32-bit range".to_owned(),
})?;
pos += ps_len;
if page_type.is_table() {
let (_rowid, rowid_len) =
read_varint(&page[pos..]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "truncated varint in table cell (rowid)".to_owned(),
})?;
pos += rowid_len;
}
let local_size = local_payload_size(payload_size, usable_size, page_type) as usize;
let local_end = pos
.checked_add(local_size)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "cell payload offset overflow".to_owned(),
})?;
if local_end > page.len() || local_end > usable_size as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (payload bytes)".to_owned(),
});
}
let total = if (local_size as u32) < payload_size {
if local_end + 4 > page.len() || local_end + 4 > usable_size as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell extends past usable page size (overflow pointer)".to_owned(),
});
}
local_end + 4 - cell_offset
} else {
local_end - cell_offset
};
Ok(total)
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
#[test]
fn test_page_type_from_flag() {
assert_eq!(
BtreePageType::from_flag(0x02),
Some(BtreePageType::InteriorIndex)
);
assert_eq!(
BtreePageType::from_flag(0x05),
Some(BtreePageType::InteriorTable)
);
assert_eq!(
BtreePageType::from_flag(0x0A),
Some(BtreePageType::LeafIndex)
);
assert_eq!(
BtreePageType::from_flag(0x0D),
Some(BtreePageType::LeafTable)
);
assert_eq!(BtreePageType::from_flag(0x00), None);
assert_eq!(BtreePageType::from_flag(0xFF), None);
}
#[test]
fn test_page_type_predicates() {
assert!(BtreePageType::InteriorTable.is_interior());
assert!(BtreePageType::InteriorIndex.is_interior());
assert!(!BtreePageType::LeafTable.is_interior());
assert!(!BtreePageType::LeafIndex.is_interior());
assert!(BtreePageType::LeafTable.is_leaf());
assert!(BtreePageType::LeafIndex.is_leaf());
assert!(BtreePageType::InteriorTable.is_table());
assert!(BtreePageType::LeafTable.is_table());
assert!(!BtreePageType::InteriorIndex.is_table());
assert!(BtreePageType::InteriorIndex.is_index());
assert!(BtreePageType::LeafIndex.is_index());
}
#[test]
fn test_page_type_header_size() {
assert_eq!(BtreePageType::LeafTable.header_size(), 8);
assert_eq!(BtreePageType::LeafIndex.header_size(), 8);
assert_eq!(BtreePageType::InteriorTable.header_size(), 12);
assert_eq!(BtreePageType::InteriorIndex.header_size(), 12);
}
#[test]
fn test_page_header_leaf_table_roundtrip() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 5,
cell_content_offset: 3800,
fragmented_free_bytes: 2,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let parsed = BtreePageHeader::parse(&page, 0).unwrap();
assert_eq!(parsed, header);
}
#[test]
fn test_page_header_interior_table_roundtrip() {
let right_child = PageNumber::new(42).unwrap();
let header = BtreePageHeader {
page_type: BtreePageType::InteriorTable,
first_freeblock: 100,
cell_count: 10,
cell_content_offset: 2048,
fragmented_free_bytes: 0,
right_child: Some(right_child),
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let parsed = BtreePageHeader::parse(&page, 0).unwrap();
assert_eq!(parsed, header);
assert_eq!(parsed.right_child.unwrap().get(), 42);
}
#[test]
fn test_page_header_page_one_offset() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 3,
cell_content_offset: 3900,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 100);
let parsed = BtreePageHeader::parse(&page, 100).unwrap();
assert_eq!(parsed, header);
}
#[test]
fn test_page_header_content_offset_zero_means_65536() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 0,
cell_content_offset: 65536,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 65536];
header.write(&mut page, 0);
assert_eq!(page[5], 0);
assert_eq!(page[6], 0);
let parsed = BtreePageHeader::parse(&page, 0).unwrap();
assert_eq!(parsed.cell_content_offset, 65536);
}
#[test]
fn test_content_offset_resolves_sentinel_and_empty_page() {
let usable = 4096u32;
let empty = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 0,
cell_content_offset: 1234,
fragmented_free_bytes: 0,
right_child: None,
};
assert_eq!(empty.content_offset(usable), usable as usize);
let sentinel = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 5,
cell_content_offset: 65536,
fragmented_free_bytes: 0,
right_child: None,
};
assert_eq!(sentinel.content_offset(usable), usable as usize);
let normal = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 5,
cell_content_offset: 3800,
fragmented_free_bytes: 0,
right_child: None,
};
assert_eq!(normal.content_offset(usable), 3800);
}
#[test]
fn test_page_header_invalid_type() {
let mut page = vec![0u8; 4096];
page[0] = 0xFF; let err = BtreePageHeader::parse(&page, 0).unwrap_err();
assert!(err.to_string().contains("invalid B-tree page type"));
}
#[test]
fn test_page_header_truncated() {
let page = vec![0u8; 4]; let err = BtreePageHeader::parse(&page, 0).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_read_write_cell_pointers() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 3,
cell_content_offset: 3800,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let ptrs = [3900u16, 3950, 4000];
write_cell_pointers(&mut page, 0, &header, &ptrs);
let read_ptrs = read_cell_pointers(&page, &header, 0).unwrap();
assert_eq!(read_ptrs, vec![3900, 3950, 4000]);
}
#[test]
fn test_read_cell_pointers_rejects_non_empty_page_with_65536_content_offset() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 1,
cell_content_offset: 65536,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let err = read_cell_pointers(&page, &header, 0).unwrap_err();
assert!(err.to_string().contains("starts outside page"));
}
#[test]
fn test_read_cell_pointers_rejects_non_empty_page_with_end_content_offset() {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 1,
cell_content_offset: 4096,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let err = read_cell_pointers(&page, &header, 0).unwrap_err();
assert!(err.to_string().contains("starts outside page"));
}
#[test]
fn test_max_local_payload_leaf_table() {
assert_eq!(max_local_payload(4096, BtreePageType::LeafTable), 4061);
}
#[test]
fn test_max_local_payload_other_types() {
let expected = (4096 - 12) * 64 / 255 - 23;
assert_eq!(
max_local_payload(4096, BtreePageType::InteriorIndex),
expected
);
assert_eq!(max_local_payload(4096, BtreePageType::LeafIndex), expected);
assert_eq!(
max_local_payload(4096, BtreePageType::InteriorTable),
expected
);
}
#[test]
fn test_min_local_payload() {
let expected = (4096 - 12) * 32 / 255 - 23;
assert_eq!(min_local_payload(4096), expected);
}
#[test]
fn test_local_payload_fits_entirely() {
assert_eq!(local_payload_size(100, 4096, BtreePageType::LeafTable), 100);
}
#[test]
fn test_local_payload_overflow() {
let usable = 4096u32;
let payload = 5000u32;
let local = local_payload_size(payload, usable, BtreePageType::LeafTable);
let max_local = max_local_payload(usable, BtreePageType::LeafTable);
let min_local = min_local_payload(usable);
assert!(local >= min_local);
assert!(local <= max_local);
assert!(local < payload);
}
#[test]
fn test_has_overflow() {
assert!(!has_overflow(100, 4096, BtreePageType::LeafTable));
assert!(has_overflow(5000, 4096, BtreePageType::LeafTable));
assert!(!has_overflow(1000, 4096, BtreePageType::LeafIndex));
assert!(has_overflow(1500, 4096, BtreePageType::LeafIndex));
}
#[test]
fn test_payload_overflow_threshold_and_clamp_across_page_sizes() {
assert_eq!(max_local_payload(512, BtreePageType::LeafTable), 477);
assert_eq!(max_local_payload(1024, BtreePageType::LeafTable), 989);
assert_eq!(max_local_payload(65536, BtreePageType::LeafTable), 65_501);
assert_eq!(max_local_payload(512, BtreePageType::LeafIndex), 102);
assert_eq!(min_local_payload(512), 39);
for &usable in &[512u32, 1024, 4096, 65536] {
for &(page_type, label) in &[
(BtreePageType::LeafTable, "table-leaf"),
(BtreePageType::LeafIndex, "leaf-index"),
] {
let max_local = max_local_payload(usable, page_type);
let min_local = min_local_payload(usable);
assert!(min_local < max_local, "U={usable} {label}: expect M < X");
assert!(
!has_overflow(max_local, usable, page_type),
"U={usable} {label}"
);
assert!(
has_overflow(max_local + 1, usable, page_type),
"U={usable} {label}"
);
assert_eq!(
local_payload_size(max_local, usable, page_type),
max_local,
"U={usable} {label}: payload == X must be entirely local"
);
for &payload in &[max_local + 1, max_local + 100, usable, usable * 4] {
let local = local_payload_size(payload, usable, page_type);
assert!(
(min_local..=max_local).contains(&local),
"U={usable} {label} P={payload}: local {local} not in [{min_local},{max_local}]"
);
assert!(
local < payload,
"U={usable} {label} P={payload}: overflow must spill"
);
}
}
}
}
#[test]
fn test_parse_leaf_table_cell_no_overflow() {
let mut page = vec![0u8; 4096];
let cell_offset = 3900;
let mut pos = cell_offset;
page[pos] = 10;
pos += 1;
page[pos] = 42;
pos += 1;
for i in 0..10 {
page[pos + i] = (i + 1) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::LeafTable, 4096).unwrap();
assert_eq!(cell.rowid, Some(42));
assert_eq!(cell.payload_size, 10);
assert_eq!(cell.local_size, 10);
assert!(cell.overflow_page.is_none());
assert!(cell.left_child.is_none());
let payload = cell.local_payload(&page);
assert_eq!(payload, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}
#[test]
fn test_parse_leaf_table_cell_rejects_truncated_local_payload() {
let mut page = vec![0u8; 64];
let cell_offset = 60;
page[cell_offset] = 10; page[cell_offset + 1] = 1;
let err = CellRef::parse(&page, cell_offset, BtreePageType::LeafTable, 64).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
assert!(err.to_string().contains("payload bytes"));
}
#[test]
fn test_parse_interior_table_cell() {
let mut page = vec![0u8; 4096];
let cell_offset = 2000;
let mut pos = cell_offset;
page[pos..pos + 4].copy_from_slice(&7u32.to_be_bytes());
pos += 4;
page[pos] = 100;
let cell = CellRef::parse(&page, cell_offset, BtreePageType::InteriorTable, 4096).unwrap();
assert_eq!(cell.left_child.unwrap().get(), 7);
assert_eq!(cell.rowid, Some(100));
assert_eq!(cell.payload_size, 0);
}
#[test]
fn test_read_interior_left_child_parses_be_and_rejects_corruption() {
let mut page = vec![0u8; 16];
page[0..4].copy_from_slice(&42u32.to_be_bytes());
assert_eq!(
CellRef::read_interior_left_child(&page, 0).unwrap(),
PageNumber::new(42).unwrap()
);
page[8..12].copy_from_slice(&256u32.to_be_bytes()); assert_eq!(
CellRef::read_interior_left_child(&page, 8).unwrap(),
PageNumber::new(256).unwrap()
);
assert!(matches!(
CellRef::read_interior_left_child(&[0u8; 4], 0),
Err(FrankenError::DatabaseCorrupt { .. })
));
assert!(matches!(
CellRef::read_interior_left_child(&[0u8, 0, 0], 0),
Err(FrankenError::DatabaseCorrupt { .. })
));
assert!(
matches!(
CellRef::read_interior_left_child(&page, 13), Err(FrankenError::DatabaseCorrupt { .. })
),
"offset too close to the end must be rejected"
);
}
#[test]
fn test_parse_leaf_index_cell_no_overflow() {
let mut page = vec![0u8; 4096];
let cell_offset = 3500;
page[cell_offset] = 5;
for i in 0..5 {
page[cell_offset + 1 + i] = (i + 10) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::LeafIndex, 4096).unwrap();
assert!(cell.left_child.is_none());
assert!(cell.rowid.is_none());
assert_eq!(cell.payload_size, 5);
assert_eq!(cell.local_size, 5);
assert!(cell.overflow_page.is_none());
let payload = cell.local_payload(&page);
assert_eq!(payload, &[10, 11, 12, 13, 14]);
}
#[test]
fn test_parse_interior_index_cell() {
let mut page = vec![0u8; 4096];
let cell_offset = 2500;
let mut pos = cell_offset;
page[pos..pos + 4].copy_from_slice(&15u32.to_be_bytes());
pos += 4;
page[pos] = 8;
pos += 1;
for i in 0..8 {
page[pos + i] = (i + 20) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::InteriorIndex, 4096).unwrap();
assert_eq!(cell.left_child.unwrap().get(), 15);
assert!(cell.rowid.is_none());
assert_eq!(cell.payload_size, 8);
assert_eq!(cell.local_size, 8);
assert!(cell.overflow_page.is_none());
}
#[test]
fn test_parse_leaf_table_cell_with_overflow() {
let mut page = vec![0u8; 4096];
let cell_offset = 0;
let payload_size: u32 = 5000;
let usable_size: u32 = 4096;
let mut varint_buf = [0u8; 9];
let ps_len =
fsqlite_types::serial_type::write_varint(&mut varint_buf, u64::from(payload_size));
page[cell_offset..cell_offset + ps_len].copy_from_slice(&varint_buf[..ps_len]);
let rowid_offset = cell_offset + ps_len;
page[rowid_offset] = 1;
let rowid_len = 1;
let payload_offset = rowid_offset + rowid_len;
let local = local_payload_size(payload_size, usable_size, BtreePageType::LeafTable);
for i in 0..local as usize {
if payload_offset + i < page.len() {
page[payload_offset + i] = (i & 0xFF) as u8;
}
}
let overflow_ptr_offset = payload_offset + local as usize;
if overflow_ptr_offset + 4 <= page.len() {
page[overflow_ptr_offset..overflow_ptr_offset + 4]
.copy_from_slice(&99u32.to_be_bytes());
}
let cell =
CellRef::parse(&page, cell_offset, BtreePageType::LeafTable, usable_size).unwrap();
assert_eq!(cell.rowid, Some(1));
assert_eq!(cell.payload_size, payload_size);
assert_eq!(cell.local_size, local);
assert!(cell.local_size < cell.payload_size);
assert_eq!(cell.overflow_page.unwrap().get(), 99);
}
#[test]
fn test_header_offset_for_page() {
assert_eq!(header_offset_for_page(PageNumber::ONE), 100);
assert_eq!(header_offset_for_page(PageNumber::new(2).unwrap()), 0);
assert_eq!(header_offset_for_page(PageNumber::new(100).unwrap()), 0);
}
#[test]
fn test_read_table_leaf_rowid_at_offset_single_byte() {
let mut page = vec![0u8; 4096];
let cell_offset = 3900;
page[cell_offset] = 10; page[cell_offset + 1] = 42; let rowid = read_table_leaf_rowid_at_offset(&page, cell_offset).unwrap();
assert_eq!(rowid, 42);
}
#[test]
fn test_read_table_leaf_rowid_at_offset_multi_byte_varints() {
let mut page = vec![0u8; 4096];
let cell_offset = 100;
let mut pos = cell_offset;
let mut buf = [0u8; 9];
let ps_len = fsqlite_types::serial_type::write_varint(&mut buf, 1000);
page[pos..pos + ps_len].copy_from_slice(&buf[..ps_len]);
pos += ps_len;
let rid_len = fsqlite_types::serial_type::write_varint(&mut buf, 0xDEAD_BEEF);
page[pos..pos + rid_len].copy_from_slice(&buf[..rid_len]);
let rowid = read_table_leaf_rowid_at_offset(&page, cell_offset).unwrap();
#[allow(clippy::cast_possible_wrap)]
let expected = 0xDEAD_BEEFu64 as i64;
assert_eq!(rowid, expected);
}
#[test]
fn test_read_table_leaf_rowid_at_offset_out_of_range() {
let page = vec![0u8; 16];
assert!(read_table_leaf_rowid_at_offset(&page, 17).is_none());
}
#[test]
fn test_parse_leaf_table_rowid_reports_out_of_range_offset() {
let page = vec![0u8; 16];
let err = CellRef::parse_leaf_table_rowid(&page, 17).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
}
#[test]
fn test_parse_leaf_table_rowid_decodes_rowid_after_payload_size_varint() {
use fsqlite_types::serial_type::write_varint;
let mut page = vec![0u8; 64];
page[10] = 5; page[11] = 42; assert_eq!(CellRef::parse_leaf_table_rowid(&page, 10).unwrap(), 42);
let mut page = vec![0u8; 64];
let off = 4;
let n = write_varint(&mut page[off..], 200u64); let _ = write_varint(&mut page[off + n..], 7u64); assert_eq!(CellRef::parse_leaf_table_rowid(&page, off).unwrap(), 7);
let mut page = vec![0u8; 64];
let n = write_varint(&mut page[0..], 5u64); let _ = write_varint(&mut page[n..], 300u64); assert_eq!(CellRef::parse_leaf_table_rowid(&page, 0).unwrap(), 300);
let mut page = vec![0u8; 64];
let n = write_varint(&mut page[0..], 1u64);
let _ = write_varint(&mut page[n..], 1_000_000_000u64);
assert_eq!(
CellRef::parse_leaf_table_rowid(&page, 0).unwrap(),
1_000_000_000
);
}
#[test]
fn test_cellref_parse_reports_out_of_range_offset() {
let page = vec![0u8; 16];
let err = CellRef::parse(&page, 17, BtreePageType::LeafTable, 4096).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
}
#[test]
fn test_cell_on_page_size_fast_matches_cellref_leaf_table_no_overflow() {
let mut page = vec![0u8; 4096];
let cell_offset = 3900;
let mut pos = cell_offset;
page[pos] = 10; pos += 1;
page[pos] = 42; pos += 1;
for i in 0..10 {
page[pos + i] = (i + 1) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::LeafTable, 4096).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::LeafTable, 4096).unwrap();
assert_eq!(fast, expected);
assert_eq!(fast, 1 + 1 + 10);
}
#[test]
fn test_cell_on_page_size_fast_interior_table_cell() {
use fsqlite_types::serial_type::write_varint;
let usable: u32 = 4096;
let mut page = vec![0u8; 4096];
let cell_offset = 100;
page[cell_offset..cell_offset + 4].copy_from_slice(&7u32.to_be_bytes());
let rowid_len = write_varint(&mut page[cell_offset + 4..], 300u64);
let cell =
CellRef::parse(&page, cell_offset, BtreePageType::InteriorTable, usable).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast = cell_on_page_size_fast(&page, cell_offset, BtreePageType::InteriorTable, usable)
.unwrap();
assert_eq!(fast, expected);
assert_eq!(
fast,
4 + rowid_len,
"interior-table cell size = 4-byte child pointer + rowid varint"
);
}
#[test]
fn test_cell_on_page_size_fast_index_cells_have_no_rowid() {
use fsqlite_types::serial_type::write_varint;
let usable: u32 = 4096;
let mut page = vec![0u8; 4096];
let off = 50;
let ps_len = write_varint(&mut page[off..], 10u64); let cell = CellRef::parse(&page, off, BtreePageType::LeafIndex, usable).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, off);
let fast = cell_on_page_size_fast(&page, off, BtreePageType::LeafIndex, usable).unwrap();
assert_eq!(fast, expected);
assert_eq!(
fast,
ps_len + 10,
"leaf-index size = payload_size varint + payload"
);
let mut page = vec![0u8; 4096];
let off = 50;
page[off..off + 4].copy_from_slice(&9u32.to_be_bytes());
let ps_len = write_varint(&mut page[off + 4..], 10u64);
let cell = CellRef::parse(&page, off, BtreePageType::InteriorIndex, usable).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, off);
let fast =
cell_on_page_size_fast(&page, off, BtreePageType::InteriorIndex, usable).unwrap();
assert_eq!(fast, expected);
assert_eq!(
fast,
4 + ps_len + 10,
"interior-index size = child pointer + payload_size varint + payload"
);
}
#[test]
fn test_cell_on_page_size_fast_matches_cellref_leaf_table_with_overflow() {
let mut page = vec![0u8; 4096];
let cell_offset = 0;
let payload_size: u32 = 5000;
let usable_size: u32 = 4096;
let mut buf = [0u8; 9];
let ps_len = fsqlite_types::serial_type::write_varint(&mut buf, u64::from(payload_size));
page[cell_offset..cell_offset + ps_len].copy_from_slice(&buf[..ps_len]);
let rowid_offset = cell_offset + ps_len;
page[rowid_offset] = 1;
let rowid_len = 1;
let payload_offset = rowid_offset + rowid_len;
let local = local_payload_size(payload_size, usable_size, BtreePageType::LeafTable);
for i in 0..local as usize {
if payload_offset + i < page.len() {
page[payload_offset + i] = (i & 0xFF) as u8;
}
}
let overflow_ptr_offset = payload_offset + local as usize;
if overflow_ptr_offset + 4 <= page.len() {
page[overflow_ptr_offset..overflow_ptr_offset + 4]
.copy_from_slice(&99u32.to_be_bytes());
}
let cell =
CellRef::parse(&page, cell_offset, BtreePageType::LeafTable, usable_size).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::LeafTable, usable_size)
.unwrap();
assert_eq!(fast, expected);
}
#[test]
fn test_cell_on_page_size_fast_matches_cellref_interior_table() {
let mut page = vec![0u8; 4096];
let cell_offset = 2000;
page[cell_offset..cell_offset + 4].copy_from_slice(&7u32.to_be_bytes());
page[cell_offset + 4] = 100;
let cell = CellRef::parse(&page, cell_offset, BtreePageType::InteriorTable, 4096).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::InteriorTable, 4096).unwrap();
assert_eq!(fast, expected);
assert_eq!(fast, 4 + 1);
}
#[test]
fn test_cell_on_page_size_fast_matches_cellref_interior_index() {
let mut page = vec![0u8; 4096];
let cell_offset = 2500;
page[cell_offset..cell_offset + 4].copy_from_slice(&15u32.to_be_bytes());
page[cell_offset + 4] = 8; for i in 0..8 {
page[cell_offset + 5 + i] = (i + 20) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::InteriorIndex, 4096).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::InteriorIndex, 4096).unwrap();
assert_eq!(fast, expected);
assert_eq!(fast, 4 + 1 + 8);
}
#[test]
fn test_cell_on_page_size_fast_matches_cellref_leaf_index() {
let mut page = vec![0u8; 4096];
let cell_offset = 3500;
page[cell_offset] = 5;
for i in 0..5 {
page[cell_offset + 1 + i] = (i + 10) as u8;
}
let cell = CellRef::parse(&page, cell_offset, BtreePageType::LeafIndex, 4096).unwrap();
let expected = crate::payload::cell_on_page_size(&cell, cell_offset);
let fast =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::LeafIndex, 4096).unwrap();
assert_eq!(fast, expected);
assert_eq!(fast, 1 + 5);
}
#[test]
fn test_cell_on_page_size_fast_rejects_out_of_bounds() {
let page = vec![0u8; 16];
let err = cell_on_page_size_fast(&page, 17, BtreePageType::LeafTable, 4096).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
}
#[test]
fn test_cell_on_page_size_fast_rejects_truncated_interior_child() {
let page = vec![0u8; 3]; let err = cell_on_page_size_fast(&page, 0, BtreePageType::InteriorTable, 4096).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
}
#[test]
fn test_cell_on_page_size_fast_rejects_payload_past_page() {
let mut page = vec![0u8; 64];
let cell_offset = 60;
page[cell_offset] = 10;
page[cell_offset + 1] = 1;
let err =
cell_on_page_size_fast(&page, cell_offset, BtreePageType::LeafTable, 64).unwrap_err();
assert!(matches!(err, FrankenError::DatabaseCorrupt { .. }));
}
#[test]
fn test_local_payload_various_page_sizes() {
for &page_size in &[512u32, 1024, 2048, 4096, 8192, 16384, 32768, 65536] {
let max_tbl = max_local_payload(page_size, BtreePageType::LeafTable);
let max_idx = max_local_payload(page_size, BtreePageType::LeafIndex);
let min = min_local_payload(page_size);
assert!(
max_tbl > min,
"page_size={page_size}: max_tbl={max_tbl} <= min={min}"
);
assert!(
max_idx > min,
"page_size={page_size}: max_idx={max_idx} <= min={min}"
);
assert!(
max_tbl > max_idx,
"page_size={page_size}: max_tbl={max_tbl} <= max_idx={max_idx}"
);
}
}
#[inline(never)]
fn parse_header_naive(page: &[u8], header_offset: usize) -> Result<BtreePageHeader> {
let remaining = page.len().saturating_sub(header_offset);
if remaining < BTREE_LEAF_HEADER_SIZE as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"page too small for B-tree header: {remaining} bytes at offset {header_offset}",
),
});
}
let h = &page[header_offset..];
let page_type =
BtreePageType::from_flag(h[0]).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid B-tree page type flag: {:#04x}", h[0]),
})?;
let first_freeblock = u16::from_be_bytes([h[1], h[2]]);
let cell_count = u16::from_be_bytes([h[3], h[4]]);
let raw_content_offset = u16::from_be_bytes([h[5], h[6]]);
let cell_content_offset = if raw_content_offset == 0 {
65536
} else {
u32::from(raw_content_offset)
};
let fragmented_free_bytes = h[7];
let right_child = if page_type.is_interior() {
if remaining < BTREE_INTERIOR_HEADER_SIZE as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "page too small for interior B-tree header".to_owned(),
});
}
let pgno = u32::from_be_bytes([h[8], h[9], h[10], h[11]]);
Some(
PageNumber::new(pgno).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: "interior page has zero right-child pointer".to_owned(),
})?,
)
} else {
None
};
Ok(BtreePageHeader {
page_type,
first_freeblock,
cell_count,
cell_content_offset,
fragmented_free_bytes,
right_child,
})
}
fn build_leaf_page() -> Vec<u8> {
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
cell_count: 17,
cell_content_offset: 3800,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
page
}
fn build_interior_page() -> Vec<u8> {
let header = BtreePageHeader {
page_type: BtreePageType::InteriorTable,
first_freeblock: 0,
cell_count: 9,
cell_content_offset: 2048,
fragmented_free_bytes: 0,
right_child: PageNumber::new(7),
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
page
}
#[test]
#[ignore = "microbench; run with --release --ignored --nocapture bench_parse_page_header"]
fn bench_parse_page_header() {
use std::hint::black_box;
use std::time::Instant;
let leaf = build_leaf_page();
let interior = build_interior_page();
let pages: [&[u8]; 4] = [&leaf, &interior, &leaf, &interior];
let iters: u64 = 4_000_000;
let trials = 5;
for trial in 0..trials {
let t = Instant::now();
for i in 0..iters {
let page = pages[(i as usize) & 3];
let header = BtreePageHeader::parse(black_box(page), black_box(0)).unwrap();
black_box(header);
}
let opt = t.elapsed();
let t = Instant::now();
for i in 0..iters {
let page = pages[(i as usize) & 3];
let header = parse_header_naive(black_box(page), black_box(0)).unwrap();
black_box(header);
}
let naive = t.elapsed();
#[allow(clippy::cast_precision_loss)]
let opt_ns = opt.as_nanos() as f64 / iters as f64;
#[allow(clippy::cast_precision_loss)]
let naive_ns = naive.as_nanos() as f64 / iters as f64;
let delta_pct = (opt_ns - naive_ns) / naive_ns * 100.0;
println!(
"[trial {trial}] parse: optimized={opt_ns:.2} ns naive={naive_ns:.2} ns \
delta={delta_pct:+.1}% (mixed leaf/interior, n={iters})",
);
}
}
#[inline(never)]
#[allow(clippy::incompatible_msrv)]
fn read_cell_pointers_into_naive(
page: &[u8],
header: &BtreePageHeader,
header_offset: usize,
buf: &mut Vec<u16>,
) -> Result<()> {
let ptr_array_start = header_offset + header.page_type.header_size() as usize;
let count = header.cell_count as usize;
let ptr_array_end = ptr_array_start + count * CELL_POINTER_SIZE as usize;
if ptr_array_end > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell pointer array extends past page".to_owned(),
});
}
if ptr_array_end > header.cell_content_offset as usize {
return Err(FrankenError::DatabaseCorrupt {
detail: "cell pointer array overlaps with cell content area".to_owned(),
});
}
buf.clear();
buf.reserve(count);
let ptr_bytes = &page[ptr_array_start..ptr_array_end];
buf.extend(
ptr_bytes
.chunks_exact(CELL_POINTER_SIZE as usize)
.map(|c| u16::from_be_bytes([c[0], c[1]])),
);
Ok(())
}
#[inline(never)]
fn write_cell_pointers_naive(
page: &mut [u8],
header_offset: usize,
header: &BtreePageHeader,
pointers: &[u16],
) {
let ptr_array_start = header_offset + header.page_type.header_size() as usize;
for (i, &ptr) in pointers.iter().enumerate() {
let off = ptr_array_start + i * CELL_POINTER_SIZE as usize;
page[off..off + 2].copy_from_slice(&ptr.to_be_bytes());
}
}
#[test]
#[ignore = "microbench; run with --release --ignored --nocapture bench_cell_pointers"]
fn bench_cell_pointers() {
use std::hint::black_box;
use std::time::Instant;
let cell_count: usize = 64;
let header = BtreePageHeader {
page_type: BtreePageType::LeafTable,
first_freeblock: 0,
#[allow(clippy::cast_possible_truncation)]
cell_count: cell_count as u16,
cell_content_offset: 3000,
fragmented_free_bytes: 0,
right_child: None,
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let pointers: Vec<u16> = (0..cell_count).map(|i| 3000u16 + (i as u16) * 16).collect();
write_cell_pointers(&mut page, 0, &header, &pointers);
let iters: u64 = 200_000;
let trials = 5;
let mut buf: Vec<u16> = Vec::with_capacity(cell_count);
for trial in 0..trials {
let t = Instant::now();
for _ in 0..iters {
read_cell_pointers_into(black_box(&page), black_box(&header), 0, &mut buf).unwrap();
black_box(&buf);
}
let read_opt = t.elapsed();
let t = Instant::now();
for _ in 0..iters {
read_cell_pointers_into_naive(black_box(&page), black_box(&header), 0, &mut buf)
.unwrap();
black_box(&buf);
}
let read_naive = t.elapsed();
let mut wpage = vec![0u8; 4096];
header.write(&mut wpage, 0);
let t = Instant::now();
for _ in 0..iters {
write_cell_pointers(
black_box(&mut wpage),
0,
black_box(&header),
black_box(&pointers),
);
}
let write_opt = t.elapsed();
let mut wpage = vec![0u8; 4096];
header.write(&mut wpage, 0);
let t = Instant::now();
for _ in 0..iters {
write_cell_pointers_naive(
black_box(&mut wpage),
0,
black_box(&header),
black_box(&pointers),
);
}
let write_naive = t.elapsed();
#[allow(clippy::cast_precision_loss)]
let total = (iters as f64) * (cell_count as f64);
#[allow(clippy::cast_precision_loss)]
let read_opt_ns = read_opt.as_nanos() as f64 / total;
#[allow(clippy::cast_precision_loss)]
let read_naive_ns = read_naive.as_nanos() as f64 / total;
let read_delta = (read_opt_ns - read_naive_ns) / read_naive_ns * 100.0;
#[allow(clippy::cast_precision_loss)]
let write_opt_ns = write_opt.as_nanos() as f64 / total;
#[allow(clippy::cast_precision_loss)]
let write_naive_ns = write_naive.as_nanos() as f64 / total;
let write_delta = (write_opt_ns - write_naive_ns) / write_naive_ns * 100.0;
println!(
"[trial {trial}] read: opt={read_opt_ns:.3} ns/ptr naive={read_naive_ns:.3} ns/ptr delta={read_delta:+.1}% \
| write: opt={write_opt_ns:.3} ns/ptr naive={write_naive_ns:.3} ns/ptr delta={write_delta:+.1}% \
({cell_count} ptrs/page, n={iters})",
);
}
}
#[cold]
#[inline(never)]
fn cell_pointer_oob_bench(
cell_idx: u16,
page_no: PageNumber,
ptr_offset: usize,
page_len: usize,
) -> FrankenError {
FrankenError::DatabaseCorrupt {
detail: format!(
"cell pointer {cell_idx} extends past page {} (ptr_offset={ptr_offset} len={page_len})",
page_no.get(),
),
}
}
#[inline]
fn read_cell_pointer_inline_optimized(
page: &[u8],
page_no: PageNumber,
header_size: usize,
cell_idx: u16,
) -> Result<u16> {
let header_offset = header_offset_for_page(page_no);
let ptr_offset = header_offset + header_size + (cell_idx as usize) * 2;
let bytes: &[u8; 2] = page
.get(ptr_offset..ptr_offset + 2)
.and_then(|s| s.try_into().ok())
.ok_or_else(|| cell_pointer_oob_bench(cell_idx, page_no, ptr_offset, page.len()))?;
Ok(u16::from_be_bytes(*bytes))
}
#[inline(never)]
fn read_cell_pointer_inline_naive(
page: &[u8],
page_no: PageNumber,
header_size: usize,
cell_idx: u16,
) -> Result<u16> {
let header_offset = header_offset_for_page(page_no);
let ptr_offset = header_offset + header_size + (cell_idx as usize) * 2;
if ptr_offset + 2 > page.len() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"cell pointer {cell_idx} extends past page {} (ptr_offset={ptr_offset} len={})",
page_no.get(),
page.len()
),
});
}
Ok(u16::from_be_bytes([page[ptr_offset], page[ptr_offset + 1]]))
}
#[test]
#[ignore = "microbench; run with --release --ignored --nocapture bench_read_cell_pointer_inline"]
fn bench_read_cell_pointer_inline() {
use std::hint::black_box;
use std::time::Instant;
let cell_count: usize = 32;
let header = BtreePageHeader {
page_type: BtreePageType::InteriorTable,
first_freeblock: 0,
#[allow(clippy::cast_possible_truncation)]
cell_count: cell_count as u16,
cell_content_offset: 3000,
fragmented_free_bytes: 0,
right_child: PageNumber::new(99),
};
let mut page = vec![0u8; 4096];
header.write(&mut page, 0);
let header_size = usize::from(header.page_type.header_size());
for i in 0..cell_count {
let off = header_size + i * 2;
#[allow(clippy::cast_possible_truncation)]
let payload = (3000 + i as u16 * 10).to_be_bytes();
page[off] = payload[0];
page[off + 1] = payload[1];
}
let page_no = PageNumber::new(7).unwrap();
let iters: u64 = 4_000_000;
let trials = 5;
for trial in 0..trials {
let t = Instant::now();
let mut sum_opt = 0u64;
for i in 0..iters {
#[allow(clippy::cast_possible_truncation)]
let idx = (i as usize % cell_count) as u16;
let v = read_cell_pointer_inline_optimized(
black_box(&page),
black_box(page_no),
black_box(header_size),
black_box(idx),
)
.unwrap();
sum_opt = sum_opt.wrapping_add(u64::from(v));
}
let opt = t.elapsed();
black_box(sum_opt);
let t = Instant::now();
let mut sum_naive = 0u64;
for i in 0..iters {
#[allow(clippy::cast_possible_truncation)]
let idx = (i as usize % cell_count) as u16;
let v = read_cell_pointer_inline_naive(
black_box(&page),
black_box(page_no),
black_box(header_size),
black_box(idx),
)
.unwrap();
sum_naive = sum_naive.wrapping_add(u64::from(v));
}
let naive = t.elapsed();
black_box(sum_naive);
assert_eq!(sum_opt, sum_naive);
#[allow(clippy::cast_precision_loss)]
let opt_ns = opt.as_nanos() as f64 / iters as f64;
#[allow(clippy::cast_precision_loss)]
let naive_ns = naive.as_nanos() as f64 / iters as f64;
let delta_pct = (opt_ns - naive_ns) / naive_ns * 100.0;
println!(
"[trial {trial}] read_cell_pointer_inline: optimized={opt_ns:.3} ns naive={naive_ns:.3} ns \
delta={delta_pct:+.1}% (n={iters}, cells={cell_count})",
);
}
}
}