use crate::error::GpkgError;
pub fn decode_sqlite_varint(data: &[u8]) -> Result<(u64, usize), GpkgError> {
if data.is_empty() {
return Err(GpkgError::InsufficientData {
needed: 1,
available: 0,
});
}
let mut result: u64 = 0;
for i in 0..8 {
if i >= data.len() {
return Err(GpkgError::InsufficientData {
needed: i + 1,
available: data.len(),
});
}
let byte = data[i];
result = (result << 7) | (byte & 0x7F) as u64;
if byte & 0x80 == 0 {
return Ok((result, i + 1));
}
}
if data.len() < 9 {
return Err(GpkgError::InsufficientData {
needed: 9,
available: data.len(),
});
}
result = (result << 8) | data[8] as u64;
Ok((result, 9))
}
pub fn encode_sqlite_varint(mut value: u64) -> Vec<u8> {
if value <= 0x7F {
return vec![value as u8];
}
if value > 0x00FF_FFFF_FFFF_FFFF {
let mut buf = [0u8; 9];
buf[8] = (value & 0xFF) as u8;
value >>= 8;
for i in (0..8).rev() {
buf[i] = (value & 0x7F) as u8 | 0x80;
value >>= 7;
}
return buf.to_vec();
}
let mut temp = [0u8; 9];
temp[0] = (value & 0x7F) as u8;
value >>= 7;
let mut len: usize = 1;
while value > 0 {
temp[len] = (value & 0x7F) as u8;
value >>= 7;
len += 1;
}
let mut result = Vec::with_capacity(len);
for i in (1..len).rev() {
result.push(temp[i] | 0x80);
}
result.push(temp[0]); result
}
#[derive(Debug, Clone, PartialEq)]
pub enum CellValue {
Null,
Integer(i64),
Float(f64),
Text(String),
Blob(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct MasterEntry {
pub entry_type: String,
pub name: String,
pub tbl_name: String,
pub rootpage: u32,
pub sql: String,
}
fn parse_record(data: &[u8]) -> Result<Vec<CellValue>, GpkgError> {
if data.is_empty() {
return Err(GpkgError::InvalidFormat("Empty record payload".into()));
}
let (header_len, hdr_varint_size) = decode_sqlite_varint(data)?;
let header_len = header_len as usize;
if header_len > data.len() {
return Err(GpkgError::InsufficientData {
needed: header_len,
available: data.len(),
});
}
let mut serial_types = Vec::new();
let mut hdr_pos = hdr_varint_size;
while hdr_pos < header_len {
if hdr_pos >= data.len() {
return Err(GpkgError::InsufficientData {
needed: hdr_pos + 1,
available: data.len(),
});
}
let (st, consumed) = decode_sqlite_varint(&data[hdr_pos..])?;
hdr_pos += consumed;
serial_types.push(st);
}
let mut val_pos = header_len;
let mut values = Vec::with_capacity(serial_types.len());
for st in &serial_types {
let (value, size) = decode_serial_value(data, val_pos, *st)?;
values.push(value);
val_pos += size;
}
Ok(values)
}
fn decode_serial_value(
data: &[u8],
pos: usize,
serial_type: u64,
) -> Result<(CellValue, usize), GpkgError> {
match serial_type {
0 => Ok((CellValue::Null, 0)),
1 => {
check_bounds(data, pos, 1)?;
let v = data[pos] as i8 as i64;
Ok((CellValue::Integer(v), 1))
}
2 => {
check_bounds(data, pos, 2)?;
let v = i16::from_be_bytes([data[pos], data[pos + 1]]) as i64;
Ok((CellValue::Integer(v), 2))
}
3 => {
check_bounds(data, pos, 3)?;
let raw =
((data[pos] as u32) << 16) | ((data[pos + 1] as u32) << 8) | data[pos + 2] as u32;
let v = if raw & 0x80_0000 != 0 {
(raw | 0xFF00_0000) as i32 as i64
} else {
raw as i64
};
Ok((CellValue::Integer(v), 3))
}
4 => {
check_bounds(data, pos, 4)?;
let v =
i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as i64;
Ok((CellValue::Integer(v), 4))
}
5 => {
check_bounds(data, pos, 6)?;
let raw = ((data[pos] as u64) << 40)
| ((data[pos + 1] as u64) << 32)
| ((data[pos + 2] as u64) << 24)
| ((data[pos + 3] as u64) << 16)
| ((data[pos + 4] as u64) << 8)
| data[pos + 5] as u64;
let v = if raw & 0x0000_8000_0000_0000 != 0 {
(raw | 0xFFFF_0000_0000_0000) as i64
} else {
raw as i64
};
Ok((CellValue::Integer(v), 6))
}
6 => {
check_bounds(data, pos, 8)?;
let v = i64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
Ok((CellValue::Integer(v), 8))
}
7 => {
check_bounds(data, pos, 8)?;
let v = f64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
Ok((CellValue::Float(v), 8))
}
8 => Ok((CellValue::Integer(0), 0)),
9 => Ok((CellValue::Integer(1), 0)),
10 | 11 => Ok((CellValue::Null, 0)),
n if n >= 12 && n % 2 == 0 => {
let len = ((n - 12) / 2) as usize;
check_bounds(data, pos, len)?;
let blob = data[pos..pos + len].to_vec();
Ok((CellValue::Blob(blob), len))
}
n if n >= 13 => {
let len = ((n - 13) / 2) as usize;
check_bounds(data, pos, len)?;
let text = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
Ok((CellValue::Text(text), len))
}
other => Err(GpkgError::InvalidFormat(format!(
"Unknown serial type {other}"
))),
}
}
fn check_bounds(data: &[u8], pos: usize, size: usize) -> Result<(), GpkgError> {
let end = pos
.checked_add(size)
.ok_or_else(|| GpkgError::InvalidFormat("Overflow computing data bounds".into()))?;
if end > data.len() {
return Err(GpkgError::InsufficientData {
needed: end,
available: data.len(),
});
}
Ok(())
}
pub fn parse_leaf_table_page(
page_data: &[u8],
page_size: usize,
header_offset: usize,
) -> Result<Vec<(i64, Vec<CellValue>)>, GpkgError> {
if page_data.len() < page_size {
return Err(GpkgError::InsufficientData {
needed: page_size,
available: page_data.len(),
});
}
let hdr = header_offset;
if hdr >= page_data.len() {
return Err(GpkgError::InvalidFormat(
"Header offset beyond page boundary".into(),
));
}
let page_type = page_data[hdr];
if page_type != 13 {
return Err(GpkgError::InvalidFormat(format!(
"Expected leaf table page (type 13), got type {page_type}"
)));
}
if hdr + 8 > page_data.len() {
return Err(GpkgError::InsufficientData {
needed: hdr + 8,
available: page_data.len(),
});
}
let cell_count = u16::from_be_bytes([page_data[hdr + 3], page_data[hdr + 4]]) as usize;
let ptr_array_start = hdr + 8;
let ptr_array_end = ptr_array_start + cell_count * 2;
if ptr_array_end > page_data.len() {
return Err(GpkgError::InsufficientData {
needed: ptr_array_end,
available: page_data.len(),
});
}
let mut rows = Vec::with_capacity(cell_count);
for i in 0..cell_count {
let ptr_offset = ptr_array_start + i * 2;
let cell_offset =
u16::from_be_bytes([page_data[ptr_offset], page_data[ptr_offset + 1]]) as usize;
if cell_offset >= page_size || cell_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Cell pointer {i} offset {cell_offset} out of page bounds"
)));
}
let cell_data = &page_data[cell_offset..];
let (payload_len, pl_size) = decode_sqlite_varint(cell_data)?;
let payload_len = payload_len as usize;
let (rowid_raw, rid_size) = decode_sqlite_varint(&cell_data[pl_size..])?;
let rowid = rowid_raw as i64;
let payload_start = pl_size + rid_size;
let usable_size = page_size; let overflow_threshold = usable_size.saturating_sub(35);
let inline_len = if payload_len > overflow_threshold {
let m = overflow_threshold.min(payload_len);
if payload_start + m + 4 > cell_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Cell {i}: overflow cell needs {} bytes inline + 4-byte pointer, \
but only {} available from cell start",
m,
cell_data.len().saturating_sub(payload_start)
)));
}
m
} else {
payload_len
};
if payload_start + inline_len > cell_data.len() {
return Err(GpkgError::InsufficientData {
needed: payload_start + inline_len,
available: cell_data.len(),
});
}
let payload = &cell_data[payload_start..payload_start + inline_len];
let values = if inline_len < payload_len {
parse_record(payload).map_err(|_| {
GpkgError::InvalidFormat(format!(
"Cell {i}: payload overflows to another page \
(total {payload_len} bytes, inline {inline_len} bytes); \
overflow pages are not yet supported"
))
})?
} else {
parse_record(payload)?
};
rows.push((rowid, values));
}
Ok(rows)
}
pub fn parse_interior_table_page(
page_data: &[u8],
page_size: usize,
header_offset: usize,
) -> Result<(Vec<(u32, i64)>, u32), GpkgError> {
if page_data.len() < page_size {
return Err(GpkgError::InsufficientData {
needed: page_size,
available: page_data.len(),
});
}
let hdr = header_offset;
if hdr >= page_data.len() {
return Err(GpkgError::InvalidFormat(
"Header offset beyond page boundary".into(),
));
}
let page_type = page_data[hdr];
if page_type != 5 {
return Err(GpkgError::InvalidFormat(format!(
"Expected interior table page (type 5), got type {page_type}"
)));
}
if hdr + 12 > page_data.len() {
return Err(GpkgError::InsufficientData {
needed: hdr + 12,
available: page_data.len(),
});
}
let cell_count = u16::from_be_bytes([page_data[hdr + 3], page_data[hdr + 4]]) as usize;
let rightmost_child = u32::from_be_bytes([
page_data[hdr + 8],
page_data[hdr + 9],
page_data[hdr + 10],
page_data[hdr + 11],
]);
let ptr_array_start = hdr + 12;
let ptr_array_end = ptr_array_start + cell_count * 2;
if ptr_array_end > page_data.len() {
return Err(GpkgError::InsufficientData {
needed: ptr_array_end,
available: page_data.len(),
});
}
let mut children = Vec::with_capacity(cell_count);
for i in 0..cell_count {
let ptr_offset = ptr_array_start + i * 2;
let cell_offset =
u16::from_be_bytes([page_data[ptr_offset], page_data[ptr_offset + 1]]) as usize;
if cell_offset >= page_size || cell_offset + 4 >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Interior cell pointer {i} offset {cell_offset} out of bounds"
)));
}
let cell_data = &page_data[cell_offset..];
if cell_data.len() < 4 {
return Err(GpkgError::InsufficientData {
needed: cell_offset + 4,
available: page_data.len(),
});
}
let left_child =
u32::from_be_bytes([cell_data[0], cell_data[1], cell_data[2], cell_data[3]]);
let (key_rowid, _) = decode_sqlite_varint(&cell_data[4..])?;
children.push((left_child, key_rowid as i64));
}
Ok((children, rightmost_child))
}
pub fn scan_table(
file_data: &[u8],
root_page: u32,
page_size: usize,
) -> Result<Vec<(i64, Vec<CellValue>)>, GpkgError> {
let mut results = Vec::new();
let mut stack: Vec<u32> = vec![root_page];
while let Some(page_num) = stack.pop() {
let page_data = get_page_slice(file_data, page_num, page_size)?;
let header_offset = if page_num == 1 { 100 } else { 0 };
if header_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: header offset {header_offset} beyond page"
)));
}
let page_type = page_data[header_offset];
match page_type {
13 => {
let rows = parse_leaf_table_page(page_data, page_size, header_offset)?;
results.extend(rows);
}
5 => {
let (children, rightmost) =
parse_interior_table_page(page_data, page_size, header_offset)?;
stack.push(rightmost);
for (child_page, _key) in children.iter().rev() {
stack.push(*child_page);
}
}
other => {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: unexpected B-tree page type {other} \
(expected 5 for interior or 13 for leaf)"
)));
}
}
}
Ok(results)
}
fn get_page_slice(file_data: &[u8], page_num: u32, page_size: usize) -> Result<&[u8], GpkgError> {
if page_num == 0 {
return Err(GpkgError::InvalidFormat(
"Page numbers are 1-indexed; 0 is invalid".into(),
));
}
let offset = (page_num as usize - 1) * page_size;
let end = offset + page_size;
if end > file_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num} out of range (file has {} bytes, need {end})",
file_data.len()
)));
}
Ok(&file_data[offset..end])
}
pub fn scan_table_paginated(
file_data: &[u8],
root_page: u32,
page_size: usize,
offset: usize,
limit: usize,
) -> Result<Vec<(i64, Vec<CellValue>)>, GpkgError> {
if limit == 0 {
return Ok(Vec::new());
}
let mut results = Vec::with_capacity(limit.min(256));
let mut stack: Vec<u32> = vec![root_page];
let mut skipped: usize = 0;
let mut collected: usize = 0;
'outer: while let Some(page_num) = stack.pop() {
let page_data = get_page_slice(file_data, page_num, page_size)?;
let header_offset = if page_num == 1 { 100 } else { 0 };
if header_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: header offset {header_offset} beyond page"
)));
}
let page_type = page_data[header_offset];
match page_type {
13 => {
let rows = parse_leaf_table_page(page_data, page_size, header_offset)?;
for (rowid, cols) in rows {
if skipped < offset {
skipped += 1;
continue;
}
results.push((rowid, cols));
collected += 1;
if collected == limit {
break 'outer;
}
}
}
5 => {
let (children, rightmost) =
parse_interior_table_page(page_data, page_size, header_offset)?;
stack.push(rightmost);
for (child_page, _key) in children.iter().rev() {
stack.push(*child_page);
}
}
other => {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: unexpected B-tree page type {other} \
(expected 5 for interior or 13 for leaf)"
)));
}
}
}
Ok(results)
}
pub fn scan_table_filtered(
file_data: &[u8],
root_page: u32,
page_size: usize,
expr: &crate::filter::FilterExpr,
) -> Result<Vec<(i64, Vec<CellValue>)>, GpkgError> {
let mut results = Vec::new();
let mut stack: Vec<u32> = vec![root_page];
while let Some(page_num) = stack.pop() {
let page_data = get_page_slice(file_data, page_num, page_size)?;
let header_offset = if page_num == 1 { 100 } else { 0 };
if header_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: header offset {header_offset} beyond page"
)));
}
let page_type = page_data[header_offset];
match page_type {
13 => {
let rows = parse_leaf_table_page(page_data, page_size, header_offset)?;
for (rowid, cols) in rows {
if crate::filter::evaluate(expr, &cols) {
results.push((rowid, cols));
}
}
}
5 => {
let (children, rightmost) =
parse_interior_table_page(page_data, page_size, header_offset)?;
stack.push(rightmost);
for (child_page, _key) in children.iter().rev() {
stack.push(*child_page);
}
}
other => {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: unexpected B-tree page type {other} \
(expected 5 for interior or 13 for leaf)"
)));
}
}
}
Ok(results)
}
pub fn scan_table_filtered_paginated(
file_data: &[u8],
root_page: u32,
page_size: usize,
expr: &crate::filter::FilterExpr,
offset: usize,
limit: usize,
) -> Result<Vec<(i64, Vec<CellValue>)>, GpkgError> {
if limit == 0 {
return Ok(Vec::new());
}
let mut results = Vec::with_capacity(limit.min(256));
let mut stack: Vec<u32> = vec![root_page];
let mut skipped: usize = 0;
let mut collected: usize = 0;
'outer: while let Some(page_num) = stack.pop() {
let page_data = get_page_slice(file_data, page_num, page_size)?;
let header_offset = if page_num == 1 { 100 } else { 0 };
if header_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: header offset {header_offset} beyond page"
)));
}
let page_type = page_data[header_offset];
match page_type {
13 => {
let rows = parse_leaf_table_page(page_data, page_size, header_offset)?;
for (rowid, cols) in rows {
if !crate::filter::evaluate(expr, &cols) {
continue;
}
if skipped < offset {
skipped += 1;
continue;
}
results.push((rowid, cols));
collected += 1;
if collected == limit {
break 'outer;
}
}
}
5 => {
let (children, rightmost) =
parse_interior_table_page(page_data, page_size, header_offset)?;
stack.push(rightmost);
for (child_page, _key) in children.iter().rev() {
stack.push(*child_page);
}
}
other => {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: unexpected B-tree page type {other} \
(expected 5 for interior or 13 for leaf)"
)));
}
}
}
Ok(results)
}
pub fn count_table_rows(
file_data: &[u8],
root_page: u32,
page_size: usize,
) -> Result<u64, GpkgError> {
let mut total: u64 = 0;
let mut stack: Vec<u32> = vec![root_page];
while let Some(page_num) = stack.pop() {
let page_data = get_page_slice(file_data, page_num, page_size)?;
let header_offset = if page_num == 1 { 100 } else { 0 };
if header_offset >= page_data.len() {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: header offset {header_offset} beyond page"
)));
}
let page_type = page_data[header_offset];
match page_type {
13 => {
if header_offset + 5 > page_data.len() {
return Err(GpkgError::InsufficientData {
needed: header_offset + 5,
available: page_data.len(),
});
}
let cell_count = u16::from_be_bytes([
page_data[header_offset + 3],
page_data[header_offset + 4],
]) as u64;
total += cell_count;
}
5 => {
let (children, rightmost) =
parse_interior_table_page(page_data, page_size, header_offset)?;
stack.push(rightmost);
for (child_page, _key) in children.iter().rev() {
stack.push(*child_page);
}
}
other => {
return Err(GpkgError::InvalidFormat(format!(
"Page {page_num}: unexpected B-tree page type {other} \
(expected 5 for interior or 13 for leaf)"
)));
}
}
}
Ok(total)
}
pub fn scan_sqlite_master(
file_data: &[u8],
page_size: usize,
) -> Result<Vec<MasterEntry>, GpkgError> {
let rows = scan_table(file_data, 1, page_size)?;
let mut entries = Vec::with_capacity(rows.len());
for (_, values) in rows {
if values.len() < 5 {
continue; }
let entry_type = cell_value_to_string(&values[0]);
let name = cell_value_to_string(&values[1]);
let tbl_name = cell_value_to_string(&values[2]);
let rootpage = cell_value_to_u32(&values[3]);
let sql = cell_value_to_string(&values[4]);
entries.push(MasterEntry {
entry_type,
name,
tbl_name,
rootpage,
sql,
});
}
Ok(entries)
}
fn cell_value_to_string(val: &CellValue) -> String {
match val {
CellValue::Text(s) => s.clone(),
CellValue::Integer(i) => i.to_string(),
CellValue::Float(f) => f.to_string(),
CellValue::Blob(b) => String::from_utf8_lossy(b).into_owned(),
CellValue::Null => String::new(),
}
}
fn cell_value_to_u32(val: &CellValue) -> u32 {
match val {
CellValue::Integer(i) if *i >= 0 && *i <= u32::MAX as i64 => *i as u32,
_ => 0,
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::approx_constant, clippy::vec_init_then_push)]
mod tests {
use super::*;
#[test]
fn varint_single_byte_zero() {
let data = [0x00];
let (val, len) = decode_sqlite_varint(&data).expect("decode");
assert_eq!(val, 0);
assert_eq!(len, 1);
}
#[test]
fn varint_single_byte_max() {
let data = [0x7F]; let (val, len) = decode_sqlite_varint(&data).expect("decode");
assert_eq!(val, 127);
assert_eq!(len, 1);
}
#[test]
fn varint_two_bytes_128() {
let data = [0x81, 0x00];
let (val, len) = decode_sqlite_varint(&data).expect("decode");
assert_eq!(val, 128);
assert_eq!(len, 2);
}
#[test]
fn varint_two_bytes_16383() {
let data = [0xFF, 0x7F];
let (val, len) = decode_sqlite_varint(&data).expect("decode");
assert_eq!(val, 16383);
assert_eq!(len, 2);
}
#[test]
fn varint_nine_bytes_max_u64() {
let data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
let (val, len) = decode_sqlite_varint(&data).expect("decode");
assert_eq!(val, u64::MAX);
assert_eq!(len, 9);
}
#[test]
fn varint_empty_data_error() {
let data: [u8; 0] = [];
assert!(decode_sqlite_varint(&data).is_err());
}
#[test]
fn varint_encode_decode_roundtrip() {
let test_values = [
0u64,
1,
127,
128,
255,
256,
16383,
16384,
1_000_000,
u32::MAX as u64,
u64::MAX / 2,
u64::MAX,
];
for &v in &test_values {
let encoded = encode_sqlite_varint(v);
let (decoded, len) = decode_sqlite_varint(&encoded)
.unwrap_or_else(|e| panic!("Failed to decode varint for value {v}: {e}"));
assert_eq!(
decoded, v,
"Round-trip failed for value {v}: encoded={encoded:?}, decoded={decoded}"
);
assert_eq!(len, encoded.len(), "Consumed length mismatch for value {v}");
}
}
#[test]
fn varint_encode_single_byte_values() {
for v in 0..=127u64 {
let encoded = encode_sqlite_varint(v);
assert_eq!(encoded.len(), 1, "Values 0-127 should encode to 1 byte");
assert_eq!(encoded[0], v as u8);
}
}
#[test]
fn parse_record_null_column() {
let data = [0x02, 0x00];
let values = parse_record(&data).expect("parse");
assert_eq!(values.len(), 1);
assert_eq!(values[0], CellValue::Null);
}
#[test]
fn parse_record_integer_i8() {
let data = [0x02, 0x01, 0x2A];
let values = parse_record(&data).expect("parse");
assert_eq!(values.len(), 1);
assert_eq!(values[0], CellValue::Integer(42));
}
#[test]
fn parse_record_integer_i16() {
let data = [0x02, 0x02, 0x01, 0x00];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(256));
}
#[test]
fn parse_record_float_f64() {
let mut data = vec![0x02, 0x07];
data.extend_from_slice(&3.14f64.to_be_bytes());
let values = parse_record(&data).expect("parse");
if let CellValue::Float(f) = values[0] {
assert!((f - 3.14).abs() < 1e-10);
} else {
panic!("Expected Float, got {:?}", values[0]);
}
}
#[test]
fn parse_record_literal_zero() {
let data = [0x02, 0x08];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(0));
}
#[test]
fn parse_record_literal_one() {
let data = [0x02, 0x09];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(1));
}
#[test]
fn parse_record_text() {
let data = [0x02, 0x0F, b'A'];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Text("A".to_string()));
}
#[test]
fn parse_record_text_multi_byte() {
let mut data = vec![0x02, 0x17];
data.extend_from_slice(b"Hello");
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Text("Hello".to_string()));
}
#[test]
fn parse_record_blob() {
let data = [0x02, 0x12, 0xDE, 0xAD, 0xBE];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Blob(vec![0xDE, 0xAD, 0xBE]));
}
#[test]
fn parse_record_multiple_columns() {
let data = [0x03, 0x01, 0x11, 0x2A, b'H', b'i'];
let values = parse_record(&data).expect("parse");
assert_eq!(values.len(), 2);
assert_eq!(values[0], CellValue::Integer(42));
assert_eq!(values[1], CellValue::Text("Hi".to_string()));
}
fn build_leaf_page(page_size: usize, cells: &[(i64, &[u8])], header_offset: usize) -> Vec<u8> {
let mut page = vec![0u8; page_size];
let cell_count = cells.len();
let mut content_end = page_size;
let mut cell_offsets = Vec::with_capacity(cell_count);
for (rowid, payload) in cells {
let pl_varint = encode_sqlite_varint(payload.len() as u64);
let rid_varint = encode_sqlite_varint(*rowid as u64);
let cell_size = pl_varint.len() + rid_varint.len() + payload.len();
content_end -= cell_size;
let start = content_end;
cell_offsets.push(start);
let mut pos = start;
page[pos..pos + pl_varint.len()].copy_from_slice(&pl_varint);
pos += pl_varint.len();
page[pos..pos + rid_varint.len()].copy_from_slice(&rid_varint);
pos += rid_varint.len();
page[pos..pos + payload.len()].copy_from_slice(payload);
}
let hdr = header_offset;
page[hdr] = 13; page[hdr + 1] = 0; page[hdr + 2] = 0; page[hdr + 3] = ((cell_count >> 8) & 0xFF) as u8;
page[hdr + 4] = (cell_count & 0xFF) as u8;
let content_start = content_end as u16;
page[hdr + 5] = ((content_start >> 8) & 0xFF) as u8;
page[hdr + 6] = (content_start & 0xFF) as u8;
page[hdr + 7] = 0;
let ptr_start = hdr + 8;
for (i, offset) in cell_offsets.iter().enumerate() {
let o = *offset as u16;
page[ptr_start + i * 2] = ((o >> 8) & 0xFF) as u8;
page[ptr_start + i * 2 + 1] = (o & 0xFF) as u8;
}
page
}
#[test]
fn parse_leaf_page_single_cell() {
let page_size = 4096;
let payload = [0x02u8, 0x08]; let page = build_leaf_page(page_size, &[(1, &payload)], 0);
let rows = parse_leaf_table_page(&page, page_size, 0).expect("parse");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, 1); assert_eq!(rows[0].1.len(), 1);
assert_eq!(rows[0].1[0], CellValue::Integer(0));
}
#[test]
fn parse_leaf_page_multiple_cells() {
let page_size = 4096;
let payload1 = [0x02u8, 0x01, 0x2A];
let payload2 = [0x02u8, 0x11, b'A', b'B'];
let page = build_leaf_page(page_size, &[(1, &payload1), (2, &payload2)], 0);
let rows = parse_leaf_table_page(&page, page_size, 0).expect("parse");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].0, 1);
assert_eq!(rows[0].1[0], CellValue::Integer(42));
assert_eq!(rows[1].0, 2);
assert_eq!(rows[1].1[0], CellValue::Text("AB".to_string()));
}
#[test]
fn parse_leaf_page_with_header_offset_100() {
let page_size = 4096;
let payload = [0x02u8, 0x09]; let page = build_leaf_page(page_size, &[(10, &payload)], 100);
let rows = parse_leaf_table_page(&page, page_size, 100).expect("parse page 1");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, 10);
assert_eq!(rows[0].1[0], CellValue::Integer(1));
}
#[test]
fn parse_leaf_page_wrong_type_error() {
let mut page = vec![0u8; 4096];
page[0] = 5; let result = parse_leaf_table_page(&page, 4096, 0);
assert!(result.is_err());
}
#[test]
fn parse_leaf_page_empty() {
let page_size = 4096;
let page = build_leaf_page(page_size, &[], 0);
let rows = parse_leaf_table_page(&page, page_size, 0).expect("parse");
assert!(rows.is_empty());
}
#[test]
fn parse_interior_page_basic() {
let page_size = 4096;
let mut page = vec![0u8; page_size];
let hdr = 0;
page[hdr] = 5; page[hdr + 1] = 0; page[hdr + 2] = 0; page[hdr + 3] = 0; page[hdr + 4] = 1; page[hdr + 5] = 0; page[hdr + 6] = 0; page[hdr + 7] = 0;
page[hdr + 8] = 0;
page[hdr + 9] = 0;
page[hdr + 10] = 0;
page[hdr + 11] = 3;
let cell_offset: u16 = 4080;
page[hdr + 12] = (cell_offset >> 8) as u8;
page[hdr + 13] = (cell_offset & 0xFF) as u8;
let co = cell_offset as usize;
page[co] = 0;
page[co + 1] = 0;
page[co + 2] = 0;
page[co + 3] = 2;
page[co + 4] = 50;
let (children, rightmost) = parse_interior_table_page(&page, page_size, 0).expect("parse");
assert_eq!(children.len(), 1);
assert_eq!(children[0].0, 2); assert_eq!(children[0].1, 50); assert_eq!(rightmost, 3);
}
#[test]
fn parse_interior_page_wrong_type_error() {
let mut page = vec![0u8; 4096];
page[0] = 13; let result = parse_interior_table_page(&page, 4096, 0);
assert!(result.is_err());
}
#[test]
fn scan_table_single_leaf_page() {
let page_size = 4096;
let payload = [0x02u8, 0x01, 0x07];
let page = build_leaf_page(page_size, &[(1, &payload)], 100);
let mut file_data = page;
file_data[..16].copy_from_slice(b"SQLite format 3\x00");
file_data[16..18].copy_from_slice(&(page_size as u16).to_be_bytes());
file_data[28..32].copy_from_slice(&1u32.to_be_bytes());
let rows = scan_table(&file_data, 1, page_size).expect("scan");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, 1);
assert_eq!(rows[0].1[0], CellValue::Integer(7));
}
#[test]
fn scan_table_interior_plus_leaf_with_ten_rows() {
let page_size = 4096;
let mut file_data = vec![0u8; page_size * 3];
file_data[..16].copy_from_slice(b"SQLite format 3\x00");
file_data[16..18].copy_from_slice(&(page_size as u16).to_be_bytes());
file_data[28..32].copy_from_slice(&3u32.to_be_bytes());
let hdr = 100;
file_data[hdr] = 5; file_data[hdr + 3] = 0;
file_data[hdr + 4] = 1; file_data[hdr + 5] = ((4080u16) >> 8) as u8;
file_data[hdr + 6] = (4080u16 & 0xFF) as u8;
file_data[hdr + 8] = 0;
file_data[hdr + 9] = 0;
file_data[hdr + 10] = 0;
file_data[hdr + 11] = 3; file_data[hdr + 12] = (4080u16 >> 8) as u8;
file_data[hdr + 13] = (4080u16 & 0xFF) as u8;
file_data[4080] = 0;
file_data[4081] = 0;
file_data[4082] = 0;
file_data[4083] = 2;
file_data[4084] = 5;
let page2_cells: Vec<(i64, Vec<u8>)> = (1..=5i64)
.map(|rowid| {
let val = (rowid * 10) as u8;
(rowid, vec![0x02u8, 0x01, val])
})
.collect();
let page2_refs: Vec<(i64, &[u8])> = page2_cells
.iter()
.map(|(r, p)| (*r, p.as_slice()))
.collect();
let leaf2 = build_leaf_page(page_size, &page2_refs, 0);
file_data[page_size..page_size * 2].copy_from_slice(&leaf2);
let page3_cells: Vec<(i64, Vec<u8>)> = (6..=10i64)
.map(|rowid| {
let val = (rowid * 10) as u8;
(rowid, vec![0x02u8, 0x01, val])
})
.collect();
let page3_refs: Vec<(i64, &[u8])> = page3_cells
.iter()
.map(|(r, p)| (*r, p.as_slice()))
.collect();
let leaf3 = build_leaf_page(page_size, &page3_refs, 0);
file_data[page_size * 2..page_size * 3].copy_from_slice(&leaf3);
let rows = scan_table(&file_data, 1, page_size).expect("scan");
assert_eq!(rows.len(), 10, "expected 10 rows across both leaves");
for expected_rowid in 1..=10i64 {
let row = rows
.iter()
.find(|(rid, _)| *rid == expected_rowid)
.unwrap_or_else(|| panic!("rowid {expected_rowid} missing"));
assert_eq!(
row.1[0],
CellValue::Integer(expected_rowid * 10),
"rowid {expected_rowid}: wrong value"
);
}
}
#[test]
fn scan_table_with_interior_node() {
let page_size = 4096;
let mut file_data = vec![0u8; page_size * 3];
file_data[..16].copy_from_slice(b"SQLite format 3\x00");
file_data[16..18].copy_from_slice(&(page_size as u16).to_be_bytes());
file_data[28..32].copy_from_slice(&3u32.to_be_bytes());
let hdr = 100;
file_data[hdr] = 5; file_data[hdr + 3] = 0;
file_data[hdr + 4] = 1;
file_data[hdr + 5] = ((4080u16) >> 8) as u8;
file_data[hdr + 6] = (4080u16 & 0xFF) as u8;
file_data[hdr + 8] = 0;
file_data[hdr + 9] = 0;
file_data[hdr + 10] = 0;
file_data[hdr + 11] = 3;
file_data[hdr + 12] = (4080u16 >> 8) as u8;
file_data[hdr + 13] = (4080u16 & 0xFF) as u8;
file_data[4080] = 0;
file_data[4081] = 0;
file_data[4082] = 0;
file_data[4083] = 2; file_data[4084] = 5;
let page2_base = page_size;
let payload1 = [0x02u8, 0x01, 0x64]; let leaf2 = build_leaf_page(page_size, &[(1, &payload1)], 0);
file_data[page2_base..page2_base + page_size].copy_from_slice(&leaf2);
let page3_base = page_size * 2;
let payload2 = [0x02u8, 0x02, 0x00, 0xC8];
let leaf3 = build_leaf_page(page_size, &[(10, &payload2)], 0);
file_data[page3_base..page3_base + page_size].copy_from_slice(&leaf3);
let rows = scan_table(&file_data, 1, page_size).expect("scan");
assert_eq!(rows.len(), 2);
let rowids: Vec<i64> = rows.iter().map(|r| r.0).collect();
assert!(rowids.contains(&1), "Should contain rowid 1");
assert!(rowids.contains(&10), "Should contain rowid 10");
for (rowid, values) in &rows {
match *rowid {
1 => assert_eq!(values[0], CellValue::Integer(100)),
10 => assert_eq!(values[0], CellValue::Integer(200)),
other => panic!("Unexpected rowid {other}"),
}
}
}
#[test]
fn scan_sqlite_master_basic() {
let page_size = 4096;
let entry_type = b"table";
let name = b"my_table";
let tbl_name = b"my_table";
let rootpage: i32 = 2;
let sql = b"CREATE TABLE my_table(id INTEGER)";
let header_len = 6u8;
let mut payload = Vec::new();
payload.push(header_len);
payload.push(0x17); payload.push(0x1D); payload.push(0x1D); payload.push(0x04); payload.push(0x4F);
payload.extend_from_slice(entry_type);
payload.extend_from_slice(name);
payload.extend_from_slice(tbl_name);
payload.extend_from_slice(&rootpage.to_be_bytes());
payload.extend_from_slice(sql);
let page = build_leaf_page(page_size, &[(1, &payload)], 100);
let mut file_data = page;
file_data[..16].copy_from_slice(b"SQLite format 3\x00");
file_data[16..18].copy_from_slice(&(page_size as u16).to_be_bytes());
file_data[28..32].copy_from_slice(&1u32.to_be_bytes());
let entries = scan_sqlite_master(&file_data, page_size).expect("scan");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].entry_type, "table");
assert_eq!(entries[0].name, "my_table");
assert_eq!(entries[0].tbl_name, "my_table");
assert_eq!(entries[0].rootpage, 2);
assert_eq!(entries[0].sql, "CREATE TABLE my_table(id INTEGER)");
}
#[test]
fn parse_record_negative_i8() {
let data = [0x02, 0x01, 0xFF];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(-1));
}
#[test]
fn parse_record_i24_negative() {
let data = [0x02, 0x03, 0xFF, 0xFF, 0xFF];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(-1));
}
#[test]
fn parse_record_i32() {
let data = [0x02, 0x04, 0x00, 0x01, 0x86, 0xA0];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(100_000));
}
#[test]
fn parse_record_i48() {
let data = [0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(1));
}
#[test]
fn parse_record_i48_negative() {
let data = [0x02, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(-1));
}
#[test]
fn parse_record_i64() {
let mut data = vec![0x02, 0x06];
data.extend_from_slice(&i64::MAX.to_be_bytes());
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(i64::MAX));
}
#[test]
fn parse_record_i64_negative() {
let mut data = vec![0x02, 0x06];
data.extend_from_slice(&i64::MIN.to_be_bytes());
let values = parse_record(&data).expect("parse");
assert_eq!(values[0], CellValue::Integer(i64::MIN));
}
}