Read a file's metadata the way netCDF-C 4.9.3 does (libhdf5/hdf5open.c), for the whole file on first use (src/model.rs, replacing src/scope.rs): - links in creation order when the group tracks it, else name order; a group's datasets before its subgroups; dimension ids file-wide; - variables' dimensions from _Netcdf4Coordinates (file-wide ids), else the scales DIMENSION_LIST attaches when the first axis has one, else netCDF-C's phony dimensions phony_dim_<id> (create_phony_dims: shared by length and unlimitedness within a group, not between two axes of one variable, numbered subgroups first, a zero length unlimited); - datasets of types netCDF-C cannot represent are not variables (references, bit fields, time, arrays, compounds/enums/VLENs over them), replaying netCDF-C's file-wide type list, failed types included; - unlimited lengths as nc4_find_dim_len (its group and below). NcType gains Enum, Compound, VLen, Opaque and is #[non_exhaustive]; Variable::nc_type is netCDF-C's type (1-byte strings NC_CHAR). New clawhdf5_format::group_v2::links_in_creation_order_in. Tests compare with netCDF-C itself (tests/netcdf_c_view.py calls the libnetcdf netCDF4-python bundles through ctypes): new interop cases for h5py files without dimension scales, every type class, link order; and the gated corpus_vs_netcdf_c (CLAWHDF5_NETCDF_CORPUS): 420 of the 429 conformance-corpus files netCDF-C opens match (main: 68); the other 9 are explained in tests/corpus_known_differences.txt and known-issues. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1141 lines
42 KiB
Rust
1141 lines
42 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::checked_addr;
|
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records_in, find_btree_v2_records_in};
|
|
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::storage::Storage;
|
|
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> {
|
|
resolve_v2_group_entries_in(file_data, object_header, offset_size, length_size)
|
|
}
|
|
|
|
/// [`resolve_v2_group_entries`] over any [`Storage`].
|
|
pub fn resolve_v2_group_entries_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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)
|
|
}
|
|
|
|
/// What a version-2 B-tree header takes with 8-byte offsets and lengths
|
|
/// (22 bytes of fields, the root node's address and record count, and the
|
|
/// checksum), rounded up: hinted before one is read.
|
|
const BTREE_V2_HEADER_HINT_LEN: usize = 64;
|
|
|
|
/// The fractal heap of a dense group. The name index's header and the
|
|
/// heap's root block are read next, whatever the lookup: they are hinted
|
|
/// (see [`Storage::hint`]) so that a storage fetching between attempts
|
|
/// gets them in the same round trip as the heap's header.
|
|
fn dense_heap<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
link_info: &LinkInfoMessage,
|
|
fh_addr: u64,
|
|
offset_size: u8,
|
|
length_size: u8,
|
|
) -> Result<FractalHeapHeader, FormatError> {
|
|
if let Some(btree_addr) = link_info.btree_name_index_address {
|
|
file_data.hint(btree_addr, BTREE_V2_HEADER_HINT_LEN);
|
|
}
|
|
let fh =
|
|
FractalHeapHeader::parse_in(file_data, checked_addr(fh_addr)?, offset_size, length_size)?;
|
|
fh.hint_root_block(file_data);
|
|
Ok(fh)
|
|
}
|
|
|
|
/// Visit every link in dense storage (fractal heap + B-tree v2 name index).
|
|
///
|
|
/// With `hint_headers` (a listing, whose children are usually opened
|
|
/// next), the object header of every hard link is hinted (see
|
|
/// [`Storage::hint`]) as soon as the link is read, even after a failure.
|
|
fn for_each_dense_link<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
link_info: &LinkInfoMessage,
|
|
fh_addr: u64,
|
|
offset_size: u8,
|
|
length_size: u8,
|
|
hint_headers: bool,
|
|
mut visit: impl FnMut(LinkMessage),
|
|
) -> Result<(), FormatError> {
|
|
let fh = dense_heap(file_data, link_info, fh_addr, offset_size, length_size)?;
|
|
if hint_headers {
|
|
// Every link is read: so is every block of the heap.
|
|
fh.hint_managed_blocks(file_data);
|
|
}
|
|
|
|
// 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_in(
|
|
file_data,
|
|
checked_addr(btree_addr)?,
|
|
offset_size,
|
|
length_size,
|
|
)?;
|
|
let records = collect_btree_v2_records_in(file_data, &btree_hdr, offset_size, length_size)?;
|
|
|
|
// After the first link that fails, the others are only read, not
|
|
// visited (a touch, see `storage::touch`); that error is returned.
|
|
let mut failed = None;
|
|
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 = fh
|
|
.read_managed_object_in(file_data, id_bytes, offset_size)
|
|
.and_then(|d| parse_link(&d, offset_size));
|
|
if hint_headers
|
|
&& let Ok(Some(LinkMessage {
|
|
link_target:
|
|
LinkTarget::Hard {
|
|
object_header_address,
|
|
},
|
|
..
|
|
})) = &link
|
|
{
|
|
file_data.hint(
|
|
*object_header_address,
|
|
crate::object_header::OBJECT_HEADER_HINT_LEN,
|
|
);
|
|
}
|
|
if failed.is_some() {
|
|
continue;
|
|
}
|
|
match link {
|
|
Ok(Some(link)) => visit(link),
|
|
Ok(None) => {}
|
|
Err(e) => failed = Some(e),
|
|
}
|
|
}
|
|
match failed {
|
|
Some(e) => Err(e),
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
|
|
/// Resolve entries from dense storage (fractal heap + B-tree v2).
|
|
fn resolve_dense_entries<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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,
|
|
true,
|
|
|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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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_in(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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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 = dense_heap(file_data, &link_info, 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_in(
|
|
file_data,
|
|
checked_addr(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,
|
|
false,
|
|
|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_in(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_in(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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
object_header: &ObjectHeader,
|
|
name: &str,
|
|
offset_size: u8,
|
|
length_size: u8,
|
|
) -> Result<Option<LinkTarget>, FormatError> {
|
|
if is_v1_group(object_header) {
|
|
// Down the group's B-tree, as libhdf5 looks a name up; only when
|
|
// that does not find a hard link of that name is every entry read
|
|
// (a soft link, a group whose B-tree is out of order). A storage
|
|
// error (a read a restartable storage has not fetched yet) is
|
|
// returned as is: reading every entry would not get further.
|
|
if let Some(sym_msg) = object_header
|
|
.messages
|
|
.iter()
|
|
.find(|m| m.msg_type == MessageType::SymbolTable)
|
|
{
|
|
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
|
|
match group_v1::find_v1_entry(file_data, &stm, name, offset_size, length_size) {
|
|
Ok(Some(e)) if e.object_header_address != u64::MAX => {
|
|
return Ok(Some(LinkTarget::Hard {
|
|
object_header_address: e.object_header_address,
|
|
}));
|
|
}
|
|
Err(e @ FormatError::Storage(_)) => return Err(e),
|
|
_ => {}
|
|
}
|
|
}
|
|
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> {
|
|
resolve_child_core(file_data, superblock, group_address, name)
|
|
}
|
|
|
|
/// [`resolve_child`] over any [`Storage`]. One with the whole file in memory
|
|
/// is read as the slice, by code compiled in this crate (see
|
|
/// [`crate::storage`], "Slice entry points").
|
|
#[inline]
|
|
pub fn resolve_child_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
group_address: u64,
|
|
name: &str,
|
|
) -> Result<u64, FormatError> {
|
|
match file_data.as_contiguous() {
|
|
Some(all) => resolve_child(all, superblock, group_address, name),
|
|
None => resolve_child_core(file_data, superblock, group_address, name),
|
|
}
|
|
}
|
|
|
|
fn resolve_child_core<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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_in(file_data, checked_addr(group_address)?, os, ls)?;
|
|
if !is_v2_group(&header) || is_v1_group(&header) {
|
|
return group_children(file_data, superblock, group_address, false)?
|
|
.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_in(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_any_core(file_data, superblock, path)
|
|
}
|
|
|
|
/// [`resolve_path_any`] over any [`Storage`]. One with the whole file in memory
|
|
/// is read as the slice, by code compiled in this crate (see
|
|
/// [`crate::storage`], "Slice entry points").
|
|
#[inline]
|
|
pub fn resolve_path_any_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
path: &str,
|
|
) -> Result<u64, FormatError> {
|
|
match file_data.as_contiguous() {
|
|
Some(all) => resolve_path_any(all, superblock, path),
|
|
None => resolve_path_any_core(file_data, superblock, path),
|
|
}
|
|
}
|
|
|
|
fn resolve_path_any_core<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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> {
|
|
resolve_path_from_in(file_data, superblock, group_address, path)
|
|
}
|
|
|
|
/// [`resolve_path_from`] over any [`Storage`].
|
|
pub fn resolve_path_from_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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> {
|
|
resolve_group_children_core(file_data, superblock, group_address)
|
|
}
|
|
|
|
/// [`resolve_group_children`] over any [`Storage`]. One with the whole file in memory
|
|
/// is read as the slice, by code compiled in this crate (see
|
|
/// [`crate::storage`], "Slice entry points").
|
|
#[inline]
|
|
pub fn resolve_group_children_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
group_address: u64,
|
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
|
match file_data.as_contiguous() {
|
|
Some(all) => resolve_group_children(all, superblock, group_address),
|
|
None => resolve_group_children_core(file_data, superblock, group_address),
|
|
}
|
|
}
|
|
|
|
fn resolve_group_children_core<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
group_address: u64,
|
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
|
group_children(file_data, superblock, group_address, true)
|
|
}
|
|
|
|
/// [`resolve_group_children`]; with `hint_headers`, every child's object
|
|
/// header is hinted (see [`Storage::hint`]) as soon as its address is
|
|
/// known, for a listing whose children are opened next.
|
|
fn group_children<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
group_address: u64,
|
|
hint_headers: bool,
|
|
) -> Result<Vec<GroupEntry>, FormatError> {
|
|
let os = superblock.offset_size;
|
|
let ls = superblock.length_size;
|
|
let header = ObjectHeader::parse_in(file_data, checked_addr(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::v1_group_entries(file_data, &stm, os, ls, hint_headers)?;
|
|
if all.iter().any(|e| e.name.is_empty()) {
|
|
return Err(FormatError::InvalidLinkName);
|
|
}
|
|
if all.iter().any(group_v1::is_v1_soft_link) {
|
|
soft = group_v1::v1_soft_links_in(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, hint_headers, 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_in(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)
|
|
}
|
|
|
|
/// The names of the links of the group at `group_address` in creation
|
|
/// order — the order libhdf5 iterates them in with `H5_INDEX_CRT_ORDER`
|
|
/// (`H5Literate`) — when the group tracks the creation order of its links;
|
|
/// `None` when it does not (a version-1 group never does), in which case
|
|
/// libhdf5 can only iterate by name (`H5_INDEX_NAME`: byte order of the
|
|
/// names). Hard, soft and external links are listed (user-defined ones,
|
|
/// which cannot be followed, are not); links without a creation order
|
|
/// value, which a tracking group should not have, come last in the order
|
|
/// they are stored.
|
|
pub fn links_in_creation_order_in<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
superblock: &Superblock,
|
|
group_address: u64,
|
|
) -> Result<Option<Vec<String>>, FormatError> {
|
|
let os = superblock.offset_size;
|
|
let ls = superblock.length_size;
|
|
let header = ObjectHeader::parse_in(file_data, checked_addr(group_address)?, os, ls)?;
|
|
if !is_v2_group(&header) {
|
|
return Ok(None);
|
|
}
|
|
let link_info = find_link_info(&header, os)?;
|
|
if link_info.max_creation_order.is_none() {
|
|
return Ok(None);
|
|
}
|
|
let mut links: Vec<(u64, String)> = Vec::new();
|
|
let mut visit = |link: LinkMessage| {
|
|
links.push((link.creation_order.unwrap_or(u64::MAX), link.name));
|
|
};
|
|
if let Some(fh_addr) = link_info.fractal_heap_address {
|
|
for_each_dense_link(file_data, &link_info, fh_addr, os, ls, false, visit)?;
|
|
} else {
|
|
for msg in &header.messages {
|
|
if msg.msg_type == MessageType::Link
|
|
&& let Some(link) = parse_link(&msg.data, os)?
|
|
{
|
|
visit(link);
|
|
}
|
|
}
|
|
}
|
|
links.sort_by_key(|&(order, _)| order);
|
|
Ok(Some(links.into_iter().map(|(_, name)| name).collect()))
|
|
}
|
|
|
|
/// 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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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_in(file_data, checked_addr(start)?, os, ls)?;
|
|
|
|
for (i, component) in components.iter().enumerate() {
|
|
match lookup_link(file_data, ¤t_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_in(file_data, checked_addr(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<S: Storage + ?Sized>(
|
|
file_data: &S,
|
|
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, false)
|
|
} else if is_v2_group(object_header) {
|
|
resolve_v2_group_entries_in(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]);
|
|
}
|
|
|
|
/// `v1_groups_400.h5` from its superblock on (it has a user block).
|
|
fn v1_groups_400() -> (Vec<u8>, Superblock) {
|
|
let all: &[u8] = include_bytes!("../tests/fixtures/v1_groups_400.h5");
|
|
let data = all[signature::find_signature(all).unwrap()..].to_vec();
|
|
let sb = Superblock::parse(&data, 0).unwrap();
|
|
(data, sb)
|
|
}
|
|
|
|
/// Every child of a v1 group resolves by name, down the group's B-tree,
|
|
/// to the address the listing gives, reading a small part of what the
|
|
/// listing reads; a name the group does not hold is not found.
|
|
#[test]
|
|
fn v1_lookup_down_the_btree_agrees_with_the_listing() {
|
|
let (data, sb) = v1_groups_400();
|
|
let children = resolve_group_children(&data, &sb, sb.root_group_address).unwrap();
|
|
assert_eq!(children.len(), 401);
|
|
for c in &children {
|
|
let path = format!("/{}", c.name);
|
|
assert_eq!(
|
|
resolve_path_any(&data, &sb, &path).unwrap(),
|
|
c.object_header_address,
|
|
"{path}"
|
|
);
|
|
}
|
|
for missing in ["/g0400", "/a", "/g", "/g00000", "/zz", "/x0"] {
|
|
assert!(
|
|
matches!(
|
|
resolve_path_any(&data, &sb, missing),
|
|
Err(FormatError::PathNotFound(_))
|
|
),
|
|
"{missing}"
|
|
);
|
|
}
|
|
let st = crate::storage::CountingStorage::new(data.clone());
|
|
resolve_group_children_in(&st, &sb, sb.root_group_address).unwrap();
|
|
let listing = st.bytes_read();
|
|
st.reset();
|
|
let last = children.last().unwrap();
|
|
assert_eq!(
|
|
resolve_path_any_in(&st, &sb, &format!("/{}", last.name)).unwrap(),
|
|
last.object_header_address
|
|
);
|
|
assert!(
|
|
st.bytes_read() * 8 < listing,
|
|
"lookup read {} bytes, listing {listing}",
|
|
st.bytes_read()
|
|
);
|
|
}
|
|
|
|
/// A v1 group whose B-tree is out of name order (a name changed in the
|
|
/// heap so that it sorts past every key) is still looked up by reading
|
|
/// every entry, as before the lookup went down the B-tree.
|
|
#[test]
|
|
fn v1_lookup_falls_back_when_the_btree_is_out_of_order() {
|
|
let (mut data, sb) = v1_groups_400();
|
|
let at: Vec<usize> = data
|
|
.windows(6)
|
|
.enumerate()
|
|
.filter(|(_, w)| *w == b"g0200\0")
|
|
.map(|(i, _)| i)
|
|
.collect();
|
|
assert_eq!(at.len(), 1, "one heap string");
|
|
data[at[0]] = b'~';
|
|
let children = resolve_group_children(&data, &sb, sb.root_group_address).unwrap();
|
|
let moved = children.iter().find(|c| c.name == "~0200").unwrap();
|
|
assert_eq!(
|
|
resolve_path_any(&data, &sb, "/~0200").unwrap(),
|
|
moved.object_header_address
|
|
);
|
|
assert!(resolve_path_any(&data, &sb, "/g0200").is_err());
|
|
for c in &children {
|
|
let path = format!("/{}", c.name);
|
|
assert_eq!(
|
|
resolve_path_any(&data, &sb, &path).unwrap(),
|
|
c.object_header_address,
|
|
"{path}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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(_)));
|
|
}
|
|
}
|