Skip to main content

rustyhdf5_format/
group_v1.rs

1//! V1 group traversal: resolve group children and navigate paths.
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use crate::btree_v1::collect_symbol_table_nodes;
7use crate::error::FormatError;
8use crate::local_heap::LocalHeap;
9use crate::message_type::MessageType;
10use crate::object_header::ObjectHeader;
11use crate::symbol_table::{SymbolTableMessage, SymbolTableNode};
12
13/// A resolved group entry (child name + object header address).
14#[derive(Debug, Clone)]
15pub struct GroupEntry {
16    /// Name of the child object.
17    pub name: String,
18    /// Address of the child's object header.
19    pub object_header_address: u64,
20    /// Cache type from the symbol table entry.
21    pub cache_type: u32,
22}
23
24/// Given a SymbolTableMessage, resolve all group children.
25pub fn resolve_v1_group_entries(
26    file_data: &[u8],
27    sym_table_msg: &SymbolTableMessage,
28    offset_size: u8,
29    length_size: u8,
30) -> Result<Vec<GroupEntry>, FormatError> {
31    // Parse local heap
32    let heap = LocalHeap::parse(
33        file_data,
34        sym_table_msg.local_heap_address as usize,
35        offset_size,
36        length_size,
37    )?;
38
39    // Collect all SNOD addresses from B-tree
40    let snod_addrs = collect_symbol_table_nodes(
41        file_data,
42        sym_table_msg.btree_address,
43        offset_size,
44        length_size,
45    )?;
46
47    let mut entries = Vec::new();
48    for snod_addr in snod_addrs {
49        let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?;
50        for entry in &snod.entries {
51            let name = heap.read_string(file_data, entry.link_name_offset)?;
52            entries.push(GroupEntry {
53                name,
54                object_header_address: entry.object_header_address,
55                cache_type: entry.cache_type,
56            });
57        }
58    }
59
60    Ok(entries)
61}
62
63/// Extract the SymbolTableMessage from an object header's messages.
64fn find_symbol_table_message(
65    obj_header: &ObjectHeader,
66    offset_size: u8,
67) -> Result<SymbolTableMessage, FormatError> {
68    for msg in &obj_header.messages {
69        if msg.msg_type == MessageType::SymbolTable {
70            return SymbolTableMessage::parse(&msg.data, offset_size);
71        }
72    }
73    Err(FormatError::PathNotFound(String::from(
74        "no symbol table message found in object header",
75    )))
76}
77
78/// Navigate a path like "group1/subgroup/dataset" from a root group.
79/// Returns the object header address of the target.
80pub fn resolve_path(
81    file_data: &[u8],
82    root_sym_table: &SymbolTableMessage,
83    path: &str,
84    offset_size: u8,
85    length_size: u8,
86) -> Result<u64, FormatError> {
87    let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
88    if components.is_empty() {
89        return Err(FormatError::PathNotFound(String::from(path)));
90    }
91
92    let mut current_sym_table = root_sym_table.clone();
93
94    for (i, component) in components.iter().enumerate() {
95        let entries =
96            resolve_v1_group_entries(file_data, &current_sym_table, offset_size, length_size)?;
97
98        let found = entries.iter().find(|e| e.name == *component);
99        match found {
100            Some(entry) => {
101                if i == components.len() - 1 {
102                    // Last component — return its address
103                    return Ok(entry.object_header_address);
104                }
105                // Not last — must be a group, parse its object header to get symbol table
106                let obj_header = ObjectHeader::parse(
107                    file_data,
108                    entry.object_header_address as usize,
109                    offset_size,
110                    length_size,
111                )?;
112                current_sym_table = find_symbol_table_message(&obj_header, offset_size)?;
113            }
114            None => {
115                return Err(FormatError::PathNotFound(String::from(*component)));
116            }
117        }
118    }
119
120    Err(FormatError::PathNotFound(String::from(path)))
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::btree_v1::BTreeV1Node;
127
128    // Helper to write an offset value into a buffer
129    fn write_off(buf: &mut Vec<u8>, val: u64, size: u8) {
130        match size {
131            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
132            8 => buf.extend_from_slice(&val.to_le_bytes()),
133            _ => panic!("test offset size"),
134        }
135    }
136
137    /// Build a minimal synthetic file with a group containing named children.
138    /// Returns (file_data, SymbolTableMessage).
139    fn build_synthetic_group(
140        children: &[(&str, u64, u32)], // (name, obj_header_addr, cache_type)
141        offset_size: u8,
142        length_size: u8,
143    ) -> (Vec<u8>, SymbolTableMessage) {
144        let os = offset_size as usize;
145        let ls = length_size as usize;
146
147        // Build local heap data segment (names)
148        let mut heap_data = Vec::new();
149        let mut name_offsets = Vec::new();
150        for (name, _, _) in children {
151            name_offsets.push(heap_data.len() as u64);
152            heap_data.extend_from_slice(name.as_bytes());
153            heap_data.push(0);
154        }
155        let heap_data_size = heap_data.len();
156
157        // Layout:
158        // 0: local heap header
159        // heap_header_end: heap data segment
160        // after heap data: SNOD
161        // after SNOD: B-tree leaf
162
163        let heap_offset = 0usize;
164        let heap_header_size = 8 + ls * 2 + os;
165        let heap_data_offset = heap_header_size;
166        let snod_offset = heap_data_offset + heap_data_size;
167        // Pad to nice offset
168        let snod_offset = (snod_offset + 7) & !7;
169
170        let entry_size = os + os + 4 + 4 + 16;
171        let snod_size = 8 + children.len() * entry_size;
172        let btree_offset = snod_offset + snod_size;
173        let btree_offset = (btree_offset + 7) & !7;
174
175        // B-tree: entries_used = 1 child (the SNOD), keys = [0, last_name_end]
176        let last_key = if children.is_empty() { 0u64 } else { heap_data_size as u64 };
177        let btree_header_size = 8 + os * 2; // sig + type + level + entries + siblings
178        let btree_keys_children = os + os + os; // key[0] + child[0] + key[1]
179        let total_size = btree_offset + btree_header_size + btree_keys_children + 64;
180
181        let mut file = vec![0u8; total_size];
182
183        // Write heap header
184        {
185            let mut pos = heap_offset;
186            file[pos..pos + 4].copy_from_slice(b"HEAP");
187            pos += 4;
188            file[pos] = 0; // version
189            pos += 4; // version(1) + reserved(3)
190            // data_segment_size
191            match length_size {
192                4 => file[pos..pos + 4].copy_from_slice(&(heap_data_size as u32).to_le_bytes()),
193                8 => file[pos..pos + 8].copy_from_slice(&(heap_data_size as u64).to_le_bytes()),
194                _ => {}
195            }
196            pos += ls;
197            // free_list_head_offset
198            match length_size {
199                4 => file[pos..pos + 4].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes()),
200                8 => file[pos..pos + 8].copy_from_slice(&0xFFFFFFFFFFFFFFFFu64.to_le_bytes()),
201                _ => {}
202            }
203            pos += ls;
204            // data_segment_address
205            match offset_size {
206                4 => file[pos..pos + 4]
207                    .copy_from_slice(&(heap_data_offset as u32).to_le_bytes()),
208                8 => file[pos..pos + 8]
209                    .copy_from_slice(&(heap_data_offset as u64).to_le_bytes()),
210                _ => {}
211            }
212        }
213
214        // Write heap data segment
215        file[heap_data_offset..heap_data_offset + heap_data_size].copy_from_slice(&heap_data);
216
217        // Write SNOD
218        {
219            let mut pos = snod_offset;
220            file[pos..pos + 4].copy_from_slice(b"SNOD");
221            pos += 4;
222            file[pos] = 1; // version
223            pos += 1;
224            pos += 1; // reserved
225            file[pos..pos + 2].copy_from_slice(&(children.len() as u16).to_le_bytes());
226            pos += 2;
227            for (idx, &(_, obj_addr, cache_type)) in children.iter().enumerate() {
228                // link_name_offset
229                match offset_size {
230                    4 => file[pos..pos + 4]
231                        .copy_from_slice(&(name_offsets[idx] as u32).to_le_bytes()),
232                    8 => file[pos..pos + 8].copy_from_slice(&name_offsets[idx].to_le_bytes()),
233                    _ => {}
234                }
235                pos += os;
236                // object_header_address
237                match offset_size {
238                    4 => file[pos..pos + 4].copy_from_slice(&(obj_addr as u32).to_le_bytes()),
239                    8 => file[pos..pos + 8].copy_from_slice(&obj_addr.to_le_bytes()),
240                    _ => {}
241                }
242                pos += os;
243                file[pos..pos + 4].copy_from_slice(&cache_type.to_le_bytes());
244                pos += 4;
245                pos += 4; // reserved
246                pos += 16; // scratch pad (zeros)
247            }
248        }
249
250        // Write B-tree (leaf, level 0, 1 entry pointing to SNOD)
251        {
252            let mut pos = btree_offset;
253            file[pos..pos + 4].copy_from_slice(b"TREE");
254            pos += 4;
255            file[pos] = 0; // type=group
256            pos += 1;
257            file[pos] = 0; // level=leaf
258            pos += 1;
259            file[pos..pos + 2].copy_from_slice(&1u16.to_le_bytes()); // entries_used=1
260            pos += 2;
261            // siblings = undefined
262            for _ in 0..2 {
263                match offset_size {
264                    4 => file[pos..pos + 4].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes()),
265                    8 => file[pos..pos + 8]
266                        .copy_from_slice(&0xFFFFFFFFFFFFFFFFu64.to_le_bytes()),
267                    _ => {}
268                }
269                pos += os;
270            }
271            // key[0]
272            match offset_size {
273                4 => file[pos..pos + 4].copy_from_slice(&0u32.to_le_bytes()),
274                8 => file[pos..pos + 8].copy_from_slice(&0u64.to_le_bytes()),
275                _ => {}
276            }
277            pos += os;
278            // child[0] = snod_offset
279            match offset_size {
280                4 => file[pos..pos + 4].copy_from_slice(&(snod_offset as u32).to_le_bytes()),
281                8 => file[pos..pos + 8].copy_from_slice(&(snod_offset as u64).to_le_bytes()),
282                _ => {}
283            }
284            pos += os;
285            // key[1]
286            match offset_size {
287                4 => file[pos..pos + 4].copy_from_slice(&(last_key as u32).to_le_bytes()),
288                8 => file[pos..pos + 8].copy_from_slice(&last_key.to_le_bytes()),
289                _ => {}
290            }
291        }
292
293        let msg = SymbolTableMessage {
294            btree_address: btree_offset as u64,
295            local_heap_address: heap_offset as u64,
296        };
297
298        (file, msg)
299    }
300
301    #[test]
302    fn resolve_entries_two_children() {
303        let (file, msg) = build_synthetic_group(
304            &[("alpha", 0x1000, 0), ("beta", 0x2000, 0)],
305            8,
306            8,
307        );
308        let entries = resolve_v1_group_entries(&file, &msg, 8, 8).unwrap();
309        assert_eq!(entries.len(), 2);
310        assert_eq!(entries[0].name, "alpha");
311        assert_eq!(entries[0].object_header_address, 0x1000);
312        assert_eq!(entries[1].name, "beta");
313        assert_eq!(entries[1].object_header_address, 0x2000);
314    }
315
316    #[test]
317    fn resolve_path_single_level() {
318        let (file, msg) = build_synthetic_group(
319            &[("child1", 0x3000, 0), ("child2", 0x4000, 0)],
320            8,
321            8,
322        );
323        let addr = resolve_path(&file, &msg, "child1", 8, 8).unwrap();
324        assert_eq!(addr, 0x3000);
325    }
326
327    #[test]
328    fn resolve_path_not_found() {
329        let (file, msg) = build_synthetic_group(&[("x", 0x100, 0)], 8, 8);
330        let err = resolve_path(&file, &msg, "nonexistent", 8, 8).unwrap_err();
331        assert!(matches!(err, FormatError::PathNotFound(_)));
332    }
333
334    // Helper to extract dataset components from an object header
335    fn extract_dataset(
336        file_data: &[u8],
337        hdr: &crate::object_header::ObjectHeader,
338        offset_size: u8,
339        length_size: u8,
340    ) -> (
341        crate::datatype::Datatype,
342        crate::dataspace::Dataspace,
343        crate::data_layout::DataLayout,
344    ) {
345        let dt_data = &hdr.messages.iter().find(|m| m.msg_type == MessageType::Datatype).unwrap().data;
346        let ds_data = &hdr.messages.iter().find(|m| m.msg_type == MessageType::Dataspace).unwrap().data;
347        let dl_data = &hdr.messages.iter().find(|m| m.msg_type == MessageType::DataLayout).unwrap().data;
348        let (dt, _) = crate::datatype::Datatype::parse(dt_data).unwrap();
349        let ds = crate::dataspace::Dataspace::parse(ds_data, length_size).unwrap();
350        let dl = crate::data_layout::DataLayout::parse(dl_data, offset_size, length_size).unwrap();
351        (dt, ds, dl)
352    }
353
354    fn get_root_sym_table(
355        file_data: &[u8],
356        sb: &crate::superblock::Superblock,
357    ) -> SymbolTableMessage {
358        let root_header = ObjectHeader::parse(
359            file_data, sb.root_group_address as usize, sb.offset_size, sb.length_size,
360        ).unwrap();
361        let sym_msg = root_header.messages.iter()
362            .find(|m| m.msg_type == MessageType::SymbolTable).unwrap();
363        SymbolTableMessage::parse(&sym_msg.data, sb.offset_size).unwrap()
364    }
365
366    // Integration tests with real HDF5 files
367
368    #[test]
369    fn integration_simple_dataset_full_traversal() {
370        let file_data: &[u8] = include_bytes!("../tests/fixtures/simple_dataset.h5");
371        let sig_offset = crate::signature::find_signature(file_data).unwrap();
372        let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap();
373        let root_sym = get_root_sym_table(file_data, &sb);
374
375        let entries = resolve_v1_group_entries(file_data, &root_sym, sb.offset_size, sb.length_size).unwrap();
376        let data_entry = entries.iter().find(|e| e.name == "data").expect("should have 'data'");
377
378        let hdr = ObjectHeader::parse(file_data, data_entry.object_header_address as usize, sb.offset_size, sb.length_size).unwrap();
379        let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
380        let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
381        let values = crate::data_read::read_as_f64(&raw, &dt).unwrap();
382        assert_eq!(values, vec![1.0, 2.0, 3.0]);
383    }
384
385    #[test]
386    fn integration_two_groups_group1_values() {
387        let file_data: &[u8] = include_bytes!("../tests/fixtures/two_groups.h5");
388        let sig_offset = crate::signature::find_signature(file_data).unwrap();
389        let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap();
390        let root_sym = get_root_sym_table(file_data, &sb);
391
392        let addr = resolve_path(file_data, &root_sym, "group1/values", sb.offset_size, sb.length_size).unwrap();
393        let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
394        let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
395        let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
396        let values = crate::data_read::read_as_i32(&raw, &dt).unwrap();
397        assert_eq!(values, vec![10, 20, 30]);
398    }
399
400    #[test]
401    fn integration_two_groups_group2_temps() {
402        let file_data: &[u8] = include_bytes!("../tests/fixtures/two_groups.h5");
403        let sig_offset = crate::signature::find_signature(file_data).unwrap();
404        let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap();
405        let root_sym = get_root_sym_table(file_data, &sb);
406
407        let addr = resolve_path(file_data, &root_sym, "group2/temps", sb.offset_size, sb.length_size).unwrap();
408        let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
409        let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
410        let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
411        let values = crate::data_read::read_as_f32(&raw, &dt).unwrap();
412        assert!((values[0] - 98.6).abs() < 0.01);
413        assert!((values[1] - 37.0).abs() < 0.01);
414    }
415
416    #[test]
417    fn integration_nested_groups() {
418        let file_data: &[u8] = include_bytes!("../tests/fixtures/nested_groups.h5");
419        let sig_offset = crate::signature::find_signature(file_data).unwrap();
420        let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap();
421        let root_sym = get_root_sym_table(file_data, &sb);
422
423        let addr = resolve_path(file_data, &root_sym, "a/b/c/deep", sb.offset_size, sb.length_size).unwrap();
424        let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
425        let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
426        let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
427        let values = crate::data_read::read_as_f64(&raw, &dt).unwrap();
428        assert_eq!(values, vec![42.0]);
429    }
430}