- Path resolution follows soft links in both old-style (symbol table, cache
type 2) and new-style (compact and dense Link message) groups: absolute and
relative targets, links to groups, links through links, with a depth limit
so a link cycle is NestingDepthExceeded rather than a hang. A dangling link
reports the target it could not find. Previously every soft link was
PathNotFound.
- An external link is FormatError::ExternalLinkUnsupported { filename,
object_path } instead of a misleading PathNotFound.
- Message 0x0007 (External Data Files) is now a known MessageType, and a
dataset carrying it is FormatError::ExternalDataFilesUnsupported. Such a
dataset has no data address in this file, so it would otherwise be read as
"never written" and answered with fill values — wrong data, no error.
- Dense link iteration is shared between hard-link listing and the new
symbolic-link lookup; entry listing behaviour is unchanged.
- h5py interop test for both libver settings.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
529 lines
19 KiB
Rust
529 lines
19 KiB
Rust
//! 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<Vec<GroupEntry>, 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<Option<String>, 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<SymbolTableMessage, FormatError> {
|
|
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<u64, FormatError> {
|
|
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<u8>, 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<u8>, 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]);
|
|
}
|
|
}
|