use crate::{
error::Error,
storage::sstable::bti::node::{BtiNode, BtiNodeData, BtiResult},
};
use std::io::{Read, Seek, SeekFrom};
use super::partitions::{parse_bti_node_for_traversal, read_node_payload, BtiPartitionLocation};
pub(crate) fn load_bti_trie_via_footer<R: Read + Seek>(
reader: &mut R,
) -> BtiResult<(Vec<u8>, usize)> {
let file_size = reader.seek(SeekFrom::End(0))?;
if file_size < 8 {
return Err(Error::Parse(format!(
"BTI file too small ({file_size} bytes; need at least 8 for footer)"
)));
}
reader.seek(SeekFrom::End(-8))?;
let mut footer = [0u8; 8];
reader.read_exact(&mut footer)?;
let root_offset = u64::from_be_bytes(footer);
let trie_size = file_size - 8;
if root_offset >= trie_size {
return Err(Error::Parse(format!(
"BTI file: root_offset {root_offset} >= trie_size {trie_size}"
)));
}
reader.seek(SeekFrom::Start(0))?;
let mut trie_data = vec![0u8; trie_size as usize];
reader.read_exact(&mut trie_data)?;
Ok((trie_data, root_offset as usize))
}
fn ordered_children(node: &BtiNode) -> Vec<(u8, usize)> {
match &node.data {
BtiNodeData::PayloadOnly { .. } => Vec::new(),
BtiNodeData::Single { transition } => {
vec![(transition.byte, transition.child.distance as usize)]
}
BtiNodeData::Sparse { transitions } => {
let mut out: Vec<(u8, usize)> = transitions
.iter()
.map(|t| (t.byte, t.child.distance as usize))
.collect();
out.sort_by_key(|&(b, _)| b);
out
}
BtiNodeData::Dense {
start_byte,
children,
} => {
let mut out = Vec::new();
for (i, child) in children.iter().enumerate() {
if let Some(ptr) = child {
let transition_byte = start_byte.wrapping_add(i as u8);
out.push((transition_byte, ptr.distance as usize));
}
}
out
}
}
}
pub(crate) fn dfs_collect_in_order<T, F>(
trie_data: &[u8],
root_offset: usize,
mut decode_payload: F,
) -> BtiResult<Vec<(Vec<u8>, T)>>
where
F: FnMut(&[u8], usize) -> BtiResult<Option<T>>,
{
enum DfsOp {
Enter {
node_offset: usize,
transition: Option<u8>,
depth: usize,
},
Pop,
}
let mut results: Vec<(Vec<u8>, T)> = Vec::new();
let mut path: Vec<u8> = Vec::new();
let mut stack: Vec<DfsOp> = vec![DfsOp::Enter {
node_offset: root_offset,
transition: None,
depth: 1,
}];
let mut nodes_visited: usize = 0;
let mut visited = vec![0u8; trie_data.len().div_ceil(8)];
while let Some(op) = stack.pop() {
let DfsOp::Enter {
node_offset,
transition,
depth: node_depth,
} = op
else {
let _ = path.pop();
continue;
};
if let Some(b) = transition {
path.push(b);
stack.push(DfsOp::Pop);
}
nodes_visited = nodes_visited.saturating_add(1);
if nodes_visited > trie_data.len() {
return Err(Error::Parse(format!(
"BTI DFS exceeded total work bound ({nodes_visited} nodes visited > \
trie size {}; corrupt or cyclic trie)",
trie_data.len()
)));
}
if path.len() > trie_data.len() {
return Err(Error::Parse(format!(
"BTI DFS key path exceeds trie size {} (corrupt or cyclic trie)",
trie_data.len()
)));
}
if node_depth > trie_data.len().saturating_add(1) {
return Err(Error::Parse(format!(
"BTI DFS exceeded node depth (path of {node_depth} nodes > trie size {} + 1; \
corrupt or cyclic trie)",
trie_data.len()
)));
}
if node_offset >= trie_data.len() {
return Err(Error::Parse(format!(
"BTI DFS: node_offset {node_offset} out of bounds (trie size {})",
trie_data.len()
)));
}
let word = node_offset >> 3;
let bit = 1u8 << (node_offset & 7);
if visited[word] & bit != 0 {
return Err(Error::Parse(format!(
"BTI DFS revisited node offset {node_offset} (corrupt or cyclic trie)"
)));
}
visited[word] |= bit;
if let Some(payload) = decode_payload(trie_data, node_offset)? {
results.push((path.clone(), payload));
}
let node = parse_bti_node_for_traversal(trie_data, node_offset)?;
let children = ordered_children(&node);
let child_depth = node_depth.saturating_add(1);
for &(transition_byte, child_offset) in children.iter().rev() {
stack.push(DfsOp::Enter {
node_offset: child_offset,
transition: Some(transition_byte),
depth: child_depth,
});
}
}
Ok(results)
}
pub(crate) fn dfs_collect_partition_entries(
trie_data: &[u8],
root_offset: usize,
) -> BtiResult<Vec<(Vec<u8>, BtiPartitionLocation)>> {
dfs_collect_in_order(trie_data, root_offset, |data, off| {
read_node_payload(data, off)
})
}
pub fn iterate_partitions_in_bti_file<R: Read + Seek>(
reader: &mut R,
) -> BtiResult<Vec<(Vec<u8>, BtiPartitionLocation)>> {
let file_size = reader.seek(SeekFrom::End(0))?;
if file_size < 8 {
return Ok(Vec::new());
}
let (trie_data, root_offset) = load_bti_trie_via_footer(reader)?;
dfs_collect_partition_entries(&trie_data, root_offset)
}
#[cfg(test)]
mod tests {
use super::super::node_decode::parse_bti_node;
use super::*;
use std::io::Cursor;
fn make_partitions_db(trie_bytes: Vec<u8>, root_offset: u64) -> Vec<u8> {
let mut v = trie_bytes;
v.extend_from_slice(&root_offset.to_be_bytes());
v
}
fn partition_leaf(hash: u8, position: i8) -> Vec<u8> {
vec![0x08, hash, position as u8]
}
fn dense16_node(payload_flags: u8, start: u8, deltas: &[u16]) -> Vec<u8> {
let len = deltas.len() as u8;
let mut v = vec![0xB0 | (payload_flags & 0x0F), start, len - 1];
for &d in deltas {
v.extend_from_slice(&d.to_be_bytes());
}
v
}
#[test]
fn dfs_partition_sparse_ascending_order_with_offsets() {
let mut trie = vec![0u8; 12];
trie[0..3].copy_from_slice(&partition_leaf(0x11, -1));
trie[3..6].copy_from_slice(&partition_leaf(0x22, -65));
trie[6] = 0x50; trie[7] = 0x02; trie[8] = 0xAA;
trie[9] = 0xBB;
trie[10] = 0x06; trie[11] = 0x03;
let entries = dfs_collect_partition_entries(&trie, 6).unwrap();
assert_eq!(
entries,
vec![
(vec![0xAA], BtiPartitionLocation::DataOffset(0)),
(vec![0xBB], BtiPartitionLocation::DataOffset(64)),
],
"Sparse DFS must emit ascending transition bytes with correct offsets"
);
}
#[test]
fn dfs_partition_dense_skips_gaps() {
let mut trie = vec![0x00u8]; let l1 = trie.len() as u64; trie.extend_from_slice(&partition_leaf(0x11, -1)); let l2 = trie.len() as u64; trie.extend_from_slice(&partition_leaf(0x22, -65)); let dense_off = trie.len() as u64; trie.push(0xB0); trie.push(0x10); trie.push(0x02); trie.extend_from_slice(&((dense_off - l1) as u16).to_be_bytes()); trie.extend_from_slice(&0u16.to_be_bytes()); trie.extend_from_slice(&((dense_off - l2) as u16).to_be_bytes());
let entries = dfs_collect_partition_entries(&trie, dense_off as usize).unwrap();
assert_eq!(
entries,
vec![
(vec![0x10], BtiPartitionLocation::DataOffset(0)),
(vec![0x12], BtiPartitionLocation::DataOffset(64)),
],
"Dense DFS must skip distance==0 gaps and emit start_byte+i order"
);
}
#[test]
fn dfs_partition_internal_payload_before_children() {
let mut trie = Vec::new();
trie.extend_from_slice(&partition_leaf(0x11, -1)); let node_off = trie.len() as u64; trie.push(0x28); trie.push(0xCC); trie.push(node_off as u8); trie.push(0x99); trie.push((-65i8) as u8);
let entries = dfs_collect_partition_entries(&trie, node_off as usize).unwrap();
assert_eq!(
entries,
vec![
(vec![], BtiPartitionLocation::DataOffset(64)),
(vec![0xCC], BtiPartitionLocation::DataOffset(0)),
],
"An internal node's payload must be emitted before its children"
);
}
#[test]
fn partition_iterator_full_traversal_synthetic() {
let mut trie = vec![0u8; 12];
trie[0..3].copy_from_slice(&partition_leaf(0x11, -1));
trie[3..6].copy_from_slice(&partition_leaf(0x22, -65));
trie[6] = 0x50;
trie[7] = 0x02;
trie[8] = 0xAA;
trie[9] = 0xBB;
trie[10] = 0x06;
trie[11] = 0x03;
let file = make_partitions_db(trie, 6);
let (trie_data, root) = load_bti_trie_via_footer(&mut Cursor::new(file)).unwrap();
assert_eq!(root, 6);
let entries = dfs_collect_partition_entries(&trie_data, root).unwrap();
assert_eq!(
entries,
vec![
(vec![0xAA], BtiPartitionLocation::DataOffset(0)),
(vec![0xBB], BtiPartitionLocation::DataOffset(64)),
]
);
}
fn partition_chain_to_leaf(n_links: usize) -> (Vec<u8>, usize) {
let mut trie = Vec::new();
trie.extend_from_slice(&partition_leaf(0x11, -1)); let mut child_off = 0usize;
for i in 0..n_links {
let node_off = trie.len();
let delta = node_off - child_off;
assert!(
delta <= 0x0F,
"chain link delta {delta} does not fit a SingleNoPayload4 nibble"
);
trie.push(0x10 | (delta as u8 & 0x0F));
trie.push((i % 255) as u8 + 1); child_off = node_off;
}
let root = trie.len() - 2; (trie, root)
}
#[test]
fn dfs_long_partition_key_over_128_bytes_decodes() {
let (trie, root) = partition_chain_to_leaf(200);
let entries =
dfs_collect_partition_entries(&trie, root).expect("a ~200-byte key path must decode");
assert_eq!(entries.len(), 1, "the single leaf must be emitted");
assert_eq!(
entries[0].0.len(),
200,
"reconstructed key path is 200 bytes (one per transition)"
);
assert_eq!(entries[0].1, BtiPartitionLocation::DataOffset(0));
}
#[test]
fn dfs_chain_just_over_old_128_cap_decodes() {
let (trie, root) = partition_chain_to_leaf(129);
let entries = dfs_collect_partition_entries(&trie, root)
.expect("a 129-byte key path must decode (old cap was 128)");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0.len(), 129);
}
#[test]
fn dfs_long_encoded_path_over_u16_max_decodes() {
let (trie, root) = partition_chain_to_leaf(70_000);
assert!(
70_000 > u16::MAX as usize,
"the path must exceed the removed u16::MAX cap to be a regression"
);
let entries = dfs_collect_partition_entries(&trie, root)
.expect("a >65535-byte encoded key path must decode (no fixed cap)");
assert_eq!(entries.len(), 1, "the single leaf must be emitted");
assert_eq!(
entries[0].0.len(),
70_000,
"reconstructed key path is 70000 bytes (one per transition)"
);
assert_eq!(entries[0].1, BtiPartitionLocation::DataOffset(0));
}
#[test]
fn dfs_reconverging_trie_hits_work_bound_no_hang() {
let mut trie = Vec::new();
trie.extend_from_slice(&partition_leaf(0x11, -1)); let mut child_off = 0usize;
let levels = 30;
for _ in 0..levels {
let node_off = trie.len();
let delta = (node_off - child_off) as u8; trie.push(0x50); trie.push(0x02); trie.push(0x01); trie.push(0x02); trie.push(delta); trie.push(delta); child_off = node_off;
}
let root = trie.len() - 6;
let err = dfs_collect_partition_entries(&trie, root)
.expect_err("a reconverging trie must error, not hang");
let msg = format!("{err}");
assert!(
msg.contains("revisited node offset") || msg.contains("total work bound"),
"expected the revisit/total-work error, got: {msg}"
);
}
#[test]
fn dfs_cyclic_trie_errors_with_trie_relative_message() {
let trie = vec![0x10u8, 0x01u8];
let err = dfs_collect_partition_entries(&trie, 0)
.expect_err("a cyclic (self-looping) trie must error, not hang");
let msg = format!("{err}");
assert!(
msg.contains("revisited node offset")
|| msg.contains("total work bound")
|| msg.contains("key path exceeds trie size")
|| msg.contains("node depth"),
"expected a trie-size-relative error, got: {msg}"
);
}
#[test]
fn dfs_padded_reconvergence_rejected_before_stack_blowup() {
let fan_out: usize = 200;
let mut trie = vec![0x50u8, fan_out as u8]; trie.extend((1..=fan_out).map(|b| b as u8)); trie.resize(trie.len() + fan_out, 0); let live_len = trie.len();
trie.resize(live_len + 100_000, 0);
assert!(
trie.len() > live_len * 100,
"padded trie must dwarf the live node so total-work alone would not save us"
);
let err = dfs_collect_partition_entries(&trie, 0)
.expect_err("a padded high-fan-out self-loop must error, not blow up the stack");
let msg = format!("{err}");
assert!(
msg.contains("revisited node offset"),
"the visited guard must fire first (before total work), got: {msg}"
);
}
#[test]
fn dense_find_child_offset_zero_and_gap() {
let mut trie = vec![0x01u8, 5, 0x01u8, 9];
let root = trie.len() as u64; let deltas = [root as u16, 0x0000, (root - 2) as u16];
trie.extend(dense16_node(0, 0x10, &deltas));
let node = parse_bti_node(&trie[root as usize..], root).unwrap();
let c10 = node.find_child(0x10).expect("0x10 child must be found");
assert_eq!(c10.distance, 0, "0x10 must route to absolute offset 0");
assert!(
node.find_child(0x11).is_none(),
"0x11 is the no-transition gap"
);
assert!(node.find_child(0x12).is_some());
}
}