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]>
This commit is contained in:
@@ -6,6 +6,11 @@
|
||||
#[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;
|
||||
@@ -159,45 +164,34 @@ fn resolve_dense_entries(
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// The soft or external link called `name` in this 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_symbolic_link(
|
||||
/// 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> {
|
||||
if is_v1_group(object_header) {
|
||||
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)?;
|
||||
return group_v1::find_v1_soft_link(file_data, &stm, name, offset_size, length_size)
|
||||
.map(|target| target.map(|target_path| LinkTarget::Soft { target_path }));
|
||||
}
|
||||
if !is_v2_group(object_header) {
|
||||
let Some(sym_msg) = object_header
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| m.msg_type == MessageType::SymbolTable)
|
||||
else {
|
||||
return Ok(None);
|
||||
}
|
||||
// As when all links were scanned: the last symbolic link of that name.
|
||||
Ok(
|
||||
links_named(file_data, object_header, name, offset_size, length_size)?
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|link| link.link_target)
|
||||
.find(|t| !matches!(t, LinkTarget::Hard { .. })),
|
||||
)
|
||||
};
|
||||
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).
|
||||
/// 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,
|
||||
@@ -272,6 +266,32 @@ fn links_named(
|
||||
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
|
||||
@@ -293,29 +313,25 @@ fn lookup_link(
|
||||
object_header_address: e.object_header_address,
|
||||
}));
|
||||
}
|
||||
return find_symbolic_link(file_data, object_header, name, offset_size, length_size);
|
||||
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",
|
||||
)));
|
||||
}
|
||||
let links = links_named(file_data, object_header, name, offset_size, length_size)?;
|
||||
if let Some(addr) = links.iter().find_map(|l| match l.link_target {
|
||||
LinkTarget::Hard {
|
||||
object_header_address,
|
||||
} if object_header_address != u64::MAX => Some(object_header_address),
|
||||
_ => None,
|
||||
}) {
|
||||
return Ok(Some(LinkTarget::Hard {
|
||||
object_header_address: addr,
|
||||
}));
|
||||
}
|
||||
Ok(links
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|link| link.link_target)
|
||||
.find(|t| !matches!(t, LinkTarget::Hard { .. })))
|
||||
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
|
||||
@@ -342,19 +358,14 @@ pub fn resolve_child(
|
||||
.map(|e| e.object_header_address)
|
||||
.ok_or_else(not_found);
|
||||
}
|
||||
let links = links_named(file_data, &header, name, os, ls)?;
|
||||
// The listing puts hard links before resolved soft links.
|
||||
if let Some(addr) = links.iter().find_map(|l| match l.link_target {
|
||||
LinkTarget::Hard {
|
||||
// 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,
|
||||
} => Some(object_header_address),
|
||||
_ => None,
|
||||
}) {
|
||||
return Ok(addr);
|
||||
}
|
||||
for link in links {
|
||||
if let LinkTarget::Soft { target_path } = link.link_target {
|
||||
return match resolve_path_from(file_data, superblock, group_address, &target_path) {
|
||||
}) => 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(_)
|
||||
@@ -362,10 +373,10 @@ pub fn resolve_child(
|
||||
| FormatError::ExternalLinkUnsupported { .. },
|
||||
) => Err(not_found()),
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
}
|
||||
Some(LinkTarget::External { .. }) | None => Err(not_found()),
|
||||
}
|
||||
Err(not_found())
|
||||
}
|
||||
|
||||
/// Find and parse the Link Info message from an object header.
|
||||
@@ -471,16 +482,23 @@ pub fn resolve_group_children(
|
||||
}
|
||||
entries.extend(all.into_iter().filter(|e| !group_v1::is_v1_soft_link(e)));
|
||||
} else if is_v2_group(&header) {
|
||||
let mut visit = |link: LinkMessage| 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 { .. } => {}
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user