Skip to main content

hdf5_reader/
btree_v1.rs

1//! HDF5 B-link Tree Version 1.
2//!
3//! V1 B-trees are used by old-style (v0/v1) groups for link name lookup
4//! (type 0) and by chunked datasets for raw data chunk indexing (type 1).
5//!
6//! The tree is a B-link tree: each node carries left and right sibling
7//! pointers. Keys bracket children — a node with N children has N+1 keys.
8//! Leaf nodes (level 0) point to either symbol table nodes (SNODs) for
9//! type 0 or raw data chunk addresses for type 1. Internal nodes point to
10//! child B-tree nodes.
11
12use crate::error::{Error, Result};
13use crate::io::Cursor;
14use crate::storage::Storage;
15
16/// Signature bytes for a v1 B-tree node: ASCII `TREE`.
17const BTREE_V1_SIGNATURE: [u8; 4] = *b"TREE";
18
19/// A key within a v1 B-tree node.
20#[derive(Debug, Clone)]
21pub enum BTreeV1Key {
22    /// Type 0 (group) key: offset into the local heap for the link name.
23    Group { local_heap_offset: u64 },
24    /// Type 1 (raw data chunk) key: chunk size, filter mask, and per-dimension
25    /// offsets (including one extra for the dataset element offset).
26    RawData {
27        chunk_size: u32,
28        filter_mask: u32,
29        offsets: Vec<u64>,
30    },
31}
32
33/// A parsed v1 B-tree node.
34#[derive(Debug, Clone)]
35pub struct BTreeV1Node {
36    /// Node type: 0 = group, 1 = raw data chunk.
37    pub node_type: u8,
38    /// Node level: 0 = leaf, > 0 = internal.
39    pub level: u8,
40    /// Number of entries (children) actually in use.
41    pub entries_used: u16,
42    /// Address of the left sibling node (undefined if none).
43    pub left_sibling: u64,
44    /// Address of the right sibling node (undefined if none).
45    pub right_sibling: u64,
46    /// Keys bracketing the children. Length is `entries_used + 1`.
47    pub keys: Vec<BTreeV1Key>,
48    /// Child addresses. Length is `entries_used`.
49    pub children: Vec<u64>,
50}
51
52impl BTreeV1Node {
53    /// Parse a v1 B-tree node at the current cursor position.
54    ///
55    /// `ndims` is required for type-1 (raw data chunk) nodes — it is the
56    /// number of dimensions of the dataset's dataspace. For type-0 (group)
57    /// nodes pass `None`.
58    ///
59    /// Format:
60    /// - Signature: `TREE` (4 bytes)
61    /// - Node type (u8): 0 = group, 1 = raw data
62    /// - Node level (u8)
63    /// - Entries used (u16 LE)
64    /// - Left sibling address (`offset_size` bytes)
65    /// - Right sibling address (`offset_size` bytes)
66    /// - Then interleaved keys and child pointers:
67    ///   key[0], child[0], key[1], child[1], ..., key[K-1], child[K-1], key[K]
68    ///   where K = entries_used.
69    pub fn parse(
70        cursor: &mut Cursor,
71        offset_size: u8,
72        length_size: u8,
73        ndims: Option<u32>,
74    ) -> Result<Self> {
75        let sig = cursor.read_bytes(4)?;
76        if sig != BTREE_V1_SIGNATURE {
77            return Err(Error::InvalidBTreeSignature);
78        }
79
80        let node_type = cursor.read_u8()?;
81        let level = cursor.read_u8()?;
82        let entries_used = cursor.read_u16_le()?;
83        let left_sibling = cursor.read_offset(offset_size)?;
84        let right_sibling = cursor.read_offset(offset_size)?;
85
86        let num_keys = entries_used as usize + 1;
87        let num_children = entries_used as usize;
88
89        let mut keys = Vec::with_capacity(num_keys);
90        let mut children = Vec::with_capacity(num_children);
91
92        // Read interleaved keys and children:
93        // key[0], child[0], key[1], child[1], ..., key[K-1], child[K-1], key[K]
94        for i in 0..num_keys {
95            let key = match node_type {
96                0 => parse_group_key(cursor, length_size)?,
97                1 => parse_raw_data_key(cursor, ndims)?,
98                _ => {
99                    return Err(Error::InvalidData(format!(
100                        "unknown v1 B-tree node type: {node_type}"
101                    )));
102                }
103            };
104            keys.push(key);
105
106            // Read child address after every key except the last.
107            if i < num_children {
108                let child_addr = cursor.read_offset(offset_size)?;
109                children.push(child_addr);
110            }
111        }
112
113        Ok(BTreeV1Node {
114            node_type,
115            level,
116            entries_used,
117            left_sibling,
118            right_sibling,
119            keys,
120            children,
121        })
122    }
123}
124
125/// Parse a type-0 (group) key: just a length-sized offset into the local heap.
126fn parse_group_key(cursor: &mut Cursor, length_size: u8) -> Result<BTreeV1Key> {
127    let local_heap_offset = cursor.read_length(length_size)?;
128    Ok(BTreeV1Key::Group { local_heap_offset })
129}
130
131/// Parse a type-1 (raw data chunk) key.
132///
133/// Format:
134/// - chunk_size (u32 LE) — size of the chunk in bytes after filters
135/// - filter_mask (u32 LE) — bit mask of filters to skip
136/// - (ndims + 1) chunk-offset values per dimension (plus the trailing element
137///   offset, typically 0)
138///
139/// Each chunk-offset value is **always 8 bytes**, independent of the
140/// superblock's "size of offsets" field. The HDF5 format spec fixes these key
141/// dimension offsets at 64 bits (see the "Version 1 B-trees" / raw-data chunk
142/// key layout); reading them as `size_of_offsets` bytes corrupts the cursor
143/// position on 32-bit-superblock files (`size_of_offsets = 4`), yielding a
144/// garbage chunk address and a failed chunk decode.
145fn parse_raw_data_key(cursor: &mut Cursor, ndims: Option<u32>) -> Result<BTreeV1Key> {
146    let nd = ndims.ok_or_else(|| {
147        Error::InvalidData("ndims required for raw data chunk B-tree keys".into())
148    })?;
149
150    let chunk_size = cursor.read_u32_le()?;
151    let filter_mask = cursor.read_u32_le()?;
152
153    let num_offsets = nd as usize + 1;
154    let mut offsets = Vec::with_capacity(num_offsets);
155    for _ in 0..num_offsets {
156        // Fixed 64-bit dimension offset per the HDF5 spec — NOT size_of_offsets.
157        offsets.push(cursor.read_u64_le()?);
158    }
159
160    Ok(BTreeV1Key::RawData {
161        chunk_size,
162        filter_mask,
163        offsets,
164    })
165}
166
167/// Walk a v1 B-tree and collect all (key, child_address) pairs from leaf nodes.
168///
169/// For leaf nodes (level 0), this returns one entry per child paired with the
170/// key that precedes it (key[i] for child[i]). For internal nodes, this
171/// recurses into each child node.
172///
173/// `data` must be the full file data (or at least the region containing the
174/// B-tree nodes). `root_address` is the file offset of the root node.
175pub fn collect_btree_v1_leaves(
176    data: &[u8],
177    root_address: u64,
178    offset_size: u8,
179    length_size: u8,
180    ndims: Option<u32>,
181    chunk_dims: &[u32],
182    chunk_bounds: Option<(&[u64], &[u64])>,
183) -> Result<Vec<(BTreeV1Key, u64)>> {
184    let mut results = Vec::new();
185    collect_recursive(
186        BTreeV1CollectContext {
187            data,
188            offset_size,
189            length_size,
190            ndims,
191            chunk_dims,
192            chunk_bounds,
193        },
194        root_address,
195        &mut results,
196    )?;
197    Ok(results)
198}
199
200/// Walk a v1 B-tree from random-access storage and collect leaf entries.
201pub fn collect_btree_v1_leaves_storage(
202    storage: &dyn Storage,
203    root_address: u64,
204    offset_size: u8,
205    length_size: u8,
206    ndims: Option<u32>,
207    chunk_dims: &[u32],
208    chunk_bounds: Option<(&[u64], &[u64])>,
209) -> Result<Vec<(BTreeV1Key, u64)>> {
210    let mut results = Vec::new();
211    collect_recursive_storage(
212        BTreeV1CollectContextStorage {
213            storage,
214            offset_size,
215            length_size,
216            ndims,
217            chunk_dims,
218            chunk_bounds,
219        },
220        root_address,
221        &mut results,
222    )?;
223    Ok(results)
224}
225
226#[derive(Clone, Copy)]
227struct BTreeV1CollectContext<'a> {
228    data: &'a [u8],
229    offset_size: u8,
230    length_size: u8,
231    ndims: Option<u32>,
232    chunk_dims: &'a [u32],
233    chunk_bounds: Option<(&'a [u64], &'a [u64])>,
234}
235
236#[derive(Clone, Copy)]
237struct BTreeV1CollectContextStorage<'a> {
238    storage: &'a dyn Storage,
239    offset_size: u8,
240    length_size: u8,
241    ndims: Option<u32>,
242    chunk_dims: &'a [u32],
243    chunk_bounds: Option<(&'a [u64], &'a [u64])>,
244}
245
246/// Recursive helper for tree traversal.
247fn collect_recursive(
248    context: BTreeV1CollectContext<'_>,
249    address: u64,
250    results: &mut Vec<(BTreeV1Key, u64)>,
251) -> Result<()> {
252    if Cursor::is_undefined_offset(address, context.offset_size) {
253        return Ok(());
254    }
255
256    if address as usize >= context.data.len() {
257        return Err(Error::OffsetOutOfBounds(address));
258    }
259
260    let mut cursor = Cursor::new(context.data);
261    cursor.set_position(address);
262
263    let node = BTreeV1Node::parse(
264        &mut cursor,
265        context.offset_size,
266        context.length_size,
267        context.ndims,
268    )?;
269
270    if node.level == 0 {
271        // Leaf node — collect (key[i], child[i]) pairs.
272        for (i, child_addr) in node.children.iter().enumerate() {
273            let include = match &node.keys[i] {
274                BTreeV1Key::RawData { offsets, .. } => {
275                    context.chunk_bounds.map_or(true, |(first, last)| {
276                        offsets
277                            .iter()
278                            .zip(context.chunk_dims.iter())
279                            .enumerate()
280                            .all(|(dim, (offset, chunk_dim))| {
281                                let chunk_index = *offset / u64::from(*chunk_dim);
282                                chunk_index >= first[dim] && chunk_index <= last[dim]
283                            })
284                    })
285                }
286                _ => true,
287            };
288
289            if include {
290                results.push((node.keys[i].clone(), *child_addr));
291            }
292        }
293    } else {
294        // Internal node — recurse into each child.
295        for child_addr in &node.children {
296            collect_recursive(context, *child_addr, results)?;
297        }
298    }
299
300    Ok(())
301}
302
303fn collect_recursive_storage(
304    context: BTreeV1CollectContextStorage<'_>,
305    address: u64,
306    results: &mut Vec<(BTreeV1Key, u64)>,
307) -> Result<()> {
308    if Cursor::is_undefined_offset(address, context.offset_size) {
309        return Ok(());
310    }
311
312    let header_len = 4 + 1 + 1 + 2 + 2 * usize::from(context.offset_size);
313    let header = context.storage.read_range(address, header_len)?;
314    let mut header_cursor = Cursor::new(header.as_ref());
315    let sig = header_cursor.read_bytes(4)?;
316    if sig != BTREE_V1_SIGNATURE {
317        return Err(Error::InvalidBTreeSignature);
318    }
319
320    let node_type = header_cursor.read_u8()?;
321    let _level = header_cursor.read_u8()?;
322    let entries_used = header_cursor.read_u16_le()?;
323    let key_size = match node_type {
324        0 => usize::from(context.length_size),
325        1 => {
326            let ndims = context.ndims.ok_or_else(|| {
327                Error::InvalidData("ndims required for raw data chunk B-tree keys".into())
328            })?;
329            // chunk_size (4) + filter_mask (4), then (ndims + 1) dimension
330            // offsets at a fixed 8 bytes each — NOT size_of_offsets (HDF5 spec).
331            8 + (usize::try_from(ndims).map_err(|_| {
332                Error::InvalidData("B-tree v1 ndims exceeds platform usize capacity".into())
333            })? + 1)
334                * 8
335        }
336        _ => {
337            return Err(Error::InvalidData(format!(
338                "unknown v1 B-tree node type: {node_type}"
339            )));
340        }
341    };
342    let node_len = header_len
343        + (usize::from(entries_used) + 1) * key_size
344        + usize::from(entries_used) * usize::from(context.offset_size);
345    let node_bytes = context.storage.read_range(address, node_len)?;
346    let mut cursor = Cursor::new(node_bytes.as_ref());
347    let node = BTreeV1Node::parse(
348        &mut cursor,
349        context.offset_size,
350        context.length_size,
351        context.ndims,
352    )?;
353
354    if node.level == 0 {
355        for (i, child_addr) in node.children.iter().enumerate() {
356            let include = match &node.keys[i] {
357                BTreeV1Key::RawData { offsets, .. } => {
358                    context.chunk_bounds.map_or(true, |(first, last)| {
359                        offsets
360                            .iter()
361                            .zip(context.chunk_dims.iter())
362                            .enumerate()
363                            .all(|(dim, (offset, chunk_dim))| {
364                                let chunk_index = *offset / u64::from(*chunk_dim);
365                                chunk_index >= first[dim] && chunk_index <= last[dim]
366                            })
367                    })
368                }
369                _ => true,
370            };
371
372            if include {
373                results.push((node.keys[i].clone(), *child_addr));
374            }
375        }
376    } else {
377        for child_addr in &node.children {
378            collect_recursive_storage(context, *child_addr, results)?;
379        }
380    }
381
382    Ok(())
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    /// Build a v1 B-tree node (type 0, group) from the given parameters.
390    #[allow(clippy::too_many_arguments)]
391    fn build_group_node(
392        level: u8,
393        entries_used: u16,
394        left_sibling: u64,
395        right_sibling: u64,
396        keys: &[u64],     // local heap offsets
397        children: &[u64], // child addresses
398        offset_size: u8,
399        length_size: u8,
400    ) -> Vec<u8> {
401        assert_eq!(keys.len(), entries_used as usize + 1);
402        assert_eq!(children.len(), entries_used as usize);
403
404        let mut buf = Vec::new();
405        buf.extend_from_slice(b"TREE");
406        buf.push(0); // type 0 = group
407        buf.push(level);
408        buf.extend_from_slice(&entries_used.to_le_bytes());
409        push_offset(&mut buf, left_sibling, offset_size);
410        push_offset(&mut buf, right_sibling, offset_size);
411
412        for i in 0..keys.len() {
413            push_length(&mut buf, keys[i], length_size);
414            if i < children.len() {
415                push_offset(&mut buf, children[i], offset_size);
416            }
417        }
418
419        buf
420    }
421
422    /// Build a v1 B-tree node (type 1, raw data) from the given parameters.
423    fn build_rawdata_node(
424        level: u8,
425        entries_used: u16,
426        left_sibling: u64,
427        right_sibling: u64,
428        keys: &[(u32, u32, Vec<u64>)], // (chunk_size, filter_mask, offsets)
429        children: &[u64],
430        offset_size: u8,
431    ) -> Vec<u8> {
432        assert_eq!(keys.len(), entries_used as usize + 1);
433        assert_eq!(children.len(), entries_used as usize);
434
435        let mut buf = Vec::new();
436        buf.extend_from_slice(b"TREE");
437        buf.push(1); // type 1 = raw data
438        buf.push(level);
439        buf.extend_from_slice(&entries_used.to_le_bytes());
440        push_offset(&mut buf, left_sibling, offset_size);
441        push_offset(&mut buf, right_sibling, offset_size);
442
443        for i in 0..keys.len() {
444            let (cs, fm, ref offs) = keys[i];
445            buf.extend_from_slice(&cs.to_le_bytes());
446            buf.extend_from_slice(&fm.to_le_bytes());
447            for &o in offs {
448                // Chunk-key dimension offsets are ALWAYS 8 bytes, independent of
449                // the superblock's size_of_offsets (HDF5 format spec). Child
450                // pointers below remain offset_size-byte file addresses.
451                buf.extend_from_slice(&o.to_le_bytes());
452            }
453            if i < children.len() {
454                push_offset(&mut buf, children[i], offset_size);
455            }
456        }
457
458        buf
459    }
460
461    fn push_offset(buf: &mut Vec<u8>, val: u64, size: u8) {
462        match size {
463            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
464            8 => buf.extend_from_slice(&val.to_le_bytes()),
465            _ => panic!("unsupported offset size in test"),
466        }
467    }
468
469    fn push_length(buf: &mut Vec<u8>, val: u64, size: u8) {
470        match size {
471            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
472            8 => buf.extend_from_slice(&val.to_le_bytes()),
473            _ => panic!("unsupported length size in test"),
474        }
475    }
476
477    #[test]
478    fn parse_group_leaf_node() {
479        let undef8 = 0xFFFF_FFFF_FFFF_FFFFu64;
480        let data = build_group_node(
481            0,                 // leaf
482            2,                 // 2 entries
483            undef8,            // no left sibling
484            undef8,            // no right sibling
485            &[0, 8, 16],       // 3 keys (offsets into local heap)
486            &[0x1000, 0x2000], // 2 children (SNOD addresses)
487            8,
488            8,
489        );
490
491        let mut cursor = Cursor::new(&data);
492        let node = BTreeV1Node::parse(&mut cursor, 8, 8, None).unwrap();
493
494        assert_eq!(node.node_type, 0);
495        assert_eq!(node.level, 0);
496        assert_eq!(node.entries_used, 2);
497        assert!(Cursor::is_undefined_offset(node.left_sibling, 8));
498        assert!(Cursor::is_undefined_offset(node.right_sibling, 8));
499        assert_eq!(node.keys.len(), 3);
500        assert_eq!(node.children.len(), 2);
501
502        match &node.keys[0] {
503            BTreeV1Key::Group { local_heap_offset } => assert_eq!(*local_heap_offset, 0),
504            _ => panic!("expected Group key"),
505        }
506        match &node.keys[1] {
507            BTreeV1Key::Group { local_heap_offset } => assert_eq!(*local_heap_offset, 8),
508            _ => panic!("expected Group key"),
509        }
510        assert_eq!(node.children[0], 0x1000);
511        assert_eq!(node.children[1], 0x2000);
512    }
513
514    #[test]
515    fn parse_rawdata_leaf_node() {
516        let undef8 = 0xFFFF_FFFF_FFFF_FFFFu64;
517        // 2D dataset, ndims=2, so keys have 3 offsets each (ndims+1)
518        let data = build_rawdata_node(
519            0, // leaf
520            1, // 1 entry
521            undef8,
522            undef8,
523            &[
524                (1024, 0, vec![0, 0, 0]),   // key[0]: chunk at origin
525                (1024, 0, vec![10, 20, 0]), // key[1]: next chunk boundary
526            ],
527            &[0x5000], // 1 child = chunk data address
528            8,
529        );
530
531        let mut cursor = Cursor::new(&data);
532        let node = BTreeV1Node::parse(&mut cursor, 8, 8, Some(2)).unwrap();
533
534        assert_eq!(node.node_type, 1);
535        assert_eq!(node.level, 0);
536        assert_eq!(node.entries_used, 1);
537        assert_eq!(node.keys.len(), 2);
538        assert_eq!(node.children.len(), 1);
539
540        match &node.keys[0] {
541            BTreeV1Key::RawData {
542                chunk_size,
543                filter_mask,
544                offsets,
545            } => {
546                assert_eq!(*chunk_size, 1024);
547                assert_eq!(*filter_mask, 0);
548                assert_eq!(offsets, &[0, 0, 0]);
549            }
550            _ => panic!("expected RawData key"),
551        }
552
553        assert_eq!(node.children[0], 0x5000);
554    }
555
556    /// Regression test for 32-bit-superblock files (`size_of_offsets = 4`):
557    /// chunk-key dimension offsets are fixed 8-byte values, so the child chunk
558    /// address (a 4-byte file offset here) must be read at the correct cursor
559    /// position. Reading the dimension offsets as 4 bytes (the old bug)
560    /// misaligns the cursor and yields a garbage child address.
561    #[test]
562    fn parse_rawdata_leaf_4byte() {
563        let undef4 = 0xFFFF_FFFFu64;
564        let data = build_rawdata_node(
565            0,
566            1,
567            undef4,
568            undef4,
569            &[
570                (512, 0x03, vec![0, 0]), // ndims=1, key has 2 offsets (each 8 bytes)
571                (512, 0, vec![100, 0]),
572            ],
573            &[0x3000],
574            4,
575        );
576
577        let mut cursor = Cursor::new(&data);
578        let node = BTreeV1Node::parse(&mut cursor, 4, 4, Some(1)).unwrap();
579
580        assert_eq!(node.node_type, 1);
581        match &node.keys[0] {
582            BTreeV1Key::RawData {
583                chunk_size,
584                filter_mask,
585                offsets,
586            } => {
587                assert_eq!(*chunk_size, 512);
588                assert_eq!(*filter_mask, 0x03);
589                assert_eq!(offsets, &[0, 0]);
590            }
591            _ => panic!("expected RawData key"),
592        }
593        // The load-bearing assertion: the child chunk address is located only
594        // when the 8-byte dimension offsets are parsed at full width.
595        assert_eq!(node.children[0], 0x3000);
596    }
597
598    #[test]
599    fn bad_signature() {
600        let mut data = build_group_node(0, 0, u64::MAX, u64::MAX, &[0], &[], 8, 8);
601        data[0] = b'X'; // corrupt
602        let mut cursor = Cursor::new(&data);
603        assert!(matches!(
604            BTreeV1Node::parse(&mut cursor, 8, 8, None),
605            Err(Error::InvalidBTreeSignature)
606        ));
607    }
608
609    #[test]
610    fn collect_leaves_single_leaf() {
611        let undef8 = u64::MAX;
612        // Put the leaf node at offset 0 in our fake file data.
613        let node_data = build_group_node(0, 2, undef8, undef8, &[0, 5, 10], &[0x100, 0x200], 8, 8);
614
615        let results = collect_btree_v1_leaves(&node_data, 0, 8, 8, None, &[], None).unwrap();
616
617        assert_eq!(results.len(), 2);
618        match &results[0].0 {
619            BTreeV1Key::Group { local_heap_offset } => assert_eq!(*local_heap_offset, 0),
620            _ => panic!("expected Group key"),
621        }
622        assert_eq!(results[0].1, 0x100);
623        assert_eq!(results[1].1, 0x200);
624    }
625
626    #[test]
627    fn collect_leaves_two_level_tree() {
628        let undef8 = u64::MAX;
629
630        // Build a two-level tree:
631        //   Root (level 1) at offset 0, with 2 children
632        //     Leaf A at offset 1000
633        //     Leaf B at offset 2000
634
635        let leaf_a = build_group_node(0, 1, undef8, undef8, &[0, 5], &[0xA00], 8, 8);
636
637        let leaf_b = build_group_node(0, 1, undef8, undef8, &[10, 15], &[0xB00], 8, 8);
638
639        let root = build_group_node(
640            1,
641            2,
642            undef8,
643            undef8,
644            &[0, 5, 15],   // 3 keys for 2 children
645            &[1000, 2000], // children at offsets 1000 and 2000
646            8,
647            8,
648        );
649
650        // Assemble into a single buffer: root at 0, leaf_a at 1000, leaf_b at 2000.
651        let total_size = 3000 + leaf_b.len();
652        let mut file_data = vec![0u8; total_size];
653        file_data[..root.len()].copy_from_slice(&root);
654        file_data[1000..1000 + leaf_a.len()].copy_from_slice(&leaf_a);
655        file_data[2000..2000 + leaf_b.len()].copy_from_slice(&leaf_b);
656
657        let results = collect_btree_v1_leaves(&file_data, 0, 8, 8, None, &[], None).unwrap();
658
659        assert_eq!(results.len(), 2);
660        assert_eq!(results[0].1, 0xA00);
661        assert_eq!(results[1].1, 0xB00);
662    }
663
664    #[test]
665    fn collect_undefined_root() {
666        // Undefined root address should produce empty results.
667        let data = vec![0u8; 100];
668        let results = collect_btree_v1_leaves(&data, u64::MAX, 8, 8, None, &[], None).unwrap();
669        assert!(results.is_empty());
670    }
671}