Merge pull request 'docs(clawhdf5): document DType variants, fix unresolved doc links' (#17) from sdlc-docs/clawhdf5-types-20260514-165210 into main

This commit is contained in:
redclawsystems
2026-05-14 23:54:48 +00:00
commit 3f222f6956
3030 changed files with 89917 additions and 0 deletions
+524
View File
@@ -0,0 +1,524 @@
//! HDF5 B-tree v2 parsing.
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "checksum")]
use byteorder::{ByteOrder, LittleEndian};
use crate::error::FormatError;
/// 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<u8>,
}
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
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.
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<u64, FormatError> {
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<BTreeV2Header, FormatError> {
ensure_len(file_data, offset, 4)?;
if &file_data[offset..offset + 4] != b"BTHD" {
return Err(FormatError::InvalidBTreeV2Signature);
}
ensure_len(file_data, offset, 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1)?;
let version = file_data[offset + 4];
if version != 0 {
return Err(FormatError::InvalidBTreeV2Version(version));
}
let tree_type = file_data[offset + 5];
let node_size = u32::from_le_bytes([
file_data[offset + 6],
file_data[offset + 7],
file_data[offset + 8],
file_data[offset + 9],
]);
let record_size = u16::from_le_bytes([file_data[offset + 10], file_data[offset + 11]]);
let depth = u16::from_le_bytes([file_data[offset + 12], file_data[offset + 13]]);
let _split_percent = file_data[offset + 14];
let _merge_percent = file_data[offset + 15];
let mut pos = offset + 16;
let root_node_address = read_offset(file_data, pos, offset_size)?;
pos += offset_size as usize;
ensure_len(file_data, pos, 2)?;
let num_records_in_root = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
let total_records = read_offset(file_data, pos, length_size)?;
#[allow(unused_assignments)]
{
pos += length_size as usize;
}
// Validate header checksum
#[cfg(feature = "checksum")]
{
ensure_len(file_data, pos, 4)?;
let stored = LittleEndian::read_u32(&file_data[pos..pos + 4]);
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..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
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
}
/// 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<Vec<BTreeV2Record>, FormatError> {
if header.total_records == 0 || header.num_records_in_root == 0 {
return Ok(Vec::new());
}
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_data,
header.root_node_address as usize,
header.num_records_in_root,
header.record_size,
)
} else {
// Root is internal; traverse recursively
let mut records = Vec::new();
collect_internal_records(
file_data,
header.root_node_address as usize,
header.num_records_in_root,
header.depth,
header.record_size,
header.node_size,
offset_size,
length_size,
max_leaf_nrec,
&mut records,
)?;
Ok(records)
}
}
/// Parse records from a leaf node (signature "BTLF").
fn parse_leaf_records(
file_data: &[u8],
offset: usize,
num_records: u16,
record_size: u16,
) -> Result<Vec<BTreeV2Record>, FormatError> {
// signature(4) + version(1) + type(1) = 6 bytes header
ensure_len(file_data, offset, 6)?;
if &file_data[offset..offset + 4] != b"BTLF" {
return Err(FormatError::InvalidBTreeV2Signature);
}
let pos = offset + 6;
let rs = record_size as usize;
let total = (num_records as usize)
.checked_mul(rs)
.ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: file_data.len(),
})?;
ensure_len(file_data, pos, total)?;
// Validate checksum: 4 bytes after records + padding
#[cfg(feature = "checksum")]
{
let checksum_pos = pos + total;
if file_data.len() >= checksum_pos + 4 {
let stored = LittleEndian::read_u32(&file_data[checksum_pos..checksum_pos + 4]);
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..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: file_data[start..start + rs].to_vec(),
});
}
Ok(records)
}
/// Recursively collect records from an internal node.
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
fn collect_internal_records(
file_data: &[u8],
offset: usize,
num_records: u16,
depth: u16,
record_size: u16,
node_size: u32,
offset_size: u8,
length_size: u8,
max_leaf_nrec: u64,
out: &mut Vec<BTreeV2Record>,
) -> Result<(), FormatError> {
// signature(4) + version(1) + type(1) = 6
ensure_len(file_data, offset, 6)?;
if &file_data[offset..offset + 4] != b"BTIN" {
return Err(FormatError::InvalidBTreeV2Signature);
}
let nr = num_records as usize;
let rs = record_size as usize;
let mut pos = offset + 6;
// Read all records first
let records_total = nr.checked_mul(rs).ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: file_data.len(),
})?;
ensure_len(file_data, pos, records_total)?;
let records_start = pos;
pos += records_total;
// Compute sizes for child pointers
// max_records at child depth - for variable-width nrec encoding
let child_depth = depth - 1;
let max_nrec_child = if child_depth == 0 {
max_leaf_nrec
} else {
// For internal nodes at child_depth, the true max_nrec depends on the
// node size, record size, and the recursive width of child pointer
// entries (which themselves depend on max_nrec at deeper levels).
// Computing the exact value requires iterating from the leaf level
// upward, as described in the HDF5 spec (III.A.2 "Computing the Size
// of B-tree Nodes").
//
// We use `max_leaf_nrec * 2` as a conservative upper bound. This
// over-estimates the nrec encoding width, which means we may read
// slightly more bytes per child pointer than strictly necessary, but
// never fewer. The over-read bytes are harmless because we only
// decode `num_records` entries (the actual count from the node header).
//
// Known limitation: for very deep trees (depth > 3) with small record
// sizes, the true max could exceed this estimate, causing us to
// under-allocate the nrec encoding width and misparse child pointers.
// In practice, HDF5 B-tree v2 depths rarely exceed 2-3.
max_leaf_nrec * 2
};
let nrec_width = bytes_for_max_records(max_nrec_child);
// Total records in subtree width (only if depth > 1)
let total_nrec_width = if depth > 1 {
// Width to hold total records in a subtree
// We compute max possible total records at this subtree depth
let max_total = header_max_total_records(max_leaf_nrec, depth - 1);
bytes_for_max_records(max_total)
} else {
0
};
let num_children = nr + 1;
let child_ptr_size = offset_size as usize + nrec_width + total_nrec_width;
ensure_len(file_data, pos, num_children * child_ptr_size)?;
// Read child pointers
let mut children = Vec::with_capacity(num_children);
for _ in 0..num_children {
let addr = read_offset(file_data, pos, offset_size)?;
pos += offset_size as usize;
let child_nrec = read_var_uint(file_data, pos, nrec_width)? as u16;
pos += nrec_width;
pos += total_nrec_width; // skip total records in subtree
children.push((addr, child_nrec));
}
// Interleave: child[0], record[0], child[1], record[1], ..., child[nr]
// We collect child[0] records, then record[0], then child[1], etc.
for (i, &(child_addr, child_nrec)) in children.iter().enumerate() {
if child_depth == 0 {
let leaf_recs =
parse_leaf_records(file_data, child_addr as usize, child_nrec, record_size)?;
out.extend(leaf_recs);
} else {
collect_internal_records(
file_data,
child_addr as usize,
child_nrec,
child_depth,
record_size,
node_size,
offset_size,
length_size,
max_leaf_nrec,
out,
)?;
}
// Add record[i] (except after the last child)
if i < nr {
let rec_offset = i.checked_mul(rs).ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: file_data.len(),
})?;
let rec_start =
records_start
.checked_add(rec_offset)
.ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: file_data.len(),
})?;
let rec_end = rec_start
.checked_add(rs)
.ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: file_data.len(),
})?;
if rec_end > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: rec_end,
available: file_data.len(),
});
}
out.push(BTreeV2Record {
data: file_data[rec_start..rec_end].to_vec(),
});
}
}
Ok(())
}
/// Estimate maximum total records at a given depth (for variable-width encoding).
fn header_max_total_records(max_leaf_nrec: u64, depth: u16) -> u64 {
// Conservative: branching factor * max_leaf at each level
let mut total = max_leaf_nrec;
for _ in 0..depth {
total = total.saturating_mul(max_leaf_nrec.max(2));
}
total
}
#[cfg(test)]
mod tests {
use super::*;
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<u8> {
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<u8> {
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
}
#[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());
}
}