Skip to main content

rustyhdf5_format/
btree_v2.rs

1//! HDF5 B-tree v2 parsing.
2
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6#[cfg(feature = "checksum")]
7use byteorder::{ByteOrder, LittleEndian};
8
9use crate::error::FormatError;
10
11/// Parsed B-tree v2 header (signature "BTHD").
12#[derive(Debug, Clone)]
13pub struct BTreeV2Header {
14    /// B-tree type: 5=links indexed by name, 6=links indexed by creation order, etc.
15    pub tree_type: u8,
16    /// Node size in bytes.
17    pub node_size: u32,
18    /// Record size in bytes.
19    pub record_size: u16,
20    /// Depth of the tree (0 = root is a leaf).
21    pub depth: u16,
22    /// Address of root node.
23    pub root_node_address: u64,
24    /// Number of records in the root node.
25    pub num_records_in_root: u16,
26    /// Total number of records in all nodes.
27    pub total_records: u64,
28}
29
30/// A single record from a B-tree v2 node.
31#[derive(Debug, Clone)]
32pub struct BTreeV2Record {
33    /// Raw record bytes (record_size bytes).
34    pub data: Vec<u8>,
35}
36
37fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
38    let s = size as usize;
39    if pos + s > data.len() {
40        return Err(FormatError::UnexpectedEof {
41            expected: pos + s,
42            available: data.len(),
43        });
44    }
45    Ok(match size {
46        2 => u16::from_le_bytes([data[pos], data[pos + 1]]) as u64,
47        4 => u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64,
48        8 => u64::from_le_bytes([
49            data[pos], data[pos + 1], data[pos + 2], data[pos + 3],
50            data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
51        ]),
52        _ => return Err(FormatError::InvalidOffsetSize(size)),
53    })
54}
55
56fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError> {
57    match pos.checked_add(needed) {
58        Some(end) if end <= data.len() => Ok(()),
59        _ => Err(FormatError::UnexpectedEof {
60            expected: pos.saturating_add(needed),
61            available: data.len(),
62        }),
63    }
64}
65
66/// Compute the number of bytes needed to represent a count, using variable-width encoding.
67/// B-tree v2 uses this for the number of records fields in internal nodes.
68fn bytes_for_max_records(max_nrec: u64) -> usize {
69    if max_nrec == 0 {
70        return 1;
71    }
72    let bits = 64 - max_nrec.leading_zeros() as usize;
73    bits.div_ceil(8)
74}
75
76/// Read a variable-width unsigned integer (1-8 bytes, LE).
77fn read_var_uint(data: &[u8], pos: usize, width: usize) -> Result<u64, FormatError> {
78    ensure_len(data, pos, width)?;
79    let mut val = 0u64;
80    for i in 0..width {
81        val |= (data[pos + i] as u64) << (i * 8);
82    }
83    Ok(val)
84}
85
86impl BTreeV2Header {
87    /// Parse a B-tree v2 header at the given offset.
88    pub fn parse(
89        file_data: &[u8],
90        offset: usize,
91        offset_size: u8,
92        length_size: u8,
93    ) -> Result<BTreeV2Header, FormatError> {
94        ensure_len(file_data, offset, 4)?;
95        if &file_data[offset..offset + 4] != b"BTHD" {
96            return Err(FormatError::InvalidBTreeV2Signature);
97        }
98
99        ensure_len(file_data, offset, 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1)?;
100        let version = file_data[offset + 4];
101        if version != 0 {
102            return Err(FormatError::InvalidBTreeV2Version(version));
103        }
104
105        let tree_type = file_data[offset + 5];
106        let node_size = u32::from_le_bytes([
107            file_data[offset + 6],
108            file_data[offset + 7],
109            file_data[offset + 8],
110            file_data[offset + 9],
111        ]);
112        let record_size = u16::from_le_bytes([file_data[offset + 10], file_data[offset + 11]]);
113        let depth = u16::from_le_bytes([file_data[offset + 12], file_data[offset + 13]]);
114        let _split_percent = file_data[offset + 14];
115        let _merge_percent = file_data[offset + 15];
116
117        let mut pos = offset + 16;
118        let root_node_address = read_offset(file_data, pos, offset_size)?;
119        pos += offset_size as usize;
120
121        ensure_len(file_data, pos, 2)?;
122        let num_records_in_root =
123            u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
124        pos += 2;
125
126        let total_records = read_offset(file_data, pos, length_size)?;
127        #[allow(unused_assignments)]
128        {
129            pos += length_size as usize;
130        }
131
132        // Validate header checksum
133        #[cfg(feature = "checksum")]
134        {
135            ensure_len(file_data, pos, 4)?;
136            let stored = LittleEndian::read_u32(&file_data[pos..pos + 4]);
137            let computed = crate::checksum::jenkins_lookup3(&file_data[offset..pos]);
138            if computed != stored {
139                return Err(FormatError::ChecksumMismatch {
140                    expected: stored,
141                    computed,
142                });
143            }
144        }
145
146        Ok(BTreeV2Header {
147            tree_type,
148            node_size,
149            record_size,
150            depth,
151            root_node_address,
152            num_records_in_root,
153            total_records,
154        })
155    }
156}
157
158/// Compute maximum records per node for a given depth level.
159/// leaf: (node_size - overhead) / record_size
160/// internal: depends on pointers
161fn max_records_leaf(node_size: u32, record_size: u16) -> u64 {
162    // Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10
163    let overhead = 10u32;
164    if node_size <= overhead || record_size == 0 {
165        return 0;
166    }
167    ((node_size - overhead) / record_size as u32) as u64
168}
169
170/// Collect all records from a B-tree v2 by traversing from the root.
171pub fn collect_btree_v2_records(
172    file_data: &[u8],
173    header: &BTreeV2Header,
174    offset_size: u8,
175    length_size: u8,
176) -> Result<Vec<BTreeV2Record>, FormatError> {
177    if header.total_records == 0 || header.num_records_in_root == 0 {
178        return Ok(Vec::new());
179    }
180
181    let max_leaf_nrec = max_records_leaf(header.node_size, header.record_size);
182
183    if header.depth == 0 {
184        // Root is a leaf
185        parse_leaf_records(
186            file_data,
187            header.root_node_address as usize,
188            header.num_records_in_root,
189            header.record_size,
190        )
191    } else {
192        // Root is internal; traverse recursively
193        let mut records = Vec::new();
194        collect_internal_records(
195            file_data,
196            header.root_node_address as usize,
197            header.num_records_in_root,
198            header.depth,
199            header.record_size,
200            header.node_size,
201            offset_size,
202            length_size,
203            max_leaf_nrec,
204            &mut records,
205        )?;
206        Ok(records)
207    }
208}
209
210/// Parse records from a leaf node (signature "BTLF").
211fn parse_leaf_records(
212    file_data: &[u8],
213    offset: usize,
214    num_records: u16,
215    record_size: u16,
216) -> Result<Vec<BTreeV2Record>, FormatError> {
217    // signature(4) + version(1) + type(1) = 6 bytes header
218    ensure_len(file_data, offset, 6)?;
219    if &file_data[offset..offset + 4] != b"BTLF" {
220        return Err(FormatError::InvalidBTreeV2Signature);
221    }
222
223    let pos = offset + 6;
224    let rs = record_size as usize;
225    let total = num_records as usize * rs;
226    ensure_len(file_data, pos, total)?;
227
228    // Validate checksum: 4 bytes after records + padding
229    #[cfg(feature = "checksum")]
230    {
231        let checksum_pos = pos + total;
232        if file_data.len() >= checksum_pos + 4 {
233            let stored = LittleEndian::read_u32(&file_data[checksum_pos..checksum_pos + 4]);
234            let computed = crate::checksum::jenkins_lookup3(&file_data[offset..checksum_pos]);
235            if computed != stored {
236                return Err(FormatError::ChecksumMismatch {
237                    expected: stored,
238                    computed,
239                });
240            }
241        }
242    }
243
244    let mut records = Vec::with_capacity(num_records as usize);
245    for i in 0..num_records as usize {
246        let start = pos + i * rs;
247        records.push(BTreeV2Record {
248            data: file_data[start..start + rs].to_vec(),
249        });
250    }
251    Ok(records)
252}
253
254/// Recursively collect records from an internal node.
255#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
256fn collect_internal_records(
257    file_data: &[u8],
258    offset: usize,
259    num_records: u16,
260    depth: u16,
261    record_size: u16,
262    node_size: u32,
263    offset_size: u8,
264    length_size: u8,
265    max_leaf_nrec: u64,
266    out: &mut Vec<BTreeV2Record>,
267) -> Result<(), FormatError> {
268    // signature(4) + version(1) + type(1) = 6
269    ensure_len(file_data, offset, 6)?;
270    if &file_data[offset..offset + 4] != b"BTIN" {
271        return Err(FormatError::InvalidBTreeV2Signature);
272    }
273
274    let nr = num_records as usize;
275    let rs = record_size as usize;
276    let mut pos = offset + 6;
277
278    // Read all records first
279    ensure_len(file_data, pos, nr * rs)?;
280    let records_start = pos;
281    pos += nr * rs;
282
283    // Compute sizes for child pointers
284    // max_records at child depth - for variable-width nrec encoding
285    let child_depth = depth - 1;
286    let max_nrec_child = if child_depth == 0 {
287        max_leaf_nrec
288    } else {
289        // For internal nodes at child_depth, computing max records is complex.
290        // Use a reasonable upper bound from node_size.
291        max_leaf_nrec * 2 // conservative estimate
292    };
293    let nrec_width = bytes_for_max_records(max_nrec_child);
294
295    // Total records in subtree width (only if depth > 1)
296    let total_nrec_width = if depth > 1 {
297        // Width to hold total records in a subtree
298        // We compute max possible total records at this subtree depth
299        let max_total = header_max_total_records(max_leaf_nrec, depth - 1);
300        bytes_for_max_records(max_total)
301    } else {
302        0
303    };
304
305    let num_children = nr + 1;
306    let child_ptr_size = offset_size as usize + nrec_width + total_nrec_width;
307    ensure_len(file_data, pos, num_children * child_ptr_size)?;
308
309    // Read child pointers
310    let mut children = Vec::with_capacity(num_children);
311    for _ in 0..num_children {
312        let addr = read_offset(file_data, pos, offset_size)?;
313        pos += offset_size as usize;
314        let child_nrec = read_var_uint(file_data, pos, nrec_width)? as u16;
315        pos += nrec_width;
316        pos += total_nrec_width; // skip total records in subtree
317        children.push((addr, child_nrec));
318    }
319
320    // Interleave: child[0], record[0], child[1], record[1], ..., child[nr]
321    // We collect child[0] records, then record[0], then child[1], etc.
322    for (i, &(child_addr, child_nrec)) in children.iter().enumerate() {
323        if child_depth == 0 {
324            let leaf_recs = parse_leaf_records(
325                file_data,
326                child_addr as usize,
327                child_nrec,
328                record_size,
329            )?;
330            out.extend(leaf_recs);
331        } else {
332            collect_internal_records(
333                file_data,
334                child_addr as usize,
335                child_nrec,
336                child_depth,
337                record_size,
338                node_size,
339                offset_size,
340                length_size,
341                max_leaf_nrec,
342                out,
343            )?;
344        }
345
346        // Add record[i] (except after the last child)
347        if i < nr {
348            let rec_start = records_start + i * rs;
349            out.push(BTreeV2Record {
350                data: file_data[rec_start..rec_start + rs].to_vec(),
351            });
352        }
353    }
354
355    Ok(())
356}
357
358/// Estimate maximum total records at a given depth (for variable-width encoding).
359fn header_max_total_records(max_leaf_nrec: u64, depth: u16) -> u64 {
360    // Conservative: branching factor * max_leaf at each level
361    let mut total = max_leaf_nrec;
362    for _ in 0..depth {
363        total = total.saturating_mul(max_leaf_nrec.max(2));
364    }
365    total
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    fn build_btree_v2_header(
373        tree_type: u8,
374        node_size: u32,
375        record_size: u16,
376        depth: u16,
377        root_addr: u64,
378        num_records_root: u16,
379        total_records: u64,
380        offset_size: u8,
381        length_size: u8,
382    ) -> Vec<u8> {
383        let mut buf = Vec::new();
384        buf.extend_from_slice(b"BTHD");
385        buf.push(0); // version
386        buf.push(tree_type);
387        buf.extend_from_slice(&node_size.to_le_bytes());
388        buf.extend_from_slice(&record_size.to_le_bytes());
389        buf.extend_from_slice(&depth.to_le_bytes());
390        buf.push(85); // split_percent
391        buf.push(40); // merge_percent
392        match offset_size {
393            4 => buf.extend_from_slice(&(root_addr as u32).to_le_bytes()),
394            8 => buf.extend_from_slice(&root_addr.to_le_bytes()),
395            _ => {}
396        }
397        buf.extend_from_slice(&num_records_root.to_le_bytes());
398        match length_size {
399            4 => buf.extend_from_slice(&(total_records as u32).to_le_bytes()),
400            8 => buf.extend_from_slice(&total_records.to_le_bytes()),
401            _ => {}
402        }
403        let checksum = crate::checksum::jenkins_lookup3(&buf);
404        buf.extend_from_slice(&checksum.to_le_bytes());
405        buf
406    }
407
408    fn build_leaf_node(tree_type: u8, records: &[&[u8]]) -> Vec<u8> {
409        let mut buf = Vec::new();
410        buf.extend_from_slice(b"BTLF");
411        buf.push(0); // version
412        buf.push(tree_type);
413        for rec in records {
414            buf.extend_from_slice(rec);
415        }
416        let checksum = crate::checksum::jenkins_lookup3(&buf);
417        buf.extend_from_slice(&checksum.to_le_bytes());
418        buf
419    }
420
421    #[test]
422    fn parse_header() {
423        let data = build_btree_v2_header(5, 512, 11, 0, 0x1000, 3, 3, 8, 8);
424        let hdr = BTreeV2Header::parse(&data, 0, 8, 8).unwrap();
425        assert_eq!(hdr.tree_type, 5);
426        assert_eq!(hdr.node_size, 512);
427        assert_eq!(hdr.record_size, 11);
428        assert_eq!(hdr.depth, 0);
429        assert_eq!(hdr.root_node_address, 0x1000);
430        assert_eq!(hdr.num_records_in_root, 3);
431        assert_eq!(hdr.total_records, 3);
432    }
433
434    #[test]
435    fn parse_leaf_with_2_records() {
436        let rec1 = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
437        let rec2 = [11u8, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
438        let leaf = build_leaf_node(5, &[&rec1, &rec2]);
439
440        let leaf_offset = 256usize;
441        let header = build_btree_v2_header(5, 512, 11, 0, leaf_offset as u64, 2, 2, 8, 8);
442
443        let mut file_data = vec![0u8; 512];
444        file_data[..header.len()].copy_from_slice(&header);
445        file_data[leaf_offset..leaf_offset + leaf.len()].copy_from_slice(&leaf);
446
447        let hdr = BTreeV2Header::parse(&file_data, 0, 8, 8).unwrap();
448        let records = collect_btree_v2_records(&file_data, &hdr, 8, 8).unwrap();
449        assert_eq!(records.len(), 2);
450        assert_eq!(records[0].data, rec1.to_vec());
451        assert_eq!(records[1].data, rec2.to_vec());
452    }
453
454    #[test]
455    fn invalid_signature() {
456        let mut data = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8);
457        data[0] = b'X';
458        let err = BTreeV2Header::parse(&data, 0, 8, 8).unwrap_err();
459        assert_eq!(err, FormatError::InvalidBTreeV2Signature);
460    }
461
462    #[test]
463    fn invalid_version() {
464        let mut data = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8);
465        data[4] = 1; // bad version
466        let err = BTreeV2Header::parse(&data, 0, 8, 8).unwrap_err();
467        assert_eq!(err, FormatError::InvalidBTreeV2Version(1));
468    }
469
470    #[test]
471    fn empty_tree() {
472        let header = build_btree_v2_header(5, 512, 11, 0, 0, 0, 0, 8, 8);
473        let hdr = BTreeV2Header::parse(&header, 0, 8, 8).unwrap();
474        let records = collect_btree_v2_records(&header, &hdr, 8, 8).unwrap();
475        assert!(records.is_empty());
476    }
477}