use crate::error::{Result, SqliteError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageKind {
InteriorIndex,
InteriorTable,
LeafIndex,
LeafTable,
}
impl PageKind {
fn from_byte(b: u8) -> Option<PageKind> {
match b {
2 => Some(PageKind::InteriorIndex),
5 => Some(PageKind::InteriorTable),
10 => Some(PageKind::LeafIndex),
13 => Some(PageKind::LeafTable),
_ => None,
}
}
pub fn is_interior(self) -> bool {
matches!(self, PageKind::InteriorIndex | PageKind::InteriorTable)
}
pub fn is_table(self) -> bool {
matches!(self, PageKind::InteriorTable | PageKind::LeafTable)
}
fn header_len(self) -> usize {
if self.is_interior() {
12
} else {
8
}
}
}
pub struct BtreePage {
pub number: u32,
pub kind: PageKind,
pub right_most: Option<u32>,
bytes: Vec<u8>,
usable: usize,
cell_pointers: Vec<u16>,
}
impl BtreePage {
pub(crate) fn parse(
number: u32,
bytes: Vec<u8>,
usable: usize,
body_offset: usize,
) -> Result<BtreePage> {
if body_offset + 8 > usable {
return Err(SqliteError::malformed(
number,
"page is too small to hold a b-tree header",
));
}
let kind = PageKind::from_byte(bytes[body_offset]).ok_or_else(|| {
SqliteError::malformed(
number,
format!(
"page type {} is not one of 2, 5, 10 or 13",
bytes[body_offset]
),
)
})?;
let header_len = kind.header_len();
if body_offset + header_len > usable {
return Err(SqliteError::malformed(
number,
"page is too small to hold an interior b-tree header",
));
}
let cell_count = be16(&bytes, body_offset + 3) as usize;
let pointers_end = body_offset + header_len + cell_count * 2;
if pointers_end > usable {
return Err(SqliteError::malformed(
number,
format!("{cell_count} cell pointers do not fit in {usable} usable bytes"),
));
}
let content_start = match be16(&bytes, body_offset + 5) {
0 => 65536,
n => n as usize,
};
if content_start > usable {
return Err(SqliteError::malformed(
number,
format!("cell content area starts at {content_start}, past the usable {usable}"),
));
}
if cell_count > 0 && content_start < pointers_end {
return Err(SqliteError::malformed(
number,
format!(
"cell content area starts at {content_start}, inside the header and pointer \
array which end at {pointers_end}"
),
));
}
let mut cell_pointers = Vec::with_capacity(cell_count);
for i in 0..cell_count {
let at = be16(&bytes, body_offset + header_len + i * 2) as usize;
if at < pointers_end || at >= usable {
return Err(SqliteError::malformed(
number,
format!(
"cell {i} points at offset {at}, outside the content area \
{pointers_end}..{usable}"
),
));
}
cell_pointers.push(at as u16);
}
let right_most = if kind.is_interior() {
let child = be32(&bytes, body_offset + 8);
if child == 0 {
return Err(SqliteError::malformed(
number,
"interior page has a right most child pointer of 0",
));
}
Some(child)
} else {
None
};
Ok(BtreePage {
number,
kind,
right_most,
bytes,
usable,
cell_pointers,
})
}
pub fn cell_count(&self) -> usize {
self.cell_pointers.len()
}
pub fn cell(&self, index: usize) -> Result<&[u8]> {
let at = *self.cell_pointers.get(index).ok_or_else(|| {
SqliteError::malformed(
self.number,
format!("cell {index} requested, page has {}", self.cell_count()),
)
})? as usize;
Ok(&self.bytes[at..self.usable])
}
}
pub(crate) fn be16(bytes: &[u8], at: usize) -> u16 {
u16::from_be_bytes([bytes[at], bytes[at + 1]])
}
pub(crate) fn be32(bytes: &[u8], at: usize) -> u32 {
u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}