//! V1 group traversal: resolve group children and navigate paths. #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; use crate::btree_v1::collect_symbol_table_nodes; use crate::error::FormatError; use crate::local_heap::LocalHeap; use crate::message_type::MessageType; use crate::object_header::ObjectHeader; use crate::symbol_table::{SymbolTableMessage, SymbolTableNode}; /// A resolved group entry (child name + object header address). #[derive(Debug, Clone)] pub struct GroupEntry { /// Name of the child object. pub name: String, /// Address of the child's object header. pub object_header_address: u64, /// Cache type from the symbol table entry. pub cache_type: u32, } /// Given a SymbolTableMessage, resolve all group children. pub fn resolve_v1_group_entries( file_data: &[u8], sym_table_msg: &SymbolTableMessage, offset_size: u8, length_size: u8, ) -> Result, FormatError> { // Parse local heap let heap = LocalHeap::parse( file_data, sym_table_msg.local_heap_address as usize, offset_size, length_size, )?; // Collect all SNOD addresses from B-tree let snod_addrs = collect_symbol_table_nodes( file_data, sym_table_msg.btree_address, offset_size, length_size, )?; let mut entries = Vec::new(); for snod_addr in snod_addrs { let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?; for entry in &snod.entries { let name = heap.read_string(file_data, entry.link_name_offset)?; entries.push(GroupEntry { name, object_header_address: entry.object_header_address, cache_type: entry.cache_type, }); } } Ok(entries) } /// Symbol table cache type for a soft link: the scratch pad's first four bytes /// are the local-heap offset of the link's target path, and the entry's object /// header address is undefined. const CACHE_TYPE_SOFT_LINK: u32 = 2; /// The target path of the soft link called `name` in a v1 group, if any. pub fn find_v1_soft_link( file_data: &[u8], sym_table_msg: &SymbolTableMessage, name: &str, offset_size: u8, length_size: u8, ) -> Result, FormatError> { let heap = LocalHeap::parse( file_data, sym_table_msg.local_heap_address as usize, offset_size, length_size, )?; let snod_addrs = collect_symbol_table_nodes( file_data, sym_table_msg.btree_address, offset_size, length_size, )?; for snod_addr in snod_addrs { let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?; for entry in &snod.entries { if entry.cache_type != CACHE_TYPE_SOFT_LINK { continue; } if heap.read_string(file_data, entry.link_name_offset)? != name { continue; } let value_offset = u32::from_le_bytes([ entry.scratch_pad[0], entry.scratch_pad[1], entry.scratch_pad[2], entry.scratch_pad[3], ]); return heap .read_string(file_data, u64::from(value_offset)) .map(Some); } } Ok(None) } /// Extract the SymbolTableMessage from an object header's messages. fn find_symbol_table_message( obj_header: &ObjectHeader, offset_size: u8, ) -> Result { for msg in &obj_header.messages { if msg.msg_type == MessageType::SymbolTable { return SymbolTableMessage::parse(&msg.data, offset_size); } } Err(FormatError::PathNotFound(String::from( "no symbol table message found in object header", ))) } /// Navigate a path like "group1/subgroup/dataset" from a root group. /// Returns the object header address of the target. pub fn resolve_path( file_data: &[u8], root_sym_table: &SymbolTableMessage, path: &str, offset_size: u8, length_size: u8, ) -> Result { let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); if components.is_empty() { return Err(FormatError::PathNotFound(String::from(path))); } let mut current_sym_table = root_sym_table.clone(); for (i, component) in components.iter().enumerate() { let entries = resolve_v1_group_entries(file_data, ¤t_sym_table, offset_size, length_size)?; let found = entries.iter().find(|e| e.name == *component); match found { Some(entry) => { if i == components.len() - 1 { // Last component — return its address return Ok(entry.object_header_address); } // Not last — must be a group, parse its object header to get symbol table let obj_header = ObjectHeader::parse( file_data, entry.object_header_address as usize, offset_size, length_size, )?; current_sym_table = find_symbol_table_message(&obj_header, offset_size)?; } None => { return Err(FormatError::PathNotFound(String::from(*component))); } } } Err(FormatError::PathNotFound(String::from(path))) } #[cfg(test)] mod tests { use super::*; // Helper to write an offset value into a buffer #[allow(dead_code)] fn write_off(buf: &mut Vec, val: u64, size: u8) { match size { 4 => buf.extend_from_slice(&(val as u32).to_le_bytes()), 8 => buf.extend_from_slice(&val.to_le_bytes()), _ => panic!("test offset size"), } } /// Build a minimal synthetic file with a group containing named children. /// Returns (file_data, SymbolTableMessage). fn build_synthetic_group( children: &[(&str, u64, u32)], // (name, obj_header_addr, cache_type) offset_size: u8, length_size: u8, ) -> (Vec, SymbolTableMessage) { let os = offset_size as usize; let ls = length_size as usize; // Build local heap data segment (names) let mut heap_data = Vec::new(); let mut name_offsets = Vec::new(); for (name, _, _) in children { name_offsets.push(heap_data.len() as u64); heap_data.extend_from_slice(name.as_bytes()); heap_data.push(0); } let heap_data_size = heap_data.len(); // Layout: // 0: local heap header // heap_header_end: heap data segment // after heap data: SNOD // after SNOD: B-tree leaf let heap_offset = 0usize; let heap_header_size = 8 + ls * 2 + os; let heap_data_offset = heap_header_size; let snod_offset = heap_data_offset + heap_data_size; // Pad to nice offset let snod_offset = (snod_offset + 7) & !7; let entry_size = os + os + 4 + 4 + 16; let snod_size = 8 + children.len() * entry_size; let btree_offset = snod_offset + snod_size; let btree_offset = (btree_offset + 7) & !7; // B-tree: entries_used = 1 child (the SNOD), keys = [0, last_name_end] let last_key = if children.is_empty() { 0u64 } else { heap_data_size as u64 }; let btree_header_size = 8 + os * 2; // sig + type + level + entries + siblings let btree_keys_children = os + os + os; // key[0] + child[0] + key[1] let total_size = btree_offset + btree_header_size + btree_keys_children + 64; let mut file = vec![0u8; total_size]; // Write heap header { let mut pos = heap_offset; file[pos..pos + 4].copy_from_slice(b"HEAP"); pos += 4; file[pos] = 0; // version pos += 4; // version(1) + reserved(3) // data_segment_size match length_size { 4 => file[pos..pos + 4].copy_from_slice(&(heap_data_size as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&(heap_data_size as u64).to_le_bytes()), _ => {} } pos += ls; // free_list_head_offset match length_size { 4 => file[pos..pos + 4].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&0xFFFFFFFFFFFFFFFFu64.to_le_bytes()), _ => {} } pos += ls; // data_segment_address match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&(heap_data_offset as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&(heap_data_offset as u64).to_le_bytes()), _ => {} } } // Write heap data segment file[heap_data_offset..heap_data_offset + heap_data_size].copy_from_slice(&heap_data); // Write SNOD { let mut pos = snod_offset; file[pos..pos + 4].copy_from_slice(b"SNOD"); pos += 4; file[pos] = 1; // version pos += 1; pos += 1; // reserved file[pos..pos + 2].copy_from_slice(&(children.len() as u16).to_le_bytes()); pos += 2; for (idx, &(_, obj_addr, cache_type)) in children.iter().enumerate() { // link_name_offset match offset_size { 4 => file[pos..pos + 4] .copy_from_slice(&(name_offsets[idx] as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&name_offsets[idx].to_le_bytes()), _ => {} } pos += os; // object_header_address match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&(obj_addr as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&obj_addr.to_le_bytes()), _ => {} } pos += os; file[pos..pos + 4].copy_from_slice(&cache_type.to_le_bytes()); pos += 4; pos += 4; // reserved pos += 16; // scratch pad (zeros) } } // Write B-tree (leaf, level 0, 1 entry pointing to SNOD) { let mut pos = btree_offset; file[pos..pos + 4].copy_from_slice(b"TREE"); pos += 4; file[pos] = 0; // type=group pos += 1; file[pos] = 0; // level=leaf pos += 1; file[pos..pos + 2].copy_from_slice(&1u16.to_le_bytes()); // entries_used=1 pos += 2; // siblings = undefined for _ in 0..2 { match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&0xFFFFFFFFFFFFFFFFu64.to_le_bytes()), _ => {} } pos += os; } // key[0] match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&0u32.to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&0u64.to_le_bytes()), _ => {} } pos += os; // child[0] = snod_offset match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&(snod_offset as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&(snod_offset as u64).to_le_bytes()), _ => {} } pos += os; // key[1] match offset_size { 4 => file[pos..pos + 4].copy_from_slice(&(last_key as u32).to_le_bytes()), 8 => file[pos..pos + 8].copy_from_slice(&last_key.to_le_bytes()), _ => {} } } let msg = SymbolTableMessage { btree_address: btree_offset as u64, local_heap_address: heap_offset as u64, }; (file, msg) } #[test] fn resolve_entries_two_children() { let (file, msg) = build_synthetic_group(&[("alpha", 0x1000, 0), ("beta", 0x2000, 0)], 8, 8); let entries = resolve_v1_group_entries(&file, &msg, 8, 8).unwrap(); assert_eq!(entries.len(), 2); assert_eq!(entries[0].name, "alpha"); assert_eq!(entries[0].object_header_address, 0x1000); assert_eq!(entries[1].name, "beta"); assert_eq!(entries[1].object_header_address, 0x2000); } #[test] fn resolve_path_single_level() { let (file, msg) = build_synthetic_group(&[("child1", 0x3000, 0), ("child2", 0x4000, 0)], 8, 8); let addr = resolve_path(&file, &msg, "child1", 8, 8).unwrap(); assert_eq!(addr, 0x3000); } #[test] fn resolve_path_not_found() { let (file, msg) = build_synthetic_group(&[("x", 0x100, 0)], 8, 8); let err = resolve_path(&file, &msg, "nonexistent", 8, 8).unwrap_err(); assert!(matches!(err, FormatError::PathNotFound(_))); } // Helper to extract dataset components from an object header fn extract_dataset( _file_data: &[u8], hdr: &crate::object_header::ObjectHeader, offset_size: u8, length_size: u8, ) -> ( crate::datatype::Datatype, crate::dataspace::Dataspace, crate::data_layout::DataLayout, ) { let dt_data = &hdr .messages .iter() .find(|m| m.msg_type == MessageType::Datatype) .unwrap() .data; let ds_data = &hdr .messages .iter() .find(|m| m.msg_type == MessageType::Dataspace) .unwrap() .data; let dl_data = &hdr .messages .iter() .find(|m| m.msg_type == MessageType::DataLayout) .unwrap() .data; let (dt, _) = crate::datatype::Datatype::parse(dt_data).unwrap(); let ds = crate::dataspace::Dataspace::parse(ds_data, length_size).unwrap(); let dl = crate::data_layout::DataLayout::parse(dl_data, offset_size, length_size).unwrap(); (dt, ds, dl) } fn get_root_sym_table( file_data: &[u8], sb: &crate::superblock::Superblock, ) -> SymbolTableMessage { let root_header = ObjectHeader::parse( file_data, sb.root_group_address as usize, sb.offset_size, sb.length_size, ) .unwrap(); let sym_msg = root_header .messages .iter() .find(|m| m.msg_type == MessageType::SymbolTable) .unwrap(); SymbolTableMessage::parse(&sym_msg.data, sb.offset_size).unwrap() } // Integration tests with real HDF5 files #[test] fn integration_simple_dataset_full_traversal() { let file_data: &[u8] = include_bytes!("../tests/fixtures/simple_dataset.h5"); let sig_offset = crate::signature::find_signature(file_data).unwrap(); let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap(); let root_sym = get_root_sym_table(file_data, &sb); let entries = resolve_v1_group_entries(file_data, &root_sym, sb.offset_size, sb.length_size).unwrap(); let data_entry = entries .iter() .find(|e| e.name == "data") .expect("should have 'data'"); let hdr = ObjectHeader::parse( file_data, data_entry.object_header_address as usize, sb.offset_size, sb.length_size, ) .unwrap(); let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size); let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap(); let values = crate::data_read::read_as_f64(&raw, &dt).unwrap(); assert_eq!(values, vec![1.0, 2.0, 3.0]); } #[test] fn integration_two_groups_group1_values() { let file_data: &[u8] = include_bytes!("../tests/fixtures/two_groups.h5"); let sig_offset = crate::signature::find_signature(file_data).unwrap(); let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap(); let root_sym = get_root_sym_table(file_data, &sb); let addr = resolve_path( file_data, &root_sym, "group1/values", sb.offset_size, sb.length_size, ) .unwrap(); let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap(); let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size); let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap(); let values = crate::data_read::read_as_i32(&raw, &dt).unwrap(); assert_eq!(values, vec![10, 20, 30]); } #[test] fn integration_two_groups_group2_temps() { let file_data: &[u8] = include_bytes!("../tests/fixtures/two_groups.h5"); let sig_offset = crate::signature::find_signature(file_data).unwrap(); let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap(); let root_sym = get_root_sym_table(file_data, &sb); let addr = resolve_path( file_data, &root_sym, "group2/temps", sb.offset_size, sb.length_size, ) .unwrap(); let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap(); let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size); let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap(); let values = crate::data_read::read_as_f32(&raw, &dt).unwrap(); assert!((values[0] - 98.6).abs() < 0.01); assert!((values[1] - 37.0).abs() < 0.01); } #[test] fn integration_nested_groups() { let file_data: &[u8] = include_bytes!("../tests/fixtures/nested_groups.h5"); let sig_offset = crate::signature::find_signature(file_data).unwrap(); let sb = crate::superblock::Superblock::parse(file_data, sig_offset).unwrap(); let root_sym = get_root_sym_table(file_data, &sb); let addr = resolve_path( file_data, &root_sym, "a/b/c/deep", sb.offset_size, sb.length_size, ) .unwrap(); let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap(); let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size); let raw = crate::data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap(); let values = crate::data_read::read_as_f64(&raw, &dt).unwrap(); assert_eq!(values, vec![42.0]); } }