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
@@ -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