//! 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, 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( file_data: &S, object_header: &ObjectHeader, offset_size: u8, length_size: u8, ) -> Result, 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, 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, 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( file_data: &S, link_info: &LinkInfoMessage, fh_addr: u64, offset_size: u8, length_size: u8, ) -> Result { 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( 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( file_data: &S, link_info: &LinkInfoMessage, fh_addr: u64, offset_size: u8, length_size: u8, ) -> Result, 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( file_data: &S, object_header: &ObjectHeader, name: &str, offset_size: u8, length_size: u8, ) -> Result, 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( file_data: &S, object_header: &ObjectHeader, name: &str, offset_size: u8, length_size: u8, ) -> Result, 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( file_data: &S, object_header: &ObjectHeader, name: &str, offset_size: u8, length_size: u8, ) -> Result, 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: &S, object_header: &ObjectHeader, name: &str, offset_size: u8, length_size: u8, ) -> Result, 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 { 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( file_data: &S, superblock: &Superblock, group_address: u64, name: &str, ) -> Result { 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( file_data: &S, superblock: &Superblock, group_address: u64, name: &str, ) -> Result { 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 { 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 { 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( file_data: &S, superblock: &Superblock, path: &str, ) -> Result { 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( file_data: &S, superblock: &Superblock, path: &str, ) -> Result { 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 { resolve_path_from_in(file_data, superblock, group_address, path) } /// [`resolve_path_from`] over any [`Storage`]. pub fn resolve_path_from_in( file_data: &S, superblock: &Superblock, group_address: u64, path: &str, ) -> Result { 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, 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( file_data: &S, superblock: &Superblock, group_address: u64, ) -> Result, 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( file_data: &S, superblock: &Superblock, group_address: u64, ) -> Result, 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( file_data: &S, superblock: &Superblock, group_address: u64, hint_headers: bool, ) -> Result, 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) } /// 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: &S, superblock: &Superblock, start: u64, path: &str, depth: u8, ) -> Result { 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( file_data: &S, object_header: &ObjectHeader, offset_size: u8, length_size: u8, ) -> Result, 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, 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 = 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(_))); } }