feat(format): name the filter in UnsupportedFilter errors; Blosc2/ZFP stay errors
Blosc2 (32026) is out of reach for now: hdf5plugin's Blosc2 filter stores each HDF5 chunk as a Blosc2 super-chunk frame (msgpack header, a compressed chunk-offset index, trailer metalayers) and, for 2-D and larger chunks, as a B2ND array whose n-D blocks have to be reassembled - on top of the Blosc2 chunk format itself (extended header, filter pipeline, special-value chunks). ZFP (32013) is out of scope. Both keep failing with UnsupportedFilter, and the message now says what the ID is: "unsupported filter: 32026 (Blosc2, not implemented by clawhdf5)", or, for a filter this build left out, "... (Blosc; this build lacks the `blosc` feature)". filter_registry::known_filter exposes the table. tests/plugin_filters_interop.rs: hdf5plugin's Blosc2 and ZFP datasets read as an error naming the filter, never as data. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -406,9 +406,17 @@ impl fmt::Display for FormatError {
|
||||
FormatError::InvalidFilterPipelineVersion(v) => {
|
||||
write!(f, "invalid filter pipeline version: {v}")
|
||||
}
|
||||
FormatError::UnsupportedFilter(id) => {
|
||||
write!(f, "unsupported filter: {id}")
|
||||
}
|
||||
FormatError::UnsupportedFilter(id) => match crate::filter_registry::known_filter(*id) {
|
||||
Some((name, Some(feature))) => write!(
|
||||
f,
|
||||
"unsupported filter: {id} ({name}; this build lacks the `{feature}` feature)"
|
||||
),
|
||||
Some((name, None)) => write!(
|
||||
f,
|
||||
"unsupported filter: {id} ({name}, not implemented by clawhdf5)"
|
||||
),
|
||||
None => write!(f, "unsupported filter: {id}"),
|
||||
},
|
||||
FormatError::FilterError(msg) => {
|
||||
write!(f, "filter error: {msg}")
|
||||
}
|
||||
|
||||
@@ -130,6 +130,30 @@ pub fn builtin_filter(id: u16) -> Option<&'static BuiltinFilter> {
|
||||
builtin_filters().iter().find(|f| f.id == id)
|
||||
}
|
||||
|
||||
/// Why a filter ID may be missing from this build: the filter's name, and
|
||||
/// the cargo feature that provides it (`None`: clawhdf5 does not implement
|
||||
/// it — register a codec for it with [`register_filter`]). `None` for an ID
|
||||
/// clawhdf5 knows nothing about.
|
||||
pub fn known_filter(id: u16) -> Option<(&'static str, Option<&'static str>)> {
|
||||
Some(match id {
|
||||
1 => ("deflate", Some("deflate")),
|
||||
4 => ("SZIP", Some("szip")),
|
||||
307 => ("bzip2", Some("bzip2")),
|
||||
480 => ("pcodec", Some("pcodec")),
|
||||
32000 => ("LZF", Some("lzf")),
|
||||
32001 => ("Blosc", Some("blosc")),
|
||||
32004 => ("LZ4", Some("lz4")),
|
||||
32008 => ("bitshuffle", Some("bitshuffle")),
|
||||
32013 => ("ZFP", None),
|
||||
32015 => ("Zstandard", Some("zstd")),
|
||||
32019 => ("JPEG", None),
|
||||
32022 => ("BitGroom", None),
|
||||
32023 => ("Granular BitRound", None),
|
||||
32026 => ("Blosc2", None),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a chunk filtered with `id` can be decoded: a built-in filter or a
|
||||
/// registered one.
|
||||
pub fn is_filter_available(id: u16) -> bool {
|
||||
@@ -344,6 +368,23 @@ mod tests {
|
||||
assert!(builtin_filter(FILTER_SHUFFLE).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_filter_error_names_the_filter() {
|
||||
let msg = FormatError::UnsupportedFilter(32026).to_string();
|
||||
assert!(
|
||||
msg.contains("Blosc2") && msg.contains("not implemented"),
|
||||
"{msg}"
|
||||
);
|
||||
let msg = FormatError::UnsupportedFilter(32013).to_string();
|
||||
assert!(msg.contains("ZFP"), "{msg}");
|
||||
let msg = FormatError::UnsupportedFilter(32000).to_string();
|
||||
assert!(msg.contains("LZF") && msg.contains("`lzf`"), "{msg}");
|
||||
assert_eq!(
|
||||
FormatError::UnsupportedFilter(399).to_string(),
|
||||
"unsupported filter: 399"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_table_is_sorted_and_unique() {
|
||||
let ids: Vec<u16> = builtin_filters().iter().map(|f| f.id).collect();
|
||||
|
||||
@@ -380,3 +380,38 @@ fn lzf_written_by_clawhdf5_reads_in_h5py() {
|
||||
ds.with_lzf().without_shuffle();
|
||||
});
|
||||
}
|
||||
|
||||
/// Blosc2 and ZFP are not implemented: reading them must be a clear error
|
||||
/// naming the filter, never data.
|
||||
#[test]
|
||||
fn unimplemented_filters_are_a_clear_error() {
|
||||
if !have_python("h5py, hdf5plugin") {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("unimplemented.h5");
|
||||
run_python(
|
||||
r#"
|
||||
import sys
|
||||
import numpy as np, h5py, hdf5plugin
|
||||
with h5py.File(sys.argv[1], 'w') as f:
|
||||
d = np.arange(4096, dtype='<f4').reshape(64, 64)
|
||||
f.create_dataset('blosc2', data=d, chunks=(16, 16), **hdf5plugin.Blosc2())
|
||||
f.create_dataset('zfp', data=d, chunks=(16, 16), **hdf5plugin.Zfp(reversible=True))
|
||||
"#,
|
||||
&[path.to_str().unwrap()],
|
||||
);
|
||||
let file = File::open(&path).unwrap();
|
||||
for (name, id, label) in [("blosc2", 32026u16, "Blosc2"), ("zfp", 32013, "ZFP")] {
|
||||
let err = file
|
||||
.dataset(name)
|
||||
.unwrap()
|
||||
.read_selection(&Selection::All)
|
||||
.expect_err("an unimplemented filter must not read");
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains(&id.to_string()) && msg.contains(label),
|
||||
"{name}: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user