feat(format): follow soft links; explicit errors for external links and external raw data
- Path resolution follows soft links in both old-style (symbol table, cache
type 2) and new-style (compact and dense Link message) groups: absolute and
relative targets, links to groups, links through links, with a depth limit
so a link cycle is NestingDepthExceeded rather than a hang. A dangling link
reports the target it could not find. Previously every soft link was
PathNotFound.
- An external link is FormatError::ExternalLinkUnsupported { filename,
object_path } instead of a misleading PathNotFound.
- Message 0x0007 (External Data Files) is now a known MessageType, and a
dataset carrying it is FormatError::ExternalDataFilesUnsupported. Such a
dataset has no data address in this file, so it would otherwise be read as
"never written" and answered with fill values — wrong data, no error.
- Dense link iteration is shared between hard-link listing and the new
symbolic-link lookup; entry listing behaviour is unchanged.
- h5py interop test for both libver settings.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
12847c6c66
commit
e38c8133bc
@@ -117,6 +117,17 @@ pub enum FormatError {
|
|||||||
/// A message is marked shared but was parsed without access to the file,
|
/// A message is marked shared but was parsed without access to the file,
|
||||||
/// so the reference to the real message could not be followed.
|
/// so the reference to the real message could not be followed.
|
||||||
UnresolvedSharedMessage,
|
UnresolvedSharedMessage,
|
||||||
|
/// The dataset's raw data is stored in external files (External Data
|
||||||
|
/// Files message), which this reader does not follow.
|
||||||
|
ExternalDataFilesUnsupported,
|
||||||
|
/// The path goes through an external link (a link into another file),
|
||||||
|
/// which this reader does not follow.
|
||||||
|
ExternalLinkUnsupported {
|
||||||
|
/// The file the link points into.
|
||||||
|
filename: String,
|
||||||
|
/// The object path within that file.
|
||||||
|
object_path: String,
|
||||||
|
},
|
||||||
/// Invalid SOHM table version.
|
/// Invalid SOHM table version.
|
||||||
InvalidSohmTableVersion(u8),
|
InvalidSohmTableVersion(u8),
|
||||||
/// Invalid SOHM table signature (expected "SMTB").
|
/// Invalid SOHM table signature (expected "SMTB").
|
||||||
@@ -310,6 +321,18 @@ impl fmt::Display for FormatError {
|
|||||||
FormatError::InvalidSharedMessageVersion(v) => {
|
FormatError::InvalidSharedMessageVersion(v) => {
|
||||||
write!(f, "invalid shared message version: {v}")
|
write!(f, "invalid shared message version: {v}")
|
||||||
}
|
}
|
||||||
|
FormatError::ExternalLinkUnsupported {
|
||||||
|
filename,
|
||||||
|
object_path,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"path goes through an external link to {object_path} in {filename}, which is \
|
||||||
|
not supported"
|
||||||
|
),
|
||||||
|
FormatError::ExternalDataFilesUnsupported => write!(
|
||||||
|
f,
|
||||||
|
"dataset raw data is stored in external file(s), which is not supported"
|
||||||
|
),
|
||||||
FormatError::UnresolvedSharedMessage => write!(
|
FormatError::UnresolvedSharedMessage => write!(
|
||||||
f,
|
f,
|
||||||
"message is shared but no file data was available to resolve it"
|
"message is shared but no file data was available to resolve it"
|
||||||
|
|||||||
@@ -165,6 +165,15 @@ pub fn read_full_with_fill<E: From<FormatError>>(
|
|||||||
length_size: u8,
|
length_size: u8,
|
||||||
read: impl FnOnce() -> Result<Vec<u8>, E>,
|
read: impl FnOnce() -> Result<Vec<u8>, E>,
|
||||||
) -> Result<Vec<u8>, E> {
|
) -> Result<Vec<u8>, E> {
|
||||||
|
// A dataset with external raw data also has no data address in this
|
||||||
|
// file. It is NOT unallocated — its values live elsewhere — so it must
|
||||||
|
// never be answered with the fill value.
|
||||||
|
if messages
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.msg_type == MessageType::ExternalDataFiles)
|
||||||
|
{
|
||||||
|
return Err(FormatError::ExternalDataFilesUnsupported.into());
|
||||||
|
}
|
||||||
let fill = dataset_fill_value(messages)?;
|
let fill = dataset_fill_value(messages)?;
|
||||||
if !has_storage(layout) {
|
if !has_storage(layout) {
|
||||||
return Ok(filled_dataset(dataspace, elem_size, fill.as_deref())?);
|
return Ok(filled_dataset(dataspace, elem_size, fill.as_deref())?);
|
||||||
|
|||||||
@@ -60,6 +60,54 @@ pub fn resolve_v1_group_entries(
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Symbol table cache type for a soft link: the scratch pad's first four bytes
|
||||||
|
/// are the local-heap offset of the link's target path, and the entry's object
|
||||||
|
/// header address is undefined.
|
||||||
|
const CACHE_TYPE_SOFT_LINK: u32 = 2;
|
||||||
|
|
||||||
|
/// The target path of the soft link called `name` in a v1 group, if any.
|
||||||
|
pub fn find_v1_soft_link(
|
||||||
|
file_data: &[u8],
|
||||||
|
sym_table_msg: &SymbolTableMessage,
|
||||||
|
name: &str,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Option<String>, FormatError> {
|
||||||
|
let heap = LocalHeap::parse(
|
||||||
|
file_data,
|
||||||
|
sym_table_msg.local_heap_address as usize,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
let snod_addrs = collect_symbol_table_nodes(
|
||||||
|
file_data,
|
||||||
|
sym_table_msg.btree_address,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
|
for snod_addr in snod_addrs {
|
||||||
|
let snod = SymbolTableNode::parse(file_data, snod_addr as usize, offset_size)?;
|
||||||
|
for entry in &snod.entries {
|
||||||
|
if entry.cache_type != CACHE_TYPE_SOFT_LINK {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if heap.read_string(file_data, entry.link_name_offset)? != name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let value_offset = u32::from_le_bytes([
|
||||||
|
entry.scratch_pad[0],
|
||||||
|
entry.scratch_pad[1],
|
||||||
|
entry.scratch_pad[2],
|
||||||
|
entry.scratch_pad[3],
|
||||||
|
]);
|
||||||
|
return heap
|
||||||
|
.read_string(file_data, u64::from(value_offset))
|
||||||
|
.map(Some);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract the SymbolTableMessage from an object header's messages.
|
/// Extract the SymbolTableMessage from an object header's messages.
|
||||||
fn find_symbol_table_message(
|
fn find_symbol_table_message(
|
||||||
obj_header: &ObjectHeader,
|
obj_header: &ObjectHeader,
|
||||||
|
|||||||
@@ -63,14 +63,15 @@ fn resolve_compact_entries(
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve entries from dense storage (fractal heap + B-tree v2).
|
/// Visit every link in dense storage (fractal heap + B-tree v2 name index).
|
||||||
fn resolve_dense_entries(
|
fn for_each_dense_link(
|
||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
link_info: &LinkInfoMessage,
|
link_info: &LinkInfoMessage,
|
||||||
fh_addr: u64,
|
fh_addr: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<Vec<GroupEntry>, FormatError> {
|
mut visit: impl FnMut(LinkMessage),
|
||||||
|
) -> Result<(), FormatError> {
|
||||||
// Parse fractal heap
|
// Parse fractal heap
|
||||||
let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
|
let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
|
||||||
|
|
||||||
@@ -81,7 +82,6 @@ fn resolve_dense_entries(
|
|||||||
let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
|
let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
|
||||||
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
|
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
for record in &records {
|
for record in &records {
|
||||||
// For type 5 (name index): hash(4) + heap_id(heap_id_length)
|
// 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)
|
// For type 6 (creation order): creation_order(8) + heap_id(heap_id_length)
|
||||||
@@ -98,22 +98,94 @@ fn resolve_dense_entries(
|
|||||||
|
|
||||||
// Read managed object from fractal heap
|
// Read managed object from fractal heap
|
||||||
let link_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
|
let link_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
|
||||||
|
visit(LinkMessage::parse(&link_data, offset_size)?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Parse as Link message
|
/// Resolve entries from dense storage (fractal heap + B-tree v2).
|
||||||
let link = LinkMessage::parse(&link_data, offset_size)?;
|
fn resolve_dense_entries(
|
||||||
if let LinkTarget::Hard {
|
file_data: &[u8],
|
||||||
object_header_address,
|
link_info: &LinkInfoMessage,
|
||||||
} = link.link_target
|
fh_addr: u64,
|
||||||
{
|
offset_size: u8,
|
||||||
entries.push(GroupEntry {
|
length_size: u8,
|
||||||
name: link.name,
|
) -> 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,
|
object_header_address,
|
||||||
cache_type: 0,
|
} = link.link_target
|
||||||
});
|
{
|
||||||
|
entries.push(GroupEntry {
|
||||||
|
name: link.name,
|
||||||
|
object_header_address,
|
||||||
|
cache_type: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
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(
|
||||||
|
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) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let is_symbolic = |t: &LinkTarget| !matches!(t, LinkTarget::Hard { .. });
|
||||||
|
let link_info = find_link_info(object_header, offset_size)?;
|
||||||
|
let mut found = None;
|
||||||
|
if let Some(fh_addr) = link_info.fractal_heap_address {
|
||||||
|
for_each_dense_link(
|
||||||
|
file_data,
|
||||||
|
&link_info,
|
||||||
|
fh_addr,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
|link| {
|
||||||
|
if link.name == name && is_symbolic(&link.link_target) {
|
||||||
|
found = Some(link.link_target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
for msg in &object_header.messages {
|
||||||
|
if msg.msg_type == MessageType::Link {
|
||||||
|
let link = LinkMessage::parse(&msg.data, offset_size)?;
|
||||||
|
if link.name == name && is_symbolic(&link.link_target) {
|
||||||
|
found = Some(link.link_target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(found)
|
||||||
Ok(entries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find and parse the Link Info message from an object header.
|
/// Find and parse the Link Info message from an object header.
|
||||||
@@ -158,6 +230,19 @@ pub fn resolve_path_any(
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
superblock: &Superblock,
|
superblock: &Superblock,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
) -> Result<u64, FormatError> {
|
||||||
|
resolve_path_following_links(file_data, superblock, path, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
fn resolve_path_following_links(
|
||||||
|
file_data: &[u8],
|
||||||
|
superblock: &Superblock,
|
||||||
|
path: &str,
|
||||||
|
depth: u8,
|
||||||
) -> Result<u64, FormatError> {
|
) -> Result<u64, FormatError> {
|
||||||
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
if components.is_empty() {
|
if components.is_empty() {
|
||||||
@@ -176,7 +261,9 @@ pub fn resolve_path_any(
|
|||||||
for (i, component) in components.iter().enumerate() {
|
for (i, component) in components.iter().enumerate() {
|
||||||
let entries = resolve_group_entries(file_data, ¤t_header, os, ls)?;
|
let entries = resolve_group_entries(file_data, ¤t_header, os, ls)?;
|
||||||
|
|
||||||
let found = entries.iter().find(|e| e.name == *component);
|
let found = entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.name == *component && e.object_header_address != u64::MAX);
|
||||||
match found {
|
match found {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
if i == components.len() - 1 {
|
if i == components.len() - 1 {
|
||||||
@@ -186,7 +273,37 @@ pub fn resolve_path_any(
|
|||||||
current_header = ObjectHeader::parse(file_data, current_addr as usize, os, ls)?;
|
current_header = ObjectHeader::parse(file_data, current_addr as usize, os, ls)?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(FormatError::PathNotFound(String::from(*component)));
|
return match find_symbolic_link(file_data, ¤t_header, component, os, ls)? {
|
||||||
|
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 mut full = String::new();
|
||||||
|
if !target_path.starts_with('/') {
|
||||||
|
for parent in &components[..i] {
|
||||||
|
full.push('/');
|
||||||
|
full.push_str(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
full.push('/');
|
||||||
|
full.push_str(&target_path);
|
||||||
|
for rest in &components[i + 1..] {
|
||||||
|
full.push('/');
|
||||||
|
full.push_str(rest);
|
||||||
|
}
|
||||||
|
resolve_path_following_links(file_data, superblock, &full, depth + 1)
|
||||||
|
}
|
||||||
|
Some(LinkTarget::External {
|
||||||
|
filename,
|
||||||
|
object_path,
|
||||||
|
}) => Err(FormatError::ExternalLinkUnsupported {
|
||||||
|
filename,
|
||||||
|
object_path,
|
||||||
|
}),
|
||||||
|
_ => Err(FormatError::PathNotFound(String::from(*component))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ pub enum MessageType {
|
|||||||
Datatype,
|
Datatype,
|
||||||
FillValueOld,
|
FillValueOld,
|
||||||
FillValue,
|
FillValue,
|
||||||
|
/// External Data Files (0x0007): the dataset's raw data lives in other
|
||||||
|
/// files, listed by this message.
|
||||||
|
ExternalDataFiles,
|
||||||
Link,
|
Link,
|
||||||
DataLayout,
|
DataLayout,
|
||||||
GroupInfo,
|
GroupInfo,
|
||||||
@@ -36,6 +39,7 @@ impl MessageType {
|
|||||||
0x0004 => MessageType::FillValueOld,
|
0x0004 => MessageType::FillValueOld,
|
||||||
0x0005 => MessageType::FillValue,
|
0x0005 => MessageType::FillValue,
|
||||||
0x0006 => MessageType::Link,
|
0x0006 => MessageType::Link,
|
||||||
|
0x0007 => MessageType::ExternalDataFiles,
|
||||||
0x0008 => MessageType::DataLayout,
|
0x0008 => MessageType::DataLayout,
|
||||||
0x000A => MessageType::GroupInfo,
|
0x000A => MessageType::GroupInfo,
|
||||||
0x000B => MessageType::FilterPipeline,
|
0x000B => MessageType::FilterPipeline,
|
||||||
@@ -60,6 +64,7 @@ impl MessageType {
|
|||||||
MessageType::Datatype => 0x0003,
|
MessageType::Datatype => 0x0003,
|
||||||
MessageType::FillValueOld => 0x0004,
|
MessageType::FillValueOld => 0x0004,
|
||||||
MessageType::FillValue => 0x0005,
|
MessageType::FillValue => 0x0005,
|
||||||
|
MessageType::ExternalDataFiles => 0x0007,
|
||||||
MessageType::Link => 0x0006,
|
MessageType::Link => 0x0006,
|
||||||
MessageType::DataLayout => 0x0008,
|
MessageType::DataLayout => 0x0008,
|
||||||
MessageType::GroupInfo => 0x000A,
|
MessageType::GroupInfo => 0x000A,
|
||||||
@@ -90,6 +95,7 @@ mod tests {
|
|||||||
(0x0003, MessageType::Datatype),
|
(0x0003, MessageType::Datatype),
|
||||||
(0x0004, MessageType::FillValueOld),
|
(0x0004, MessageType::FillValueOld),
|
||||||
(0x0005, MessageType::FillValue),
|
(0x0005, MessageType::FillValue),
|
||||||
|
(0x0007, MessageType::ExternalDataFiles),
|
||||||
(0x0006, MessageType::Link),
|
(0x0006, MessageType::Link),
|
||||||
(0x0008, MessageType::DataLayout),
|
(0x0008, MessageType::DataLayout),
|
||||||
(0x000A, MessageType::GroupInfo),
|
(0x000A, MessageType::GroupInfo),
|
||||||
@@ -119,8 +125,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_type_zero_gap() {
|
fn unknown_type_zero_gap() {
|
||||||
// 0x0007 is not a defined type
|
// 0x0009 is reserved for the library's own testing; no file uses it.
|
||||||
let mt = MessageType::from_u16(0x0007);
|
let mt = MessageType::from_u16(0x0009);
|
||||||
assert_eq!(mt, MessageType::Unknown(0x0007));
|
assert_eq!(mt, MessageType::Unknown(0x0009));
|
||||||
|
// 0x0007 used to be treated as unknown: it is External Data Files.
|
||||||
|
assert_eq!(
|
||||||
|
MessageType::from_u16(0x0007),
|
||||||
|
MessageType::ExternalDataFiles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -699,3 +699,88 @@ with h5py.File("{path_str}", "r") as f:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// h5py writes soft / external links and external raw data -> clawhdf5
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Soft links are followed (absolute, relative, through groups, with a cycle
|
||||||
|
/// guard). Things this reader does not follow — external links, and datasets
|
||||||
|
/// whose raw data lives in another file — are explicit errors. They used to
|
||||||
|
/// surface as a misleading `PathNotFound`, and external raw data could read
|
||||||
|
/// back as fill values.
|
||||||
|
#[test]
|
||||||
|
fn h5py_links_clawhdf5_resolves_or_refuses() {
|
||||||
|
use clawhdf5::Error;
|
||||||
|
use clawhdf5_format::error::FormatError;
|
||||||
|
|
||||||
|
skip_if_no_python!();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let dir_str = dir.path().display().to_string();
|
||||||
|
for (tag, kwargs) in [("default", ""), ("latest", ", libver='latest'")] {
|
||||||
|
let script = format!(
|
||||||
|
r#"
|
||||||
|
import h5py, numpy as np, os
|
||||||
|
os.chdir("{dir_str}")
|
||||||
|
with h5py.File("other_{tag}.h5", "w"{kwargs}) as o:
|
||||||
|
o.create_dataset("remote", data=np.arange(3, dtype="<i4"))
|
||||||
|
with h5py.File("links_{tag}.h5", "w"{kwargs}) as f:
|
||||||
|
f.create_dataset("real", data=np.arange(4, dtype="<i4"))
|
||||||
|
g = f.create_group("grp")
|
||||||
|
g.create_dataset("inner", data=np.arange(2, dtype="<i4"))
|
||||||
|
g["rel"] = h5py.SoftLink("inner")
|
||||||
|
f["soft"] = h5py.SoftLink("/real")
|
||||||
|
f["soft_grp"] = h5py.SoftLink("/grp")
|
||||||
|
f["dangling"] = h5py.SoftLink("/nope")
|
||||||
|
f["loop_a"] = h5py.SoftLink("/loop_b")
|
||||||
|
f["loop_b"] = h5py.SoftLink("/loop_a")
|
||||||
|
f["ext"] = h5py.ExternalLink("other_{tag}.h5", "/remote")
|
||||||
|
f.create_dataset("extdata", shape=(4,), dtype="<i4", external=[("raw_{tag}.bin", 0, 16)])
|
||||||
|
f["extdata"][...] = np.array([11, 22, 33, 44], dtype="<i4")
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
run_python(&script);
|
||||||
|
|
||||||
|
let file = File::open(dir.path().join(format!("links_{tag}.h5"))).unwrap();
|
||||||
|
let read = |path: &str| file.dataset(path).and_then(|d| d.read_i32());
|
||||||
|
assert_eq!(read("soft").unwrap(), vec![0, 1, 2, 3], "{tag}");
|
||||||
|
assert_eq!(read("soft_grp/inner").unwrap(), vec![0, 1], "{tag}");
|
||||||
|
assert_eq!(
|
||||||
|
read("grp/rel").unwrap(),
|
||||||
|
vec![0, 1],
|
||||||
|
"{tag}: relative target"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
read("soft_grp/rel").unwrap(),
|
||||||
|
vec![0, 1],
|
||||||
|
"{tag}: link via link"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(read("dangling"), Err(Error::Format(FormatError::PathNotFound(p))) if p == "nope"),
|
||||||
|
"{tag}: dangling link names its missing target"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
read("loop_a"),
|
||||||
|
Err(Error::Format(FormatError::NestingDepthExceeded))
|
||||||
|
),
|
||||||
|
"{tag}: link cycle"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
read("ext"),
|
||||||
|
Err(Error::Format(FormatError::ExternalLinkUnsupported { ref object_path, .. }))
|
||||||
|
if object_path == "/remote"
|
||||||
|
),
|
||||||
|
"{tag}: external link"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
read("extdata"),
|
||||||
|
Err(Error::Format(FormatError::ExternalDataFilesUnsupported))
|
||||||
|
),
|
||||||
|
"{tag}: external raw data must not read as fill values"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user