Files
clawhdf5/crates/clawhdf5-format/src/group_v2.rs
T
osobhandClaude Opus 5.5 04a7f6f6c7 read: of two links with one name, the first wins everywhere
A valid group has one link per name, but a damaged or hand-made one can
have two. resolve_child followed the first soft link of the name, the
listing skipped a dangling one and listed the name via a later link, and
path resolution followed the last symbolic link: three answers. All now
take the first link of the name (header message order in a compact group,
name index order in a dense one) and ignore the rest, even if the first
dangles. That is libhdf5's rule for compact groups (H5G__compact_lookup
stops at the first Link message); h5py opens nothing for a dangling first
link although a later one resolves. For a dense group libhdf5
binary-searches the index and may land on another of several exact
duplicates; documented on first_link_named. find_symbolic_link's v2 branch
was dead (only v1 groups reach it) and is now v1-only.

Test: an h5py compact group with soft links dup_A (dangling, or to /d) and
dup_B (the other), dup_B renamed to dup_A in the header and re-checksummed.
Lookup, path and listing through all three readers match h5py for both
orders. With the old group_v2.rs the path lookup returned 42 where h5py
opens nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 14:11:47 -05:00

819 lines
30 KiB
Rust

//! V2 group traversal: resolve group children and navigate paths.
//!
//! Handles both compact storage (Link messages in object header) and
//! dense storage (fractal heap + B-tree v2).
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeSet;
#[cfg(feature = "std")]
use std::collections::BTreeSet;
use crate::addr::to_usize;
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records, find_btree_v2_records};
use crate::checksum::jenkins_lookup3;
use crate::error::FormatError;
use crate::fractal_heap::FractalHeapHeader;
use crate::group_v1::{self, GroupEntry};
use crate::link_info::LinkInfoMessage;
use crate::link_message::{LinkMessage, LinkTarget};
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::superblock::Superblock;
use crate::symbol_table::SymbolTableMessage;
/// Resolve v2 group entries from an object header.
///
/// Handles both compact (Link messages) and dense (fractal heap + B-tree v2) storage.
pub fn resolve_v2_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
// Look for Link Info message to determine storage type
let link_info = find_link_info(object_header, offset_size)?;
if let Some(fh_addr) = link_info.fractal_heap_address {
// Dense storage
resolve_dense_entries(file_data, &link_info, fh_addr, offset_size, length_size)
} else {
// Compact storage: links are stored directly as Link messages
resolve_compact_entries(object_header, offset_size)
}
}
/// First user-defined link type (HDF5 reserves 2-63; 64 is external).
const FIRST_USER_DEFINED_LINK_TYPE: u8 = 65;
/// Parse a Link message, or `None` for a user-defined link (type 65-255).
///
/// A user-defined link's target is only meaningful to the application that
/// registered its class, so, like libhdf5 without that class, we cannot
/// follow it. Leaving it out lets the rest of the group be listed and
/// resolved instead of one such link failing the whole group; reserved
/// types (2-63) are still an error.
fn parse_link(data: &[u8], offset_size: u8) -> Result<Option<LinkMessage>, FormatError> {
match LinkMessage::parse(data, offset_size) {
Ok(link) => Ok(Some(link)),
Err(FormatError::InvalidLinkType(t)) if t >= FIRST_USER_DEFINED_LINK_TYPE => Ok(None),
Err(e) => Err(e),
}
}
/// Extract link entries from Link messages directly in the object header (compact storage).
fn resolve_compact_entries(
object_header: &ObjectHeader,
offset_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let mut entries = Vec::new();
for msg in &object_header.messages {
if msg.msg_type == MessageType::Link {
let Some(link) = parse_link(&msg.data, offset_size)? else {
continue;
};
if let LinkTarget::Hard {
object_header_address,
} = link.link_target
{
entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
});
}
// Skip soft and external links for path resolution
}
}
Ok(entries)
}
/// Visit every link in dense storage (fractal heap + B-tree v2 name index).
fn for_each_dense_link(
file_data: &[u8],
link_info: &LinkInfoMessage,
fh_addr: u64,
offset_size: u8,
length_size: u8,
mut visit: impl FnMut(LinkMessage),
) -> Result<(), FormatError> {
// Parse fractal heap
let fh = FractalHeapHeader::parse(file_data, to_usize(fh_addr)?, offset_size, length_size)?;
// Parse B-tree v2 for name index
let btree_addr = link_info
.btree_name_index_address
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
let btree_hdr =
BTreeV2Header::parse(file_data, to_usize(btree_addr)?, offset_size, length_size)?;
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
for record in &records {
// For type 5 (name index): hash(4) + heap_id(heap_id_length)
// For type 6 (creation order): creation_order(8) + heap_id(heap_id_length)
let id_offset = if btree_hdr.tree_type == 5 {
4 // skip hash
} else {
8 // skip creation_order
};
if record.data.len() < id_offset + fh.heap_id_length as usize {
continue;
}
let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize];
// Read managed object from fractal heap
let link_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
if let Some(link) = parse_link(&link_data, offset_size)? {
visit(link);
}
}
Ok(())
}
/// Resolve entries from dense storage (fractal heap + B-tree v2).
fn resolve_dense_entries(
file_data: &[u8],
link_info: &LinkInfoMessage,
fh_addr: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let mut entries = Vec::new();
for_each_dense_link(
file_data,
link_info,
fh_addr,
offset_size,
length_size,
|link| {
if let LinkTarget::Hard {
object_header_address,
} = link.link_target
{
entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
});
}
},
)?;
Ok(entries)
}
/// The soft link called `name` in a v1 (symbol table) group, if there is
/// one. Hard links are what `resolve_group_entries` returns; this is
/// consulted only when a path component isn't among them.
fn find_v1_symbolic_link(
file_data: &[u8],
object_header: &ObjectHeader,
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Option<LinkTarget>, FormatError> {
let Some(sym_msg) = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
else {
return Ok(None);
};
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::find_v1_soft_link(file_data, &stm, name, offset_size, length_size)
.map(|target| target.map(|target_path| LinkTarget::Soft { target_path }))
}
/// B-tree v2 record type of a dense group's link name index.
const LINK_NAME_INDEX: u8 = 5;
/// The links called `name` in a v2 group (a valid group has at most one),
/// in storage order: header message order for a compact group, name index
/// order for a dense one.
///
/// In dense storage the link name index (a v2 B-tree of lookup3 name
/// hashes, record type 5) is descended to the records with the name's hash,
/// and only their links are read from the heap — O(log n) instead of every
/// link. libhdf5 orders records with equal hashes by name; all of them are
/// read and compared here, so that order does not matter. An index of
/// another type is scanned in full.
fn links_named(
file_data: &[u8],
object_header: &ObjectHeader,
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Vec<LinkMessage>, FormatError> {
let mut found = Vec::new();
let link_info = find_link_info(object_header, offset_size)?;
let Some(fh_addr) = link_info.fractal_heap_address else {
for msg in &object_header.messages {
if msg.msg_type == MessageType::Link
&& let Some(link) = parse_link(&msg.data, offset_size)?
&& link.name == name
{
found.push(link);
}
}
return Ok(found);
};
let fh = FractalHeapHeader::parse(file_data, to_usize(fh_addr)?, offset_size, length_size)?;
let btree_addr = link_info
.btree_name_index_address
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
let btree_hdr =
BTreeV2Header::parse(file_data, to_usize(btree_addr)?, offset_size, length_size)?;
if btree_hdr.tree_type != LINK_NAME_INDEX {
for_each_dense_link(
file_data,
&link_info,
fh_addr,
offset_size,
length_size,
|link| {
if link.name == name {
found.push(link);
}
},
)?;
return Ok(found);
}
// Record: hash(4) + heap ID.
let hash = jenkins_lookup3(name.as_bytes());
let records = find_btree_v2_records(file_data, &btree_hdr, offset_size, &mut |r| {
match r.get(..4) {
Some(h) => u32::from_le_bytes([h[0], h[1], h[2], h[3]]).cmp(&hash),
// Too short to hold a hash (a corrupt record size): never a match.
None => core::cmp::Ordering::Less,
}
})?;
let id_len = usize::from(fh.heap_id_length);
for record in &records {
let Some(id_bytes) = record.data.get(4..4 + id_len) else {
continue;
};
let link_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
if let Some(link) = parse_link(&link_data, offset_size)?
&& link.name == name
{
found.push(link);
}
}
Ok(found)
}
/// The link called `name` in a v2 group, if any.
///
/// A valid group has at most one; libhdf5 cannot create two. If a damaged
/// or hand-made group has several, the first wins and the rest are
/// ignored, whatever their kind and even if the first cannot be followed.
/// That is libhdf5's rule for a compact group (`H5G__compact_lookup` stops
/// at the first Link message of that name; h5py then fails to open a
/// dangling first link although a later one resolves). For a dense group
/// "first" is first in name index order; libhdf5 binary-searches the index
/// and may land on another of several exact duplicates. The listing
/// ([`resolve_group_children`]), [`resolve_child`] and path resolution all
/// apply this rule, so they agree.
fn first_link_named(
file_data: &[u8],
object_header: &ObjectHeader,
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Option<LinkMessage>, FormatError> {
Ok(
links_named(file_data, object_header, name, offset_size, length_size)?
.into_iter()
.next(),
)
}
/// The link [`resolve_path_any`] follows for one path component `name` of
/// the group with header `object_header`: a hard link (as `Hard`), else a
/// soft or external link of that name, else `None`. Fails with
/// `PathNotFound` if the object is not a group.
fn lookup_link(
file_data: &[u8],
object_header: &ObjectHeader,
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Option<LinkTarget>, FormatError> {
if is_v1_group(object_header) {
let entries = resolve_group_entries(file_data, object_header, offset_size, length_size)?;
if let Some(e) = entries
.iter()
.find(|e| e.name == name && e.object_header_address != u64::MAX)
{
return Ok(Some(LinkTarget::Hard {
object_header_address: e.object_header_address,
}));
}
return find_v1_symbolic_link(file_data, object_header, name, offset_size, length_size);
}
if !is_v2_group(object_header) {
return Err(FormatError::PathNotFound(String::from(
"object header is not a group",
)));
}
Ok(
first_link_named(file_data, object_header, name, offset_size, length_size)?
.map(|link| link.link_target)
.filter(|t| {
!matches!(
t,
LinkTarget::Hard {
object_header_address: u64::MAX
}
)
}),
)
}
/// The object header address of the child called `name` of the group at
/// `group_address`: the address [`resolve_group_children`] lists under that
/// name, or `PathNotFound` if it lists none.
///
/// A dense group's child is found through its link name index (see
/// [`links_named`]) and only the named link is read and, if it is a soft
/// link, followed — not every link in the group. A v1 group is listed.
pub fn resolve_child(
file_data: &[u8],
superblock: &Superblock,
group_address: u64,
name: &str,
) -> Result<u64, FormatError> {
let os = superblock.offset_size;
let ls = superblock.length_size;
let not_found = || FormatError::PathNotFound(String::from(name));
let header = ObjectHeader::parse(file_data, to_usize(group_address)?, os, ls)?;
if !is_v2_group(&header) || is_v1_group(&header) {
return resolve_group_children(file_data, superblock, group_address)?
.into_iter()
.find(|e| e.name == name)
.map(|e| e.object_header_address)
.ok_or_else(not_found);
}
// The first link of that name only, as the listing (see
// `first_link_named`).
match first_link_named(file_data, &header, name, os, ls)?.map(|l| l.link_target) {
Some(LinkTarget::Hard {
object_header_address,
}) => Ok(object_header_address),
Some(LinkTarget::Soft { target_path }) => {
match resolve_path_from(file_data, superblock, group_address, &target_path) {
// Left out of the listing: dangling, cyclic, or in another file.
Err(
FormatError::PathNotFound(_)
| FormatError::NestingDepthExceeded
| FormatError::ExternalLinkUnsupported { .. },
) => Err(not_found()),
other => other,
}
}
Some(LinkTarget::External { .. }) | None => Err(not_found()),
}
}
/// Find and parse the Link Info message from an object header.
fn find_link_info(
object_header: &ObjectHeader,
offset_size: u8,
) -> Result<LinkInfoMessage, FormatError> {
for msg in &object_header.messages {
if msg.msg_type == MessageType::LinkInfo {
return LinkInfoMessage::parse(&msg.data, offset_size);
}
}
// No Link Info message — might have direct Link messages
// Return a "compact" link info with no fractal heap
Ok(LinkInfoMessage {
max_creation_order: None,
fractal_heap_address: None,
btree_name_index_address: None,
btree_creation_order_address: None,
})
}
/// Detect whether an object header represents a v1 group, v2 group, or neither.
fn is_v2_group(object_header: &ObjectHeader) -> bool {
object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::LinkInfo || m.msg_type == MessageType::Link)
}
fn is_v1_group(object_header: &ObjectHeader) -> bool {
object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::SymbolTable)
}
/// Unified path resolution that works for both v1 and v2 groups.
///
/// Detects group version from object header messages and dispatches accordingly.
pub fn resolve_path_any(
file_data: &[u8],
superblock: &Superblock,
path: &str,
) -> Result<u64, FormatError> {
resolve_path_following_links(
file_data,
superblock,
superblock.root_group_address,
path,
0,
)
}
/// Resolve `path` relative to the group at `group_address` (an absolute path
/// starts at the root group instead), following soft links. This is how a
/// relative soft link's target is resolved: from the group holding the link.
pub fn resolve_path_from(
file_data: &[u8],
superblock: &Superblock,
group_address: u64,
path: &str,
) -> Result<u64, FormatError> {
let start = if path.starts_with('/') {
superblock.root_group_address
} else {
group_address
};
resolve_path_following_links(file_data, superblock, start, path, 0)
}
/// The children of the group at `group_address` that can be opened, as h5py
/// lists them: hard links, and soft links resolved to the object they point
/// at (under the soft link's own name). Links that cannot be followed are
/// left out rather than failing the listing — a dangling or cyclic soft link
/// (h5py lists its name but cannot open it), an external link (another
/// file), and a user-defined link. An object header that is not a group has
/// no children.
///
/// Any other error, such as a corrupt structure met while resolving a soft
/// link, is returned.
pub fn resolve_group_children(
file_data: &[u8],
superblock: &Superblock,
group_address: u64,
) -> Result<Vec<GroupEntry>, FormatError> {
let os = superblock.offset_size;
let ls = superblock.length_size;
let header = ObjectHeader::parse(file_data, to_usize(group_address)?, os, ls)?;
let mut entries = Vec::new();
let mut soft = Vec::new();
if is_v1_group(&header) {
let sym_msg = header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, os)?;
let all = group_v1::resolve_v1_group_entries(file_data, &stm, os, ls)?;
if all.iter().any(group_v1::is_v1_soft_link) {
soft = group_v1::v1_soft_links(file_data, &stm, os, ls)?;
}
entries.extend(all.into_iter().filter(|e| !group_v1::is_v1_soft_link(e)));
} else if is_v2_group(&header) {
// Only the first link of each name counts (see `first_link_named`).
let mut seen = BTreeSet::new();
let mut visit = |link: LinkMessage| {
if !seen.insert(link.name.clone()) {
return;
}
match link.link_target {
LinkTarget::Hard {
object_header_address,
} => entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
}),
LinkTarget::Soft { target_path } => soft.push((link.name, target_path)),
LinkTarget::External { .. } => {}
}
};
let link_info = find_link_info(&header, os)?;
if let Some(fh_addr) = link_info.fractal_heap_address {
for_each_dense_link(file_data, &link_info, fh_addr, os, ls, visit)?;
} else {
for msg in &header.messages {
if msg.msg_type == MessageType::Link
&& let Some(link) = parse_link(&msg.data, os)?
{
visit(link);
}
}
}
}
for (name, target) in soft {
match resolve_path_from(file_data, superblock, group_address, &target) {
Ok(object_header_address) => entries.push(GroupEntry {
name,
object_header_address,
cache_type: 0,
}),
// Dangling, cyclic, or ending in another file: not openable here.
Err(
FormatError::PathNotFound(_)
| FormatError::NestingDepthExceeded
| FormatError::ExternalLinkUnsupported { .. },
) => {}
Err(e) => return Err(e),
}
}
Ok(entries)
}
/// Soft links followed while resolving one path. Guards against link cycles
/// (`a -> b -> a`), which are legal to create.
const MAX_SOFT_LINK_DEPTH: u8 = 16;
/// Walk `path` from the group at `start`, following soft links.
fn resolve_path_following_links(
file_data: &[u8],
superblock: &Superblock,
start: u64,
path: &str,
depth: u8,
) -> Result<u64, FormatError> {
let components: Vec<&str> = path
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.collect();
if components.is_empty() {
return Ok(start);
}
let os = superblock.offset_size;
let ls = superblock.length_size;
let mut current_addr = start;
let mut current_header = ObjectHeader::parse(file_data, to_usize(start)?, os, ls)?;
for (i, component) in components.iter().enumerate() {
match lookup_link(file_data, &current_header, component, os, ls)? {
Some(LinkTarget::Hard {
object_header_address,
}) => {
if i == components.len() - 1 {
return Ok(object_header_address);
}
current_addr = object_header_address;
current_header = ObjectHeader::parse(file_data, to_usize(current_addr)?, os, ls)?;
}
found => {
return match found {
Some(LinkTarget::Soft { target_path }) => {
if depth >= MAX_SOFT_LINK_DEPTH {
return Err(FormatError::NestingDepthExceeded);
}
// A relative target is relative to the group holding
// the link; then the rest of the original path.
let from = if target_path.starts_with('/') {
superblock.root_group_address
} else {
current_addr
};
let mut full = target_path;
for rest in &components[i + 1..] {
full.push('/');
full.push_str(rest);
}
resolve_path_following_links(file_data, superblock, from, &full, depth + 1)
}
Some(LinkTarget::External {
filename,
object_path,
}) => Err(FormatError::ExternalLinkUnsupported {
filename,
object_path,
}),
_ => Err(FormatError::PathNotFound(String::from(*component))),
};
}
}
}
Ok(current_addr)
}
/// Resolve group entries from an object header, auto-detecting v1 vs v2.
fn resolve_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
if is_v1_group(object_header) {
// v1: find SymbolTableMessage and use existing v1 code
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
// A lookup: an entry with an empty name (which fails a listing) is
// skipped by the name comparison, as in libhdf5.
group_v1::v1_group_entries(file_data, &stm, offset_size, length_size)
} else if is_v2_group(object_header) {
resolve_v2_group_entries(file_data, object_header, offset_size, length_size)
} else {
Err(FormatError::PathNotFound(String::from(
"object header is not a group",
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_layout::DataLayout;
use crate::data_read;
use crate::dataspace::Dataspace;
use crate::datatype::Datatype;
use crate::signature;
fn extract_dataset(
_file_data: &[u8],
hdr: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> (Datatype, Dataspace, 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, _) = Datatype::parse(dt_data).unwrap();
let ds = Dataspace::parse(ds_data, length_size).unwrap();
let dl = DataLayout::parse(dl_data, offset_size, length_size).unwrap();
(dt, ds, dl)
}
#[test]
fn compact_storage_link_messages() {
// Build a v2 object header with Link messages (compact storage)
// We'll test with the actual v2_groups.h5 file since building synthetic v2 headers
// with proper checksums is complex.
// Instead, test the resolve_compact_entries path with a simple object header
let link_data = {
// Build a Link message: hard link, name="test", addr=0x1000
let mut d = Vec::new();
d.push(1); // version
d.push(0x00); // flags: no creation order, no link type (=hard), no charset, name_size=1byte
d.push(4); // name length = 4
d.extend_from_slice(b"test");
d.extend_from_slice(&0x1000u64.to_le_bytes()); // address
d
};
let oh = ObjectHeader {
version: 2,
messages: vec![
crate::object_header::HeaderMessage {
msg_type: MessageType::LinkInfo,
size: 18,
flags: 0,
creation_order: None,
data: {
let mut d = Vec::new();
d.push(0); // version
d.push(0); // flags
d.extend_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); // fh undef
d.extend_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); // btree undef
d
},
},
crate::object_header::HeaderMessage {
msg_type: MessageType::Link,
size: link_data.len(),
flags: 0,
creation_order: None,
data: link_data,
},
],
reference_count: None,
flags: 0,
access_time: None,
modification_time: None,
change_time: None,
birth_time: None,
};
let entries = resolve_v2_group_entries(&[], &oh, 8, 8).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "test");
assert_eq!(entries[0].object_header_address, 0x1000);
}
#[test]
fn integration_v2_groups_temperature() {
let file_data: &[u8] = include_bytes!("../tests/fixtures/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
assert!(sb.version >= 2); // v2/v3 superblock
let addr = resolve_path_any(file_data, &sb, "sensors/temperature").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 = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![22.5, 23.1, 21.8]);
}
#[test]
fn integration_v2_groups_humidity() {
let file_data: &[u8] = include_bytes!("../tests/fixtures/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "sensors/humidity").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 = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_i32(&raw, &dt).unwrap();
assert_eq!(values, vec![45, 50, 55]);
}
#[test]
fn integration_v2_many_links() {
let file_data: &[u8] = include_bytes!("../tests/fixtures/v2_many_links.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "dataset_015").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 = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![15.0]);
}
#[test]
fn integration_resolve_path_any_v1() {
// Test that resolve_path_any also works for v1 files
let file_data: &[u8] = include_bytes!("../tests/fixtures/two_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "group1/values").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 = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_i32(&raw, &dt).unwrap();
assert_eq!(values, vec![10, 20, 30]);
}
#[test]
fn integration_resolve_path_any_v2() {
let file_data: &[u8] = include_bytes!("../tests/fixtures/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "sensors/temperature").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 = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![22.5, 23.1, 21.8]);
}
#[test]
fn path_not_found_v2() {
let file_data: &[u8] = include_bytes!("../tests/fixtures/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let err = resolve_path_any(file_data, &sb, "nonexistent").unwrap_err();
assert!(matches!(err, FormatError::PathNotFound(_)));
}
}