From e7a7951f1eb93fc67a9c3b9b1d0a102b6a0fbabd Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 00:11:57 -0500 Subject: [PATCH] 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) --- crates/clawhdf5-format/src/error.rs | 14 +++++-- crates/clawhdf5-format/src/filter_registry.rs | 41 +++++++++++++++++++ .../clawhdf5/tests/plugin_filters_interop.rs | 35 ++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/crates/clawhdf5-format/src/error.rs b/crates/clawhdf5-format/src/error.rs index bb81939..b4c3f18 100644 --- a/crates/clawhdf5-format/src/error.rs +++ b/crates/clawhdf5-format/src/error.rs @@ -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}") } diff --git a/crates/clawhdf5-format/src/filter_registry.rs b/crates/clawhdf5-format/src/filter_registry.rs index acbf71a..62e22b8 100644 --- a/crates/clawhdf5-format/src/filter_registry.rs +++ b/crates/clawhdf5-format/src/filter_registry.rs @@ -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 = builtin_filters().iter().map(|f| f.id).collect(); diff --git a/crates/clawhdf5/tests/plugin_filters_interop.rs b/crates/clawhdf5/tests/plugin_filters_interop.rs index fc44e92..647c007 100644 --- a/crates/clawhdf5/tests/plugin_filters_interop.rs +++ b/crates/clawhdf5/tests/plugin_filters_interop.rs @@ -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='