Skip to main content

rust_hdf5/format/
dense_link.rs

1//! Dense link storage.
2//!
3//! Once a group accumulates more links than the Group Info message's
4//! `max_compact` threshold — or one link too large to encode as a header
5//! message — libhdf5 moves *all* of its links out of the object header
6//! (`H5Gobj.c::H5G_obj_insert`). The `Link Info` message then points at a
7//! fractal heap holding each link as an encoded link message, plus a v2 B-tree
8//! indexing them by name hash (`H5Gdense.c::H5G__dense_create`).
9//!
10//! The shape mirrors dense attribute storage, but none of the constants carry
11//! over: the heap uses `H5G_FHEAP_*` (512-byte first row, a 32-bit heap address
12//! space, and so a 7-byte heap ID rather than 8), and the name index is
13//! record type 5 with the hash *before* the heap ID — the reverse of the
14//! attribute record's field order.
15//!
16//! Reference: `H5Gdense.c` (`H5G__dense_create`, `H5G__dense_insert`),
17//! `H5Gbtree2.c` (`H5G__dense_btree2_name_encode`).
18
19use crate::format::checksum::checksum_metadata;
20use crate::format::chunk_index::btree_v2::{
21    collect_btree_v2_records, Bt2Header, Bt2Tree, BT2_TYPE_GRP_CORDER, BT2_TYPE_GRP_NAME,
22};
23use crate::format::creation_order::CreationOrder;
24use crate::format::fractal_heap::{
25    collect_managed_blocks, read_heap_object, FractalHeapHeader, HeapId, HeapParams,
26};
27use crate::format::fractal_heap_write::{build_heap, HeapBlock};
28use crate::format::messages::link::LinkMessage;
29use crate::format::messages::link_info::LinkInfoMessage;
30use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
31
32/// Length of the fractal-heap ID embedded in a dense-link record
33/// (`H5G_DENSE_FHEAP_ID_LEN`).
34const FHEAP_ID_LEN: usize = 7;
35
36/// A name-index record: name hash, then heap ID. 11 bytes on disk
37/// (`H5G__dense_btree2_name_encode`).
38const NAME_RECORD_LEN: usize = 4 + FHEAP_ID_LEN;
39
40/// A creation-order-index record: creation order, then heap ID. 15 bytes on
41/// disk (`H5G__dense_btree2_corder_encode`).
42const CORDER_RECORD_LEN: usize = 8 + FHEAP_ID_LEN;
43
44/// Node size of either index (`H5G_NAME_BT2_NODE_SIZE`,
45/// `H5G_CORDER_BT2_NODE_SIZE`).
46const NAME_BT2_NODE_SIZE: u32 = 512;
47
48/// The hash a link name is indexed under (`H5G__dense_insert`).
49pub fn name_hash(name: &str) -> u32 {
50    checksum_metadata(name.as_bytes())
51}
52
53/// Read every link a group keeps in dense storage.
54///
55/// Returns them in name-index (hash) order, the order `H5Literate2` walks with
56/// `H5_INDEX_NAME`. A `linfo` describing compact storage yields an empty
57/// vector; a record the reader cannot resolve to a heap object is an error,
58/// not a silent omission, so a partially-read group never masquerades as a
59/// complete one.
60///
61/// The index is what is walked, not the heap: a link at or above the heap's
62/// `max_man_size` is a "huge" object living outside the managed blocks, and
63/// the free space trailing each direct block is not distinguishable from a
64/// link message by inspection.
65pub fn read_dense_links<R: BlockReader>(
66    linfo: &LinkInfoMessage,
67    ctx: &FormatContext,
68    reader: &mut R,
69) -> FormatResult<Vec<LinkMessage>> {
70    if linfo.fractal_heap_address == UNDEF_ADDR {
71        return Ok(Vec::new());
72    }
73    if linfo.name_btree_address == UNDEF_ADDR {
74        return Err(FormatError::InvalidData(
75            "dense link storage without a name index B-tree".into(),
76        ));
77    }
78
79    // The heap header's on-disk size depends only on the address/length
80    // widths, so a generous prefix read covers it.
81    let heap_buf = reader.read_block(linfo.fractal_heap_address, 512)?;
82    let heap = FractalHeapHeader::decode(&heap_buf, ctx)?;
83    let blocks = collect_managed_blocks(&heap, ctx, reader)?;
84
85    let bt2_buf = reader.read_block(linfo.name_btree_address, 256)?;
86    let bt2 = Bt2Header::decode(&bt2_buf, ctx)?;
87    if bt2.record_type != BT2_TYPE_GRP_NAME {
88        return Err(FormatError::InvalidData(format!(
89            "link name index has B-tree record type {}, expected {}",
90            bt2.record_type, BT2_TYPE_GRP_NAME
91        )));
92    }
93    if (bt2.record_size as usize) < NAME_RECORD_LEN {
94        return Err(FormatError::InvalidData(format!(
95            "link name index record is {} bytes, expected at least {}",
96            bt2.record_size, NAME_RECORD_LEN
97        )));
98    }
99
100    let records = collect_btree_v2_records(&bt2, ctx, reader)?;
101    let rec_size = bt2.record_size as usize;
102    let mut links = Vec::with_capacity(records.len() / rec_size);
103    for rec in records.chunks_exact(rec_size) {
104        let id = HeapId::parse(&rec[4..4 + FHEAP_ID_LEN], &heap, ctx)?;
105        let bytes = read_heap_object(&id, &heap, ctx, &blocks, reader)?;
106        links.push(LinkMessage::decode(&bytes, ctx)?.0);
107    }
108    Ok(links)
109}
110
111/// Dense storage laid out for a group: what its header must say, and what must
112/// be written for that to be true.
113#[derive(Debug, Clone, PartialEq)]
114pub struct DenseLinkStorage {
115    /// The `Link Info` message naming the heap and the name index.
116    pub linfo: LinkInfoMessage,
117    /// Heap header, heap blocks, huge objects and the index's nodes.
118    pub blocks: Vec<HeapBlock>,
119}
120
121/// Lay `links` out as dense storage: a fractal heap holding one encoded link
122/// message each, plus a v2 B-tree indexing them by name hash.
123///
124/// `alloc` allocates file space and returns the address; every allocation it
125/// hands out is reported back through [`DenseLinkStorage::blocks`], so a caller
126/// that abandons the result can free exactly what it took.
127///
128/// `order` is the group's link creation-order policy: `Tracked` puts the
129/// running maximum in the `Link Info` message, and `Indexed` additionally
130/// bulk-loads the creation-order B-tree.
131///
132/// Mirrors `H5G__dense_create` followed by one `H5G__dense_insert` per link,
133/// except that the whole set is known up front, so the index is bulk-loaded
134/// rather than grown by insertion.
135pub fn build_dense_links(
136    links: &[LinkMessage],
137    ctx: &FormatContext,
138    order: CreationOrder,
139    alloc: &mut dyn FnMut(u64) -> u64,
140) -> FormatResult<DenseLinkStorage> {
141    let objects: Vec<Vec<u8>> = links.iter().map(|l| l.encode(ctx)).collect();
142    let heap = build_heap(&HeapParams::group_links(), ctx, &objects, alloc)?;
143
144    // `H5G__dense_btree2_name_compare` orders on the hash and breaks ties by
145    // strcmp of the name pulled back out of the heap, so a bulk load has to
146    // sort the same way or a lookup walking the tree misses records.
147    let mut by_name: Vec<usize> = (0..links.len()).collect();
148    by_name.sort_by(|&a, &b| {
149        name_hash(&links[a].name)
150            .cmp(&name_hash(&links[b].name))
151            .then_with(|| links[a].name.cmp(&links[b].name))
152    });
153
154    let mut records = Vec::with_capacity(by_name.len() * NAME_RECORD_LEN);
155    for &i in &by_name {
156        records.extend_from_slice(&name_hash(&links[i].name).to_le_bytes());
157        records.extend_from_slice(&heap.ids[i]);
158    }
159
160    let mut blocks = heap.blocks;
161    let bt2_addr = build_index(
162        BT2_TYPE_GRP_NAME,
163        NAME_RECORD_LEN as u16,
164        &records,
165        ctx,
166        alloc,
167        &mut blocks,
168    );
169
170    // Tracking is what stamps a creation order onto each link message, so a
171    // tracked group whose links carry none is a caller bug, not a file the
172    // index could be built without.
173    let corders: Option<Vec<i64>> = order
174        .is_tracked()
175        .then(|| links.iter().map(|l| l.creation_order).collect())
176        .flatten();
177    if order.is_tracked() && corders.is_none() {
178        return Err(FormatError::InvalidData(
179            "a group tracking link creation order has a link with no creation order".into(),
180        ));
181    }
182
183    let corder_bt2_addr = order.is_indexed().then(|| {
184        let corders = corders.as_ref().expect("indexed implies tracked");
185        let mut by_corder: Vec<usize> = (0..links.len()).collect();
186        by_corder.sort_by_key(|&i| corders[i]);
187        let mut records = Vec::with_capacity(by_corder.len() * CORDER_RECORD_LEN);
188        for &i in &by_corder {
189            records.extend_from_slice(&corders[i].to_le_bytes());
190            records.extend_from_slice(&heap.ids[i]);
191        }
192        build_index(
193            BT2_TYPE_GRP_CORDER,
194            CORDER_RECORD_LEN as u16,
195            &records,
196            ctx,
197            alloc,
198            &mut blocks,
199        )
200    });
201
202    Ok(DenseLinkStorage {
203        linfo: LinkInfoMessage {
204            // `H5G_obj_insert` post-increments, so after n links the running
205            // maximum is n.
206            max_creation_order: corders.map(|c| c.len() as u64),
207            fractal_heap_address: heap.header_addr,
208            name_btree_address: bt2_addr,
209            creation_order_btree_address: corder_bt2_addr,
210        },
211        blocks,
212    })
213}
214
215/// Bulk-load one v2 B-tree, allocate its header and nodes, and append their
216/// images to `blocks`. Returns the header address.
217fn build_index(
218    record_type: u8,
219    record_size: u16,
220    records: &[u8],
221    ctx: &FormatContext,
222    alloc: &mut dyn FnMut(u64) -> u64,
223    blocks: &mut Vec<HeapBlock>,
224) -> u64 {
225    let tree = Bt2Tree::build(
226        record_type,
227        record_size,
228        NAME_BT2_NODE_SIZE,
229        ctx.sizeof_addr,
230        records,
231    );
232    let bt2_addr = alloc(tree.header(UNDEF_ADDR).encoded_size(ctx) as u64);
233    let node_addrs: Vec<u64> = tree
234        .nodes
235        .iter()
236        .map(|_| alloc(tree.node_size as u64))
237        .collect();
238    for (image, &addr) in tree.encode(ctx, &node_addrs).into_iter().zip(&node_addrs) {
239        blocks.push(HeapBlock {
240            addr,
241            len: tree.node_size as u64,
242            image,
243        });
244    }
245    let root_addr = node_addrs.last().copied().unwrap_or(UNDEF_ADDR);
246    let image = tree.header(root_addr).encode(ctx);
247    blocks.push(HeapBlock {
248        addr: bt2_addr,
249        len: image.len() as u64,
250        image,
251    });
252    bt2_addr
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn ctx() -> FormatContext {
260        FormatContext {
261            sizeof_addr: 8,
262            sizeof_size: 8,
263        }
264    }
265
266    /// A file image the builder's blocks are written into, so the dense reader
267    /// can be pointed straight back at what the writer produced.
268    struct MemFile {
269        bytes: Vec<u8>,
270    }
271
272    impl MemFile {
273        fn new() -> Self {
274            // Leave the first block unused so address 0 never means "unset".
275            Self { bytes: vec![0; 16] }
276        }
277        fn alloc(&mut self, len: u64) -> u64 {
278            let addr = self.bytes.len() as u64;
279            self.bytes.resize(self.bytes.len() + len as usize, 0);
280            addr
281        }
282    }
283
284    impl BlockReader for MemFile {
285        fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
286            let start = offset as usize;
287            if start > self.bytes.len() {
288                return Err(FormatError::BufferTooShort {
289                    needed: start,
290                    available: self.bytes.len(),
291                });
292            }
293            let end = (start + len).min(self.bytes.len());
294            Ok(self.bytes[start..end].to_vec())
295        }
296    }
297
298    /// Lay `links` out untracked, write the result into a fresh image, and
299    /// read them back through the dense reader.
300    fn round_trip(links: &[LinkMessage]) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
301        round_trip_ordered(links, CreationOrder::Untracked)
302    }
303
304    /// [`round_trip`] under an explicit creation-order policy.
305    fn round_trip_ordered(
306        links: &[LinkMessage],
307        order: CreationOrder,
308    ) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
309        let mut file = MemFile::new();
310        let dense = build_dense_links(links, &ctx(), order, &mut |len| file.alloc(len)).unwrap();
311        for block in &dense.blocks {
312            assert_eq!(block.len as usize, block.image.len(), "block len vs image");
313            let at = block.addr as usize;
314            file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
315        }
316        let read = read_dense_links(&dense.linfo, &ctx(), &mut file).unwrap();
317        (file, dense, read)
318    }
319
320    #[test]
321    fn compact_linfo_reads_no_dense_links() {
322        let mut file = MemFile::new();
323        assert!(
324            read_dense_links(&LinkInfoMessage::compact(), &ctx(), &mut file)
325                .unwrap()
326                .is_empty()
327        );
328    }
329
330    #[test]
331    fn a_dozen_links_round_trip_through_dense_storage() {
332        let links: Vec<LinkMessage> = (0..12)
333            .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
334            .collect();
335        let (_file, _dense, read) = round_trip(&links);
336
337        assert_eq!(read.len(), links.len());
338        // The reader returns them in name-index (hash) order, so compare as
339        // sets keyed by name.
340        for want in &links {
341            let got = read
342                .iter()
343                .find(|l| l.name == want.name)
344                .unwrap_or_else(|| panic!("'{}' missing from dense storage", want.name));
345            assert_eq!(got, want);
346        }
347    }
348
349    #[test]
350    fn a_soft_link_round_trips_beside_hard_ones() {
351        let links = vec![
352            LinkMessage::hard("orig", 0x800),
353            LinkMessage::soft("alias", "/orig"),
354        ];
355        let (_file, _dense, read) = round_trip(&links);
356        assert_eq!(read.len(), 2);
357        for want in &links {
358            assert_eq!(read.iter().find(|l| l.name == want.name).unwrap(), want);
359        }
360    }
361
362    #[test]
363    fn a_group_with_no_links_yields_an_empty_index() {
364        let (_file, _dense, read) = round_trip(&[]);
365        assert!(read.is_empty());
366    }
367
368    /// The heap parameters are the group ones, not the attribute ones: a
369    /// 7-byte heap ID over a 32-bit address space, first row 512 bytes.
370    #[test]
371    fn the_heap_uses_the_group_parameters() {
372        let links: Vec<LinkMessage> = (0..12)
373            .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
374            .collect();
375        let (mut file, dense, _read) = round_trip(&links);
376        let heap_buf = file
377            .read_block(dense.linfo.fractal_heap_address, 512)
378            .unwrap();
379        let heap = FractalHeapHeader::decode(&heap_buf, &ctx()).unwrap();
380        assert_eq!(heap.id_len, 7);
381        assert_eq!(heap.heap_off_size, 4);
382        assert_eq!(heap.heap_len_size, 2);
383        assert_eq!(heap.start_block_size, 512);
384        assert_eq!(heap.man_nobjs, 12);
385    }
386
387    #[test]
388    fn name_records_are_ordered_by_hash() {
389        // Enough links that the index is more than one leaf, so a misordered
390        // bulk load would put a record under the wrong subtree.
391        let links: Vec<LinkMessage> = (0..128)
392            .map(|i| LinkMessage::hard(&format!("d{i:03}"), 0x400 + i as u64 * 8))
393            .collect();
394        let (mut file, dense, read) = round_trip(&links);
395        assert_eq!(read.len(), links.len());
396
397        let bt2_buf = file
398            .read_block(dense.linfo.name_btree_address, 256)
399            .unwrap();
400        let bt2 = Bt2Header::decode(&bt2_buf, &ctx()).unwrap();
401        assert_eq!(bt2.record_type, BT2_TYPE_GRP_NAME);
402        assert_eq!(bt2.record_size as usize, NAME_RECORD_LEN);
403        assert!(bt2.depth > 0, "expected a multi-level index, got one leaf");
404
405        let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
406        let hashes: Vec<u32> = records
407            .as_chunks::<NAME_RECORD_LEN>()
408            .0
409            .iter()
410            .map(|r| u32::from_le_bytes(r[0..4].try_into().unwrap()))
411            .collect();
412        assert_eq!(hashes.len(), links.len());
413        assert!(
414            hashes.windows(2).all(|w| w[0] <= w[1]),
415            "name index is not hash-ordered: {hashes:?}"
416        );
417    }
418
419    /// Tracking on: the corder index is a second v2 B-tree of type 6, its
420    /// records are ordered by creation order (not by name hash), and the
421    /// `Link Info` message announces both the index and the post-incremented
422    /// maximum.
423    #[test]
424    fn a_tracked_group_gets_a_creation_order_index() {
425        // Names deliberately reverse the creation order, so an index built
426        // from the name ordering would show up here.
427        let links: Vec<LinkMessage> = (0..12u32)
428            .map(|i| {
429                LinkMessage::hard(&format!("d{:02}", 11 - i), 0x400 + i as u64 * 8)
430                    .with_creation_order(i as i64)
431            })
432            .collect();
433        let (mut file, dense, read) = round_trip_ordered(&links, CreationOrder::Indexed);
434        assert_eq!(read.len(), links.len());
435        assert_eq!(dense.linfo.max_creation_order, Some(12));
436
437        let addr = dense
438            .linfo
439            .creation_order_btree_address
440            .expect("tracked links must carry a creation-order index");
441        let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
442        assert_eq!(bt2.record_type, BT2_TYPE_GRP_CORDER);
443        assert_eq!(bt2.record_size as usize, CORDER_RECORD_LEN);
444
445        let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
446        let corders: Vec<i64> = records
447            .as_chunks::<CORDER_RECORD_LEN>()
448            .0
449            .iter()
450            .map(|r| i64::from_le_bytes(r[0..8].try_into().unwrap()))
451            .collect();
452        assert_eq!(corders, (0..12i64).collect::<Vec<_>>());
453    }
454
455    /// Tracking off: no second index, no maximum.
456    #[test]
457    fn an_untracked_group_gets_no_creation_order_index() {
458        let links: Vec<LinkMessage> = (0..12)
459            .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
460            .collect();
461        let (_file, dense, _read) = round_trip(&links);
462        assert_eq!(dense.linfo.creation_order_btree_address, None);
463        assert_eq!(dense.linfo.max_creation_order, None);
464    }
465
466    /// `H5Pset_link_creation_order(H5P_CRT_ORDER_TRACKED)` without `INDEXED`
467    /// is a state libhdf5 accepts: the running maximum is recorded and every
468    /// link keeps its creation order, but no second B-tree is built.
469    #[test]
470    fn a_tracked_but_unindexed_group_records_the_maximum_and_no_index() {
471        let links: Vec<LinkMessage> = (0..12u32)
472            .map(|i| {
473                LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8)
474                    .with_creation_order(i as i64)
475            })
476            .collect();
477        let (_file, dense, read) = round_trip_ordered(&links, CreationOrder::Tracked);
478        assert_eq!(read.len(), links.len());
479        assert_eq!(dense.linfo.max_creation_order, Some(12));
480        assert_eq!(dense.linfo.creation_order_btree_address, None);
481    }
482
483    /// A group that declares tracking but hands over links with no creation
484    /// order is a caller bug; building the storage anyway would write an
485    /// index whose records claim an order the heap objects do not carry.
486    #[test]
487    fn tracking_links_that_carry_no_creation_order_is_refused() {
488        let links: Vec<LinkMessage> = (0..12)
489            .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
490            .collect();
491        let mut file = MemFile::new();
492        let err = build_dense_links(&links, &ctx(), CreationOrder::Tracked, &mut |len| {
493            file.alloc(len)
494        })
495        .unwrap_err();
496        assert!(matches!(err, FormatError::InvalidData(_)), "{err:?}");
497    }
498
499    #[test]
500    fn dense_linfo_without_name_index_is_an_error() {
501        let linfo = LinkInfoMessage {
502            max_creation_order: None,
503            fractal_heap_address: 512,
504            name_btree_address: UNDEF_ADDR,
505            creation_order_btree_address: None,
506        };
507        let mut file = MemFile::new();
508        let err = read_dense_links(&linfo, &ctx(), &mut file).unwrap_err();
509        assert!(matches!(err, FormatError::InvalidData(_)));
510    }
511}