fix: read multi-direct-block fractal heaps (root indirect block)

The fractal-heap reader split direct vs indirect block rows using the FRHP
"Starting # of Rows in Root Indirect Block" field (a constant, typically 1),
mislabeled as starting_row_of_indirect_blocks. For any heap whose data spans
more than one direct block — common in libhdf5 files with a large group or
many dense attributes — this treated direct blocks as indirect and walked into
garbage, failing with InvalidFractalHeapSignature.

Derive the split from the heap geometry instead: max_direct_rows =
log2(max_direct_block_size / starting_block_size) + 2. Rows below it hold
direct blocks; rows at/above hold child indirect blocks.

Validated against an h5py-written group with 400 dense attributes (root
indirect block, 4 rows, 13 direct blocks): all values now read correctly.
Regression fixture covers an 80-attribute multi-block heap.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
osobh
2026-06-04 01:27:45 +00:00
co-authored by Claude Opus 4.8
parent 20ad16ab69
commit 0aab49f2f0
3 changed files with 40 additions and 2 deletions
+22 -2
View File
@@ -379,8 +379,9 @@ impl FractalHeapHeader {
// Build table of (block_size, heap_offset) for each child entry
let mut current_heap_offset = iblock_heap_offset;
// Count direct block entries vs indirect block entries
let start_indirect = self.starting_row_of_indirect_blocks as usize;
// Rows below max_direct_rows hold direct blocks; rows at/above hold
// child indirect blocks. (NOT the FRHP "starting rows" field.)
let start_indirect = self.max_direct_rows();
// Read child addresses for direct block rows
let max_direct_rows = nrows_usize.min(start_indirect);
@@ -455,6 +456,25 @@ impl FractalHeapHeader {
})
}
/// Number of rows in the doubling table whose block size is at most the
/// maximum *direct* block size. Rows below this hold direct blocks; rows at
/// or above it hold child indirect blocks.
///
/// This is derived from the heap geometry, NOT the FRHP
/// "Starting # of Rows in Root Indirect Block" field (a constant, often 1)
/// — confusing the two makes a multi-direct-block heap unreadable.
fn max_direct_rows(&self) -> usize {
if self.starting_block_size == 0 {
return usize::MAX;
}
// Rows 0 and 1 share the starting block size; row r (r >= 1) is
// starting_block_size * 2^(r-1). The largest direct row reaches
// max_direct_block_size, giving log2(max/start) + 2 direct rows.
let ratio = (self.max_direct_block_size / self.starting_block_size).max(1);
let log2 = 63 - ratio.leading_zeros() as usize;
log2 + 2
}
/// Get block size for a given row in the doubling table.
fn block_size_for_row(&self, row: usize) -> u64 {
let sbs = self.starting_block_size;