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:
osobh
2026-09-26 00:11:57 -05:00
co-authored by Claude Opus 5.5
parent 1f71f3bcbc
commit e7a7951f1e
3 changed files with 87 additions and 3 deletions
+11 -3
View File
@@ -406,9 +406,17 @@ impl fmt::Display for FormatError {
FormatError::InvalidFilterPipelineVersion(v) => { FormatError::InvalidFilterPipelineVersion(v) => {
write!(f, "invalid filter pipeline version: {v}") write!(f, "invalid filter pipeline version: {v}")
} }
FormatError::UnsupportedFilter(id) => { FormatError::UnsupportedFilter(id) => match crate::filter_registry::known_filter(*id) {
write!(f, "unsupported 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) => { FormatError::FilterError(msg) => {
write!(f, "filter error: {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) 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 /// Whether a chunk filtered with `id` can be decoded: a built-in filter or a
/// registered one. /// registered one.
pub fn is_filter_available(id: u16) -> bool { pub fn is_filter_available(id: u16) -> bool {
@@ -344,6 +368,23 @@ mod tests {
assert!(builtin_filter(FILTER_SHUFFLE).is_some()); 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] #[test]
fn builtin_table_is_sorted_and_unique() { fn builtin_table_is_sorted_and_unique() {
let ids: Vec<u16> = builtin_filters().iter().map(|f| f.id).collect(); 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(); 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}"
);
}
}