//! HDF5 B-tree v2 parsing. #[cfg(not(feature = "std"))] use alloc::vec::Vec; use core::cmp::Ordering; #[cfg(feature = "checksum")] use byteorder::{ByteOrder, LittleEndian}; use crate::addr::to_usize; use crate::error::FormatError; use crate::storage::{Storage, Window, len_usize}; /// Parsed B-tree v2 header (signature "BTHD"). #[derive(Debug, Clone)] pub struct BTreeV2Header { /// B-tree type: 5=links indexed by name, 6=links indexed by creation order, etc. pub tree_type: u8, /// Node size in bytes. pub node_size: u32, /// Record size in bytes. pub record_size: u16, /// Depth of the tree (0 = root is a leaf). pub depth: u16, /// Address of root node. pub root_node_address: u64, /// Number of records in the root node. pub num_records_in_root: u16, /// Total number of records in all nodes. pub total_records: u64, } /// A single record from a B-tree v2 node. #[derive(Debug, Clone)] pub struct BTreeV2Record { /// Raw record bytes (record_size bytes). pub data: Vec, } fn read_offset(data: &[u8], pos: usize, size: u8) -> Result { let s = size as usize; if pos.checked_add(s).is_none_or(|end| end > data.len()) { return Err(FormatError::UnexpectedEof { expected: pos.saturating_add(s), available: data.len(), }); } Ok(match size { 2 => u16::from_le_bytes([data[pos], data[pos + 1]]) as u64, 4 => u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64, 8 => u64::from_le_bytes([ data[pos], data[pos + 1], data[pos + 2], data[pos + 3], data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7], ]), _ => return Err(FormatError::InvalidOffsetSize(size)), }) } fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError> { match pos.checked_add(needed) { Some(end) if end <= data.len() => Ok(()), _ => Err(FormatError::UnexpectedEof { expected: pos.saturating_add(needed), available: data.len(), }), } } /// Compute the number of bytes needed to represent a count, using variable-width encoding. /// B-tree v2 uses this for the number of records fields in internal nodes. pub(crate) fn bytes_for_max_records(max_nrec: u64) -> usize { if max_nrec == 0 { return 1; } let bits = 64 - max_nrec.leading_zeros() as usize; bits.div_ceil(8) } /// Read a variable-width unsigned integer (1-8 bytes, LE). fn read_var_uint(data: &[u8], pos: usize, width: usize) -> Result { ensure_len(data, pos, width)?; let mut val = 0u64; for i in 0..width { val |= (data[pos + i] as u64) << (i * 8); } Ok(val) } impl BTreeV2Header { /// Parse a B-tree v2 header at the given offset. pub fn parse( file_data: &[u8], offset: usize, offset_size: u8, length_size: u8, ) -> Result { Self::parse_in(file_data, offset as u64, offset_size, length_size) } /// [`Self::parse`] over any [`Storage`]: one bounded read of the /// header. pub fn parse_in( file: &S, offset: u64, offset_size: u8, length_size: u8, ) -> Result { // Every field and the checksum; the window holds all of it or ends // at the end of the file, so its bounds checks are the whole-file // ones. let full = 16 + usize::from(offset_size) + 2 + usize::from(length_size) + 4; let w = Window::read(file, offset, full)?; let d = &w.bytes; w.ensure(0, 4)?; if &d[..4] != b"BTHD" { return Err(FormatError::InvalidBTreeV2Signature); } w.ensure(0, 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1)?; let version = d[4]; if version != 0 { return Err(FormatError::InvalidBTreeV2Version(version)); } let tree_type = d[5]; let node_size = u32::from_le_bytes([d[6], d[7], d[8], d[9]]); let record_size = u16::from_le_bytes([d[10], d[11]]); let depth = u16::from_le_bytes([d[12], d[13]]); let _split_percent = d[14]; let _merge_percent = d[15]; let mut pos = 16; w.ensure(pos, usize::from(offset_size))?; let root_node_address = read_offset(d, pos, offset_size)?; pos += offset_size as usize; w.ensure(pos, 2)?; let num_records_in_root = u16::from_le_bytes([d[pos], d[pos + 1]]); pos += 2; w.ensure(pos, usize::from(length_size))?; let total_records = read_offset(d, pos, length_size)?; #[allow(unused_assignments)] { pos += length_size as usize; } // Validate header checksum #[cfg(feature = "checksum")] { w.ensure(pos, 4)?; let stored = LittleEndian::read_u32(&d[pos..pos + 4]); let computed = crate::checksum::jenkins_lookup3(&d[..pos]); if computed != stored { return Err(FormatError::ChecksumMismatch { expected: stored, computed, }); } } Ok(BTreeV2Header { tree_type, node_size, record_size, depth, root_node_address, num_records_in_root, total_records, }) } } /// Compute maximum records per node for a given depth level. /// leaf: (node_size - overhead) / record_size /// internal: depends on pointers pub(crate) fn max_records_leaf(node_size: u32, record_size: u16) -> u64 { // Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10 let overhead = 10u32; if node_size <= overhead || record_size == 0 { return 0; } ((node_size - overhead) / record_size as u32) as u64 } /// Deepest B-tree v2 accepted. See [`collect_btree_v2_records`]. const MAX_DEPTH: u16 = 64; /// Take `n` records from the traversal's budget, or refuse the tree. fn spend(budget: &mut usize, n: usize) -> Result<(), FormatError> { *budget = budget .checked_sub(n) .ok_or(FormatError::NestingDepthExceeded)?; Ok(()) } /// Collect all records from a B-tree v2 by traversing from the root. pub fn collect_btree_v2_records( file_data: &[u8], header: &BTreeV2Header, offset_size: u8, length_size: u8, ) -> Result, FormatError> { collect_btree_v2_records_in(file_data, header, offset_size, length_size) } /// [`collect_btree_v2_records`] over any [`Storage`]: one bounded read per /// node. pub fn collect_btree_v2_records_in( file: &S, header: &BTreeV2Header, offset_size: u8, length_size: u8, ) -> Result, FormatError> { if header.total_records == 0 || header.num_records_in_root == 0 { return Ok(Vec::new()); } // Recursion is one frame per level, and the depth is read from the file: // a crafted header claiming 65 535 levels over a node that is its own // child overflowed the stack. 64 matches the fractal heap's guard, and no // real tree comes close — even at the minimum fan-out of two it would // hold more than 2^64 records. if header.depth > MAX_DEPTH { return Err(FormatError::NestingDepthExceeded); } // A valid tree stores each record once, in its own bytes, so it cannot // hold more records than the file has room for. Children are addresses, // though, and nothing makes them distinct: levels whose children all // point at one shared node below reach it fan-out^depth times, which is // millions of records from a few kilobytes. Counting against what the // file could physically contain bounds that without trusting the // header's own `total_records`. let mut budget = len_usize(file) / usize::from(header.record_size.max(1)); let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size); if header.depth == 0 { // Root is a leaf parse_leaf_records( file, to_usize(header.root_node_address)?, header.num_records_in_root, header.record_size, header.node_size, ) } else { // Root is internal; traverse recursively let mut records = Vec::new(); collect_internal_records( file, to_usize(header.root_node_address)?, header.num_records_in_root, header.depth, header.record_size, header.node_size, offset_size, length_size, max_leaf_nrec, &mut budget, &mut records, )?; Ok(records) } } /// A node's bytes: `want` bytes at `offset` (fewer only at the end of the /// file), after checking its 4-byte signature. A node is read in one piece /// when it fits in `node_size` (every valid node does); a larger claimed /// extent — record counts from a damaged parent — is first checked against /// the end of the file, so it costs a read only of bytes the file has. /// Bounds errors are the whole-file ones: the signature check needs the /// first 6 bytes, then `checks` — `(position, length)` pairs relative to /// the node, in the order the parser checks them — must lie in the file. fn read_node<'a, S: Storage + ?Sized>( file: &'a S, offset: usize, want: usize, node_size: u32, signature: &[u8; 4], checks: &[(usize, usize)], ) -> Result, FormatError> { let one_read = usize::try_from(node_size).unwrap_or(usize::MAX).max(6); let w = Window::read(file, offset as u64, want.min(one_read))?; w.ensure(0, 6)?; if &w.bytes[..4] != signature { return Err(FormatError::InvalidBTreeV2Signature); } if want <= one_read { return Ok(w); } for &(rel, len) in checks { Window::check_extent(file, offset as u64, rel, len)?; } Window::read(file, offset as u64, want) } /// Parse records from a leaf node (signature "BTLF"). fn parse_leaf_records( file: &S, offset: usize, num_records: u16, record_size: u16, node_size: u32, ) -> Result, FormatError> { // signature(4) + version(1) + type(1) = 6 bytes header let pos = 6; let rs = record_size as usize; let total = (num_records as usize) .checked_mul(rs) .ok_or(FormatError::UnexpectedEof { expected: usize::MAX, available: len_usize(file), })?; let w = read_node( file, offset, pos + total + 4, node_size, b"BTLF", &[(pos, total)], )?; let d = &w.bytes; w.ensure(pos, total)?; // Validate checksum: 4 bytes after records + padding #[cfg(feature = "checksum")] { let checksum_pos = pos + total; if d.len() >= checksum_pos + 4 { let stored = LittleEndian::read_u32(&d[checksum_pos..checksum_pos + 4]); let computed = crate::checksum::jenkins_lookup3(&d[..checksum_pos]); if computed != stored { return Err(FormatError::ChecksumMismatch { expected: stored, computed, }); } } } let mut records = Vec::with_capacity(num_records as usize); for i in 0..num_records as usize { let start = pos + i * rs; records.push(BTreeV2Record { data: d[start..start + rs].to_vec(), }); } Ok(records) } /// An internal node read from the file: its bytes (from the signature on), /// where its records start, and its children as `(address, record count)`. struct InternalNode<'a> { node: Window<'a>, records_start: usize, children: Vec<(u64, u16)>, } impl InternalNode<'_> { /// Record `i`, `rs` bytes long. fn record(&self, i: usize, rs: usize) -> Result<&[u8], FormatError> { let overflow = || FormatError::UnexpectedEof { expected: usize::MAX, available: usize::MAX, }; let rec_start = i .checked_mul(rs) .and_then(|o| self.records_start.checked_add(o)) .ok_or_else(overflow)?; self.node.ensure(rec_start, rs)?; Ok(&self.node.bytes[rec_start..rec_start + rs]) } } /// An internal node's layout: where its records start, and its children as /// `(address, record count)`. #[allow(clippy::too_many_arguments)] fn read_internal_node( file: &S, offset: usize, num_records: u16, depth: u16, record_size: u16, node_size: u32, offset_size: u8, max_leaf_nrec: u64, ) -> Result, FormatError> { let nr = num_records as usize; let rs = record_size as usize; // Records first let records_total = nr.checked_mul(rs).ok_or(FormatError::UnexpectedEof { expected: usize::MAX, available: len_usize(file), })?; // Child pointer layout, as libhdf5 computes it (H5B2__hdr_init): the // child's record count is always encoded in the width needed for a // *leaf's* maximum, and — below the first internal level — the child // subtree's total record count in the width needed for the most records // a subtree of that depth can hold. let child_depth = depth - 1; let nrec_width = bytes_for_max_records(max_leaf_nrec); let total_nrec_width = if depth > 1 { bytes_for_max_records(cum_max_records( node_size, record_size, offset_size, max_leaf_nrec, child_depth, )) } else { 0 }; let num_children = nr + 1; let child_ptr_size = offset_size as usize + nrec_width + total_nrec_width; let pointers = num_children * child_ptr_size; // signature(4) + version(1) + type(1) = 6, records, pointers, checksum. let w = read_node( file, offset, 6 + records_total + pointers + 4, node_size, b"BTIN", &[(6, records_total), (6 + records_total, pointers)], )?; let d = &w.bytes; let mut pos = 6; w.ensure(pos, records_total)?; let records_start = pos; pos += records_total; w.ensure(pos, pointers)?; let mut children = Vec::with_capacity(num_children); for _ in 0..num_children { let addr = read_offset(d, pos, offset_size)?; pos += offset_size as usize; let child_nrec = read_var_uint(d, pos, nrec_width)? as u16; pos += nrec_width; pos += total_nrec_width; // skip total records in subtree children.push((addr, child_nrec)); } // The checksum follows the child pointers and covers the node up to it. // Lookups prune children by the keys in this node, so an unverified // internal node could hide a record without any error: libhdf5 refuses // a mismatch here, and so does this. #[cfg(feature = "checksum")] { w.ensure(pos, 4)?; let stored = LittleEndian::read_u32(&d[pos..pos + 4]); let computed = crate::checksum::jenkins_lookup3(&d[..pos]); if computed != stored { return Err(FormatError::ChecksumMismatch { expected: stored, computed, }); } } Ok(InternalNode { node: w, records_start, children, }) } /// Recursively collect records from an internal node. #[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)] fn collect_internal_records( file: &S, offset: usize, num_records: u16, depth: u16, record_size: u16, node_size: u32, offset_size: u8, length_size: u8, max_leaf_nrec: u64, budget: &mut usize, out: &mut Vec, ) -> Result<(), FormatError> { let nr = num_records as usize; let rs = record_size as usize; let node = read_internal_node( file, offset, num_records, depth, record_size, node_size, offset_size, max_leaf_nrec, )?; let child_depth = depth - 1; // Interleave: child[0], record[0], child[1], record[1], ..., child[nr] // We collect child[0] records, then record[0], then child[1], etc. // After the first child that fails, the others are only touched (see // `storage::touch`); that error is returned. let mut failed = None; for (i, &(child_addr, child_nrec)) in node.children.iter().enumerate() { if failed.is_some() { let len = usize::try_from(node_size) .unwrap_or(usize::MAX) .min(1 << 16); crate::storage::touch(file, child_addr, len); continue; } if let Err(e) = (|| -> Result<(), FormatError> { if child_depth == 0 { // Before parsing, so a refused tree is not also a large allocation. spend(budget, usize::from(child_nrec))?; let leaf_recs = parse_leaf_records( file, to_usize(child_addr)?, child_nrec, record_size, node_size, )?; out.extend(leaf_recs); } else { collect_internal_records( file, to_usize(child_addr)?, child_nrec, child_depth, record_size, node_size, offset_size, length_size, max_leaf_nrec, budget, out, )?; } // Add record[i] (except after the last child) if i < nr { let data = node.record(i, rs)?; spend(budget, 1)?; out.push(BTreeV2Record { data: data.to_vec(), }); } Ok(()) })() { failed = Some(e); } } match failed { Some(e) => Err(e), None => Ok(()), } } /// The records of a B-tree v2 that fall in one key range, found by /// descending the tree instead of reading all of it. /// /// `cmp` places a record relative to the range: `Less` if the record sorts /// before it, `Greater` if after, `Equal` if the record is in it. The tree /// must be ordered consistently with `cmp`, as libhdf5 orders it (a link or /// attribute name index by name hash, so all records with one hash form a /// range whatever order their names are in). Only the nodes whose key /// interval overlaps the range are read: O(depth) nodes plus those holding /// the matches. Matches come in tree order. pub fn find_btree_v2_records( file_data: &[u8], header: &BTreeV2Header, offset_size: u8, cmp: &mut dyn FnMut(&[u8]) -> Ordering, ) -> Result, FormatError> { find_btree_v2_records_in(file_data, header, offset_size, cmp) } /// [`find_btree_v2_records`] over any [`Storage`]: one bounded read per /// node visited. pub fn find_btree_v2_records_in( file: &S, header: &BTreeV2Header, offset_size: u8, cmp: &mut dyn FnMut(&[u8]) -> Ordering, ) -> Result, FormatError> { if header.total_records == 0 || header.num_records_in_root == 0 { return Ok(Vec::new()); } if header.depth > MAX_DEPTH { return Err(FormatError::NestingDepthExceeded); } // As in `collect_btree_v2_records`: a valid tree cannot hold more // records than the file has room for, however its children are shared. let mut budget = len_usize(file) / usize::from(header.record_size.max(1)); let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size); let mut out = Vec::new(); find_in_node( file, header, to_usize(header.root_node_address)?, header.num_records_in_root, header.depth, offset_size, max_leaf_nrec, cmp, &mut budget, &mut out, )?; Ok(out) } #[allow(clippy::too_many_arguments)] fn find_in_node( file: &S, header: &BTreeV2Header, offset: usize, num_records: u16, depth: u16, offset_size: u8, max_leaf_nrec: u64, cmp: &mut dyn FnMut(&[u8]) -> Ordering, budget: &mut usize, out: &mut Vec, ) -> Result<(), FormatError> { spend(budget, usize::from(num_records))?; if depth == 0 { let records = parse_leaf_records( file, offset, num_records, header.record_size, header.node_size, )?; out.extend( records .into_iter() .filter(|r| cmp(&r.data) == Ordering::Equal), ); return Ok(()); } let rs = usize::from(header.record_size); let node = read_internal_node( file, offset, num_records, depth, header.record_size, header.node_size, offset_size, max_leaf_nrec, )?; let nr = usize::from(num_records); let mut order = Vec::with_capacity(nr); for i in 0..nr { order.push(cmp(node.record(i, rs)?)); } // Child `i` holds the keys between record `i - 1` and record `i`: it can // hold a match unless the record before it is already past the range or // the record after it is still before it. for (i, &(child_addr, child_nrec)) in node.children.iter().enumerate() { let after_left = i == 0 || order[i - 1] != Ordering::Greater; let before_right = i == nr || order[i] != Ordering::Less; if after_left && before_right { find_in_node( file, header, to_usize(child_addr)?, child_nrec, depth - 1, offset_size, max_leaf_nrec, cmp, budget, out, )?; } if i < nr && order[i] == Ordering::Equal { out.push(BTreeV2Record { data: node.record(i, rs)?.to_vec(), }); } } Ok(()) } /// Most records a subtree whose root is at `depth` can hold (libhdf5's /// `cum_max_nrec`). See [`node_info`]. fn cum_max_records( node_size: u32, record_size: u16, offset_size: u8, max_leaf_nrec: u64, depth: u16, ) -> u64 { node_info_from_leaf(node_size, record_size, offset_size, max_leaf_nrec, depth) .last() .map_or(max_leaf_nrec, |n| n.cum_max_nrec) } /// Capacity of a B-tree v2 node at one depth, as libhdf5 computes it /// (`H5B2__hdr_init`'s `node_info`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct NodeInfo { /// Most records one node at this depth holds. pub(crate) max_nrec: u64, /// Most records a subtree rooted at this depth holds. pub(crate) cum_max_nrec: u64, /// Bytes a subtree's total record count takes in a pointer to a node /// at this depth (0 for a leaf, whose count is its own). pub(crate) cum_max_nrec_size: usize, } /// Node capacities for depths `0..=depth` (entry `d` for depth `d`): a leaf /// holds `max_nrec(0)` records; an internal node at depth `d` holds /// `max_nrec(d)` records and `max_nrec(d) + 1` subtrees of depth `d - 1`, /// where `max_nrec(d)` is what fits in a node once each record is paired /// with a child pointer of the width depth `d` needs (address, the child's /// record count in the width a *leaf's* maximum needs, and below the first /// internal level the child subtree's total in the width its maximum /// needs), with one pointer more than records. pub(crate) fn node_info( node_size: u32, record_size: u16, offset_size: u8, depth: u16, ) -> Vec { let max_leaf = max_records_leaf(node_size, record_size); node_info_from_leaf(node_size, record_size, offset_size, max_leaf, depth) } fn node_info_from_leaf( node_size: u32, record_size: u16, offset_size: u8, max_leaf_nrec: u64, depth: u16, ) -> Vec { // Internal node overhead: signature(4) + version(1) + type(1) + checksum(4). const PREFIX: u64 = 10; let nrec_width = bytes_for_max_records(max_leaf_nrec) as u64; let mut info = Vec::with_capacity(usize::from(depth) + 1); info.push(NodeInfo { max_nrec: max_leaf_nrec, cum_max_nrec: max_leaf_nrec, cum_max_nrec_size: 0, }); for d in 1..=depth { let below = info[usize::from(d) - 1]; let ptr = u64::from(offset_size) + nrec_width + if d > 1 { below.cum_max_nrec_size as u64 } else { 0 }; let max_nrec = u64::from(node_size) .saturating_sub(PREFIX) .saturating_sub(ptr) / (u64::from(record_size) + ptr).max(1); let cum = max_nrec .saturating_add(1) .saturating_mul(below.cum_max_nrec) .saturating_add(max_nrec); info.push(NodeInfo { max_nrec, cum_max_nrec: cum, cum_max_nrec_size: bytes_for_max_records(cum), }); } info } #[cfg(test)] mod tests { use super::*; #[allow(clippy::too_many_arguments)] fn build_btree_v2_header( tree_type: u8, node_size: u32, record_size: u16, depth: u16, root_addr: u64, num_records_root: u16, total_records: u64, offset_size: u8, length_size: u8, ) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(b"BTHD"); buf.push(0); // version buf.push(tree_type); buf.extend_from_slice(&node_size.to_le_bytes()); buf.extend_from_slice(&record_size.to_le_bytes()); buf.extend_from_slice(&depth.to_le_bytes()); buf.push(85); // split_percent buf.push(40); // merge_percent match offset_size { 4 => buf.extend_from_slice(&(root_addr as u32).to_le_bytes()), 8 => buf.extend_from_slice(&root_addr.to_le_bytes()), _ => {} } buf.extend_from_slice(&num_records_root.to_le_bytes()); match length_size { 4 => buf.extend_from_slice(&(total_records as u32).to_le_bytes()), 8 => buf.extend_from_slice(&total_records.to_le_bytes()), _ => {} } let checksum = crate::checksum::jenkins_lookup3(&buf); buf.extend_from_slice(&checksum.to_le_bytes()); buf } fn build_leaf_node(tree_type: u8, records: &[&[u8]]) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(b"BTLF"); buf.push(0); // version buf.push(tree_type); for rec in records { buf.extend_from_slice(rec); } let checksum = crate::checksum::jenkins_lookup3(&buf); buf.extend_from_slice(&checksum.to_le_bytes()); buf } /// An internal node laid out exactly as `collect_internal_records` will /// read it at `depth`: `records` zeroed records, then `children` pointers, /// all to `child_addr` claiming `child_nrec` records. fn internal_node( depth: u16, node_size: u32, record_size: u16, records: usize, children: usize, child_addr: u64, child_nrec: u64, ) -> Vec { let max_leaf = max_records_leaf(node_size, record_size); let nrec_width = bytes_for_max_records(max_leaf); let total_width = if depth > 1 { bytes_for_max_records(cum_max_records( node_size, record_size, 8, max_leaf, depth - 1, )) } else { 0 }; let mut buf = b"BTIN".to_vec(); buf.extend_from_slice(&[0, 5]); buf.resize(buf.len() + records * record_size as usize, 0); for _ in 0..children { buf.extend_from_slice(&child_addr.to_le_bytes()); buf.extend_from_slice(&child_nrec.to_le_bytes()[..nrec_width]); buf.resize(buf.len() + total_width, 0); } let sum = crate::checksum::jenkins_lookup3(&buf); buf.extend_from_slice(&sum.to_le_bytes()); buf } fn header(depth: u16, root: u64, root_nrec: u16, total: u64) -> BTreeV2Header { BTreeV2Header { tree_type: 5, node_size: 512, record_size: 8, depth, root_node_address: root, num_records_in_root: root_nrec, total_records: total, } } #[test] fn a_node_that_is_its_own_child_is_rejected_not_recursed() { // One internal node whose two children are itself, under a header // claiming the deepest tree a u16 allows. The layout stops depending // on depth once the subtree-total width saturates, so every level // parses cleanly and recursion runs ~65 000 frames deep: before the // cap this overflowed the stack and aborted the process, from a file // of under 100 bytes. let mut data = internal_node(u16::MAX, 512, 8, 1, 2, 0, 1); data.resize(4096, 0); let result = collect_btree_v2_records(&data, &header(u16::MAX, 0, 1, 1), 8, 8); assert!(result.is_err(), "{result:?}"); } #[test] fn a_shared_subtree_cannot_multiply_the_work() { // A chain of distinct levels, each node's children all pointing at the // single node below, ending in a real leaf. Every node parses and // nothing is cyclic, yet the leaf is reached fan-out^depth times: 62 // children over 4 levels is ~15 million leaf visits from a few // kilobytes. A valid tree cannot hold more records than the file has // room for, so that bounds the traversal instead. let (node_size, record_size) = (512u32, 8u16); let fanout = 62usize; let depth = 4u16; let leaf = build_leaf_node(5, &[&[0u8; 8][..]]); // Lay out root first, then each lower level, then the leaf. let mut nodes: Vec> = Vec::new(); let mut addrs = Vec::new(); let mut at = 0u64; let mut sizes = Vec::new(); for d in (1..=depth).rev() { let n = internal_node(d, node_size, record_size, fanout - 1, fanout, 0, 0); sizes.push(n.len()); } for size in &sizes { addrs.push(at); at += *size as u64; } let leaf_addr = at; for (i, d) in (1..=depth).rev().enumerate() { let (child, child_nrec) = if d == 1 { (leaf_addr, 1) } else { (addrs[i + 1], fanout as u64 - 1) }; nodes.push(internal_node( d, node_size, record_size, fanout - 1, fanout, child, child_nrec, )); } let mut data: Vec = nodes.concat(); data.extend_from_slice(&leaf); data.resize(data.len() + 64, 0); let started = std::time::Instant::now(); let result = collect_btree_v2_records(&data, &header(depth, 0, fanout as u16 - 1, u64::MAX), 8, 8); assert!( result.is_err(), "expected a refusal, got {} records", result.map_or(0, |r| r.len()) ); assert!( started.elapsed() < std::time::Duration::from_secs(2), "took {:?}", started.elapsed() ); } #[test] fn parse_header() { let data = build_btree_v2_header(5, 512, 11, 0, 0x1000, 3, 3, 8, 8); let hdr = BTreeV2Header::parse(&data, 0, 8, 8).unwrap(); assert_eq!(hdr.tree_type, 5); assert_eq!(hdr.node_size, 512); assert_eq!(hdr.record_size, 11); assert_eq!(hdr.depth, 0); assert_eq!(hdr.root_node_address, 0x1000); assert_eq!(hdr.num_records_in_root, 3); assert_eq!(hdr.total_records, 3); } #[test] fn parse_leaf_with_2_records() { let rec1 = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; let rec2 = [11u8, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; let leaf = build_leaf_node(5, &[&rec1, &rec2]); let leaf_offset = 256usize; let header = build_btree_v2_header(5, 512, 11, 0, leaf_offset as u64, 2, 2, 8, 8); let mut file_data = vec![0u8; 512]; file_data[..header.len()].copy_from_slice(&header); file_data[leaf_offset..leaf_offset + leaf.len()].copy_from_slice(&leaf); let hdr = BTreeV2Header::parse(&file_data, 0, 8, 8).unwrap(); let records = collect_btree_v2_records(&file_data, &hdr, 8, 8).unwrap(); assert_eq!(records.len(), 2); assert_eq!(records[0].data, rec1.to_vec()); assert_eq!(records[1].data, rec2.to_vec()); } #[test] fn invalid_signature() { let mut data = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8); data[0] = b'X'; let err = BTreeV2Header::parse(&data, 0, 8, 8).unwrap_err(); assert_eq!(err, FormatError::InvalidBTreeV2Signature); } #[test] fn invalid_version() { let mut data = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8); data[4] = 1; // bad version let err = BTreeV2Header::parse(&data, 0, 8, 8).unwrap_err(); assert_eq!(err, FormatError::InvalidBTreeV2Version(1)); } #[test] fn empty_tree() { let header = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8); let hdr = BTreeV2Header::parse(&header, 0, 8, 8).unwrap(); let records = collect_btree_v2_records(&header, &hdr, 8, 8).unwrap(); assert!(records.is_empty()); } #[test] fn subtree_capacity_matches_libhdf5() { // A link-name index (11-byte records, 512-byte nodes, 8-byte // addresses): libhdf5's H5B2__hdr_init gives 45 records per leaf, // then cum_max_nrec 1 149 at depth 1 and 26 449 at depth 2 — two // bytes of subtree count in a depth-3 root's child pointers, where // leaf_max^3 = 91 125 would need three. let leaf = max_records_leaf(512, 11); assert_eq!(leaf, 45); assert_eq!(cum_max_records(512, 11, 8, leaf, 0), 45); assert_eq!(cum_max_records(512, 11, 8, leaf, 1), 1_149); assert_eq!(cum_max_records(512, 11, 8, leaf, 2), 26_449); } }