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]])
}
#[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> {
debug_assert!(!self.page_type.is_leaf());
if i >= self.num_cells {
return Ok(self.right_pointer);
}
let off = self.cell_offset(i)?;
Ok(be_u32(self.data(), off))
}
pub fn table_interior_key(&self, i: usize) -> Result<i64> {
debug_assert_eq!(self.page_type, PageType::InteriorTable);
let off = self.cell_offset(i)?;
let (rowid, _) = varint::decode_i64(&self.data()[off + 4..])
.ok_or_else(|| Error::Corrupt("truncated interior cell rowid".into()))?;
Ok(rowid)
}
pub fn table_leaf_cell(&self, i: usize, usable: usize) -> Result<TableLeafCell> {
debug_assert_eq!(self.page_type, PageType::LeafTable);
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 index_cell(&self, i: usize, usable: usize) -> Result<IndexCell> {
debug_assert!(!self.page_type.is_table());
let off = self.cell_offset(i)?;
let data = self.data();
let (left_child, key_off) = if self.page_type == PageType::InteriorIndex {
(be_u32(data, off), off + 4)
} else {
(0, off)
};
let (plen, n1) = varint::decode(&data[key_off..])
.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,
})
}
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,
})
}
}
fn payload_split(page_type: PageType, usable: usize, p: usize) -> (usize, bool) {
let max_local = match page_type {
PageType::LeafTable => usable - 35,
PageType::LeafIndex | PageType::InteriorIndex => (usable - 12) * 64 / 255 - 23,
PageType::InteriorTable => return (0, false),
};
if p <= max_local {
return (p, false);
}
let min_local = (usable - 12) * 32 / 255 - 23;
let k = min_local + (p - min_local) % (usable - 4);
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);
}
}