fix(format): write and read the registered HDF5 LZ4 filter format

Filter 32004 chunks were framed as a 4-byte little-endian size plus one
LZ4 block. That is not the registered HDF5 LZ4 format (H5Zlz4.c: 8-byte
big-endian total size, 4-byte big-endian block size, then per block a
4-byte big-endian compressed length and the block, stored raw when the
length equals the block size), so libhdf5 + hdf5plugin could not read
our LZ4 datasets and we could not read theirs (h5ex_d_lz4.h5:
"lz4: 0 is not a valid match offset").

Write the registered format (cd_values[0] is honoured as the block size,
default 1 GiB like the plugin) and read it, multi-block and raw blocks
included. Chunks in the old framing stay readable: an HDF5 chunk is under
4 GiB, so a registered chunk always starts with four zero bytes and is at
least 12 bytes long, while an old one starts with four zero bytes only
when empty (5 bytes).

Tests: lz4_reads_registered_hdf5_format (chunk of the HDF Group's
h5ex_d_lz4.h5, block size 3), lz4_writes_registered_hdf5_format,
lz4_reads_legacy_clawhdf5_format, and hdf5plugin_reads_our_lz4 (ignored
interop test; failed before with "filter returned failure during read").

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 21:03:49 -05:00
co-authored by Claude Opus 5.5
parent 75bdb53342
commit aef8e766ae
4 changed files with 254 additions and 24 deletions
@@ -940,3 +940,48 @@ fn provenance_verify_written_file() {
.unwrap();
assert_eq!(result, clawhdf5_format::provenance::VerifyResult::Ok);
}
// ---- hdf5plugin interop: registered third-party compression filters ----
/// Write `data` (f64, 1-D, chunked) with `configure` applied, then read it
/// back with h5py + hdf5plugin (libhdf5's registered filter plugins) and
/// return the values it decodes.
#[cfg(any(feature = "lz4", feature = "zstd"))]
fn hdf5plugin_roundtrip(
tag: &str,
data: &[f64],
configure: impl FnOnce(&mut clawhdf5_format::type_builders::DatasetBuilder),
) -> Vec<f64> {
let mut fw = FileWriter::new();
let ds = fw.create_dataset("data");
ds.with_f64_data(data)
.with_shape(&[data.len() as u64])
.with_chunks(&[250]);
configure(ds);
let bytes = fw.finish().unwrap();
let path = std::env::temp_dir().join(format!("clawhdf5_hdf5plugin_{tag}.h5"));
std::fs::write(&path, &bytes).unwrap();
let script = format!(
"import h5py,hdf5plugin,json; f=h5py.File('{}','r'); print(json.dumps(f['data'][:].tolist()))",
path.display()
);
let stdout = h5py_read(&path, &script);
serde_json::from_str(&stdout).unwrap()
}
/// libhdf5's LZ4 plugin must decode what we write (it could not while we
/// wrote a private 4-byte-LE-size framing).
#[cfg(feature = "lz4")]
#[test]
#[ignore = "requires Python h5py + hdf5plugin"]
fn hdf5plugin_reads_our_lz4() {
let data: Vec<f64> = (0..1000).map(|i| (i % 37) as f64 * 0.5).collect();
let got = hdf5plugin_roundtrip("lz4", &data, |ds| {
ds.with_lz4();
});
assert_eq!(got, data);
let got = hdf5plugin_roundtrip("lz4_noshuffle", &data, |ds| {
ds.with_lz4().without_shuffle();
});
assert_eq!(got, data);
}