use crate::error::{Error, Result};
use crate::pager::Page;
use crate::util::varint;
use alloc::format;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageType {
InteriorIndex,
InteriorTable,
LeafIndex,
LeafTable,
}
impl PageType {
fn from_byte(b: u8) -> Result<PageType> {
Ok(match b {
2 => PageType::InteriorIndex,
5 => PageType::InteriorTable,
10 => PageType::LeafIndex,
13 => PageType::LeafTable,
other => return Err(Error::Corrupt(format!("invalid b-tree page type {other}"))),
})
}
pub fn is_leaf(self) -> bool {
matches!(self, PageType::LeafIndex | PageType::LeafTable)
}
pub fn is_table(self) -> bool {
matches!(self, PageType::InteriorTable | PageType::LeafTable)
}
fn header_len(self) -> usize {
if self.is_leaf() {
8
} else {
12
}
}
}
#[inline]
fn be_u16(buf: &[u8], at: usize) -> u16 {
u16::from_be_bytes([buf[at], buf[at + 1]])
}
#[inline]
fn be_u32(buf: &[u8], at: usize) -> u32 {
u32::from_be_bytes([buf[at], buf[at + 1], buf[at + 2], buf[at + 3]])
}
#[inline]
fn be_u32_checked(buf: &[u8], at: usize) -> Result<u32> {
let bytes = buf
.get(at..at + 4)
.ok_or_else(|| Error::Corrupt("4-byte read past end of page".into()))?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Payload {
pub total_len: usize,
pub local_offset: usize,
pub local_len: usize,
pub overflow: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct TableLeafCell {
pub rowid: i64,
pub payload: Payload,
}
#[derive(Debug, Clone, Copy)]
pub struct IndexCell {
pub left_child: u32,
pub payload: Payload,
}
#[derive(Debug, Clone)]
pub struct BtreePage {
page: Page,
page_type: PageType,
body_offset: usize,
num_cells: usize,
right_pointer: u32,
}
impl BtreePage {
pub fn parse(page: Page) -> Result<BtreePage> {
let body = page.body_offset();
let data = page.data();
if body + 8 > data.len() {
return Err(Error::Corrupt("page too small for b-tree header".into()));
}
let page_type = PageType::from_byte(data[body])?;
if body + page_type.header_len() > data.len() {
return Err(Error::Corrupt("page too small for interior header".into()));
}
let num_cells = be_u16(data, body + 3) as usize;
let right_pointer = if page_type.is_leaf() {
0
} else {
be_u32(data, body + 8)
};
Ok(BtreePage {
page,
page_type,
body_offset: body,
num_cells,
right_pointer,
})
}
pub fn page_type(&self) -> PageType {
self.page_type
}
pub fn num_cells(&self) -> usize {
self.num_cells
}
pub fn right_pointer(&self) -> u32 {
self.right_pointer
}
pub fn data(&self) -> &[u8] {
self.page.data()
}
fn cell_offset(&self, i: usize) -> Result<usize> {
if i >= self.num_cells {
return Err(Error::Corrupt(format!(
"cell index {i} out of range (num_cells={})",
self.num_cells
)));
}
let ptr = self.body_offset + self.page_type.header_len() + 2 * i;
let data = self.data();
if ptr + 2 > data.len() {
return Err(Error::Corrupt("cell pointer past end of page".into()));
}
let off = be_u16(data, ptr) as usize;
if off >= data.len() {
return Err(Error::Corrupt("cell offset past end of page".into()));
}
Ok(off)
}
pub fn child_pointer(&self, i: usize) -> Result<u32> {
if self.page_type.is_leaf() {
return Err(Error::Corrupt(
"child pointer requested on a leaf page".into(),
));
}
if i >= self.num_cells {
return Ok(self.right_pointer);
}
let off = self.cell_offset(i)?;
be_u32_checked(self.data(), off)
}
pub fn table_interior_key(&self, i: usize) -> Result<i64> {
if self.page_type != PageType::InteriorTable {
return Err(Error::Corrupt(
"interior-table key on non-interior page".into(),
));
}
let off = self.cell_offset(i)?;
let data = self.data();
let key_off = off
.checked_add(4)
.filter(|&e| e <= data.len())
.ok_or_else(|| Error::Corrupt("interior cell past end of page".into()))?;
let (rowid, _) = varint::decode_i64(&data[key_off..])
.ok_or_else(|| Error::Corrupt("truncated interior cell rowid".into()))?;
Ok(rowid)
}
pub fn table_leaf_cell(&self, i: usize, usable: usize) -> Result<TableLeafCell> {
if self.page_type != PageType::LeafTable {
return Err(Error::Corrupt(
"table-leaf cell on non-table-leaf page".into(),
));
}
let off = self.cell_offset(i)?;
let data = self.data();
let (plen, n1) = varint::decode(&data[off..])
.ok_or_else(|| Error::Corrupt("truncated leaf payload length".into()))?;
let (rowid, n2) = varint::decode_i64(&data[off + n1..])
.ok_or_else(|| Error::Corrupt("truncated leaf rowid".into()))?;
let payload_start = off + n1 + n2;
let payload = self.payload_at(payload_start, plen as usize, usable)?;
Ok(TableLeafCell { rowid, payload })
}
pub fn raw_table_leaf_cell(&self, i: usize, usable: usize) -> Result<&[u8]> {
if self.page_type != PageType::LeafTable {
return Err(Error::Corrupt(
"raw table-leaf cell on non-table-leaf page".into(),
));
}
let off = self.cell_offset(i)?;
let data = self.data();
let (plen, n1) = varint::decode(&data[off..])
.ok_or_else(|| Error::Corrupt("truncated leaf payload length".into()))?;
let (_, n2) = varint::decode_i64(&data[off + n1..])
.ok_or_else(|| Error::Corrupt("truncated leaf rowid".into()))?;
let (local_len, has_overflow) = payload_split(self.page_type, usable, plen as usize);
let len = n1 + n2 + local_len + if has_overflow { 4 } else { 0 };
if off + len > data.len() {
return Err(Error::Corrupt("leaf cell extends past page".into()));
}
Ok(&data[off..off + len])
}
pub fn raw_index_record_cell(&self, i: usize, usable: usize) -> Result<&[u8]> {
if self.page_type.is_table() {
return Err(Error::Corrupt("index record cell on a table page".into()));
}
let off = self.cell_offset(i)?;
let data = self.data();
let key_off = if self.page_type == PageType::InteriorIndex {
off + 4
} else {
off
};
let key_bytes = data
.get(key_off..)
.ok_or_else(|| Error::Corrupt("index cell key past end of page".into()))?;
let (plen, n1) = varint::decode(key_bytes)
.ok_or_else(|| Error::Corrupt("truncated index payload length".into()))?;
let (local_len, has_overflow) = payload_split(self.page_type, usable, plen as usize);
let len = n1 + local_len + if has_overflow { 4 } else { 0 };
if key_off + len > data.len() {
return Err(Error::Corrupt("index cell extends past page".into()));
}
Ok(&data[key_off..key_off + len])
}
pub fn index_cell(&self, i: usize, usable: usize) -> Result<IndexCell> {
if self.page_type.is_table() {
return Err(Error::Corrupt("index cell on a table page".into()));
}
let off = self.cell_offset(i)?;
let data = self.data();
let (left_child, key_off) = if self.page_type == PageType::InteriorIndex {
(be_u32_checked(data, off)?, off + 4)
} else {
(0, off)
};
let key_bytes = data
.get(key_off..)
.ok_or_else(|| Error::Corrupt("index cell key past end of page".into()))?;
let (plen, n1) = varint::decode(key_bytes)
.ok_or_else(|| Error::Corrupt("truncated index payload length".into()))?;
let payload = self.payload_at(key_off + n1, plen as usize, usable)?;
Ok(IndexCell {
left_child,
payload,
})
}
pub fn child_pointer_offset(&self, i: usize) -> Result<usize> {
if self.page_type.is_leaf() {
return Err(Error::Corrupt(
"child pointer offset requested on a leaf page".into(),
));
}
if i >= self.num_cells {
return Ok(self.body_offset + 8);
}
self.cell_offset(i)
}
pub fn cell_overflow_offset(&self, i: usize, usable: usize) -> Result<Option<usize>> {
let payload = match self.page_type {
PageType::LeafTable => self.table_leaf_cell(i, usable)?.payload,
PageType::LeafIndex | PageType::InteriorIndex => self.index_cell(i, usable)?.payload,
PageType::InteriorTable => return Ok(None),
};
if payload.overflow == 0 {
Ok(None)
} else {
Ok(Some(payload.local_offset + payload.local_len))
}
}
fn payload_at(&self, payload_start: usize, total: usize, usable: usize) -> Result<Payload> {
let (local_len, has_overflow) = payload_split(self.page_type, usable, total);
let data = self.data();
let need = payload_start + local_len + if has_overflow { 4 } else { 0 };
if need > data.len() {
return Err(Error::Corrupt("cell payload past end of page".into()));
}
let overflow = if has_overflow {
be_u32(data, payload_start + local_len)
} else {
0
};
Ok(Payload {
total_len: total,
local_offset: payload_start,
local_len,
overflow,
})
}
}
pub(crate) fn payload_split(page_type: PageType, usable: usize, p: usize) -> (usize, bool) {
let max_local = match page_type {
PageType::LeafTable => usable.saturating_sub(35),
PageType::LeafIndex | PageType::InteriorIndex => {
(usable.saturating_sub(12) * 64 / 255).saturating_sub(23)
}
PageType::InteriorTable => return (0, false),
};
if p <= max_local {
return (p, false);
}
let min_local = (usable.saturating_sub(12) * 32 / 255).saturating_sub(23);
let span = usable.saturating_sub(4).max(1);
let k = min_local + p.saturating_sub(min_local) % span;
let local = if k <= max_local { k } else { min_local };
(local, true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn payload_split_no_overflow_when_small() {
let (local, ov) = payload_split(PageType::LeafTable, 4096, 100);
assert_eq!((local, ov), (100, false));
}
#[test]
fn payload_split_overflow_for_large_table_leaf() {
let usable = 4096;
let p = 20000;
let (local, ov) = payload_split(PageType::LeafTable, usable, p);
assert!(ov);
let max_local = usable - 35;
let min_local = (usable - 12) * 32 / 255 - 23;
assert!(local >= min_local && local <= max_local);
let k = min_local + (p - min_local) % (usable - 4);
let expect = if k <= max_local { k } else { min_local };
assert_eq!(local, expect);
}
}