h5rs tools, browser reader, libhdf5 header checks, plugin filters, concurrency benchmark #14

Merged
osobh merged 60 commits from feat/p1-proof into main 2026-09-26 13:14:39 +00:00
2 changed files with 94 additions and 9 deletions
Showing only changes of commit 738b9491b2 - Show all commits
+89 -8
View File
@@ -9,7 +9,11 @@
//! [`builtin_filters`] lists them. //! [`builtin_filters`] lists them.
//! * **Registered filters** (`std` only) — codecs the application supplies //! * **Registered filters** (`std` only) — codecs the application supplies
//! for any other ID with [`register_filter`] (a [`FilterCodec`], or just a //! for any other ID with [`register_filter`] (a [`FilterCodec`], or just a
//! decoding closure). A registered codec cannot shadow a built-in one. //! decoding closure). A registered codec cannot shadow a built-in one,
//! except under 32023: that ID belongs to Granular BitRound, and the
//! built-in entry there only reads the pcodec chunks clawhdf5 <= 2.7.0
//! wrote (filter name `"pcodec"`), so a codec registered for 32023 handles
//! every other chunk with that ID, and writes.
//! //!
//! An ID in neither tier fails with [`FormatError::UnsupportedFilter`], as it //! An ID in neither tier fails with [`FormatError::UnsupportedFilter`], as it
//! always has. //! always has.
@@ -118,6 +122,19 @@ impl BuiltinFilter {
pub fn can_encode(&self) -> bool { pub fn can_encode(&self) -> bool {
self.encode.is_some() self.encode.is_some()
} }
/// Whether the built-in entry only borrows its ID for some chunks, so a
/// registered codec may take the rest: the legacy pcodec entry under
/// Granular BitRound's 32023, which claims only chunks named `"pcodec"`.
fn is_shared(&self) -> bool {
self.id == crate::filter_pipeline::FILTER_PCODEC_LEGACY
}
/// Whether this entry decodes chunks written with `filter`.
fn claims(&self, filter: &crate::filter_pipeline::FilterDescription) -> bool {
!self.is_shared()
|| filter.name.as_deref() == Some(crate::filter_pipeline::FILTER_PCODEC_LEGACY_NAME)
}
} }
/// The filters compiled into this build, in ID order. /// The filters compiled into this build, in ID order.
@@ -198,7 +215,11 @@ mod custom {
/// A plain closure `Fn(&[u8], &FilterContext) -> Result<Vec<u8>, FormatError>` /// A plain closure `Fn(&[u8], &FilterContext) -> Result<Vec<u8>, FormatError>`
/// registers a decoder. Replaces (and returns) an earlier registration for /// registers a decoder. Replaces (and returns) an earlier registration for
/// the same ID. Fails with [`FormatError::FilterError`] if `id` is a built-in /// the same ID. Fails with [`FormatError::FilterError`] if `id` is a built-in
/// filter of this build: those cannot be overridden. /// filter of this build: those cannot be overridden. The exception is 32023
/// (Granular BitRound): with the `pcodec` feature the built-in entry there
/// reads only chunks whose filter is named `"pcodec"` (clawhdf5 <= 2.7.0's
/// files); a codec registered for 32023 decodes every other chunk with that
/// ID and does all the writing.
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub fn register_filter<C>( pub fn register_filter<C>(
id: u16, id: u16,
@@ -207,7 +228,7 @@ pub fn register_filter<C>(
where where
C: FilterCodec + 'static, C: FilterCodec + 'static,
{ {
if let Some(builtin) = builtin_filter(id) { if let Some(builtin) = builtin_filter(id).filter(|b| !b.is_shared()) {
return Err(FormatError::FilterError(format!( return Err(FormatError::FilterError(format!(
"filter {id} ({}) is built in and cannot be re-registered", "filter {id} ({}) is built in and cannot be re-registered",
builtin.name builtin.name
@@ -229,11 +250,13 @@ pub fn registered(id: u16) -> Option<std::sync::Arc<dyn FilterCodec>> {
custom::with_read(|r| r.get(&id).cloned()) custom::with_read(|r| r.get(&id).cloned())
} }
/// Undo filter `ctx.filter` on `input`: the built-in decoder if there is one, /// Undo filter `ctx.filter` on `input`: the built-in decoder if there is one
/// else a registered one, else [`FormatError::UnsupportedFilter`]. /// that claims the chunk, else a registered one, else the built-in decoder's
/// own refusal or [`FormatError::UnsupportedFilter`].
pub(crate) fn decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, FormatError> { pub(crate) fn decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, FormatError> {
let id = ctx.filter.filter_id; let id = ctx.filter.filter_id;
if let Some(builtin) = builtin_filter(id) { let builtin = builtin_filter(id);
if let Some(builtin) = builtin.filter(|b| b.claims(ctx.filter)) {
return (builtin.decode)(input, ctx); return (builtin.decode)(input, ctx);
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
@@ -250,12 +273,21 @@ pub(crate) fn decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, F
} }
return Ok(out); return Ok(out);
} }
Err(FormatError::UnsupportedFilter(id)) match builtin {
Some(builtin) => (builtin.decode)(input, ctx),
None => Err(FormatError::UnsupportedFilter(id)),
}
} }
/// Apply filter `ctx.filter` to `input`. /// Apply filter `ctx.filter` to `input`.
pub(crate) fn encode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, FormatError> { pub(crate) fn encode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, FormatError> {
let id = ctx.filter.filter_id; let id = ctx.filter.filter_id;
#[cfg(feature = "std")]
if builtin_filter(id).is_some_and(|b| b.is_shared())
&& let Some(codec) = registered(id)
{
return codec.encode(input, ctx);
}
if let Some(builtin) = builtin_filter(id) { if let Some(builtin) = builtin_filter(id) {
return match builtin.encode { return match builtin.encode {
Some(encode) => encode(input, ctx), Some(encode) => encode(input, ctx),
@@ -270,7 +302,7 @@ pub(crate) fn encode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<u8>, F
} }
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::filter_pipeline::{FILTER_FLETCHER32, FILTER_SHUFFLE, FilterPipeline}; use crate::filter_pipeline::{FILTER_FLETCHER32, FILTER_SHUFFLE, FilterPipeline};
use crate::filters::{compress_chunk, decompress_chunk}; use crate::filters::{compress_chunk, decompress_chunk};
@@ -368,6 +400,55 @@ mod tests {
assert!(builtin_filter(FILTER_SHUFFLE).is_some()); assert!(builtin_filter(FILTER_SHUFFLE).is_some());
} }
/// Serialises the tests that register or read filter 32023 (the
/// registry is process-wide).
pub(crate) static ID_32023: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// 32023 is Granular BitRound's ID; the `pcodec` build's built-in entry
/// there reads only clawhdf5 <= 2.7.0's pcodec chunks (named "pcodec"),
/// so a codec can be registered for the rest, and writes with it.
#[test]
fn a_codec_can_be_registered_for_granular_bitround() {
let _guard = ID_32023
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let named = |name: Option<&str>| FilterPipeline {
version: 2,
filters: vec![FilterDescription {
filter_id: 32023,
name: name.map(Into::into),
flags: 0,
client_data: vec![7],
}],
};
let prev = register_filter(32023, Xor).expect("32023 must be registrable");
assert!(prev.is_none());
let data = b"granular bitround".to_vec();
for name in [None, Some("granular_bitround"), Some("test")] {
let pl = named(name);
let enc = compress_chunk(&data, &pl, 1).unwrap();
assert_ne!(enc, data);
assert_eq!(decompress_chunk(&enc, &pl, data.len(), 1).unwrap(), data);
}
// clawhdf5 <= 2.7.0's pcodec chunks still go to the built-in reader.
#[cfg(feature = "pcodec")]
{
let raw: Vec<u8> = (0..64)
.flat_map(|i| (f64::from(i) * 0.5).to_le_bytes())
.collect();
let comp = crate::filters::pcodec_compress(&raw, 8).unwrap();
let mut pl = named(Some("pcodec"));
pl.filters[0].client_data = vec![8];
assert_eq!(decompress_chunk(&comp, &pl, raw.len(), 8).unwrap(), raw);
}
assert!(unregister_filter(32023));
let pl = named(None);
assert!(matches!(
decompress_chunk(&data, &pl, data.len(), 1),
Err(FormatError::UnsupportedFilter(32023))
));
}
#[test] #[test]
fn unsupported_filter_error_names_the_filter() { fn unsupported_filter_error_names_the_filter() {
let msg = FormatError::UnsupportedFilter(32026).to_string(); let msg = FormatError::UnsupportedFilter(32026).to_string();
+5 -1
View File
@@ -1446,7 +1446,7 @@ fn fletcher32_append(data: &[u8]) -> Result<Vec<u8>, FormatError> {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[cfg(feature = "pcodec")] #[cfg(feature = "pcodec")]
fn pcodec_compress(data: &[u8], element_size: usize) -> Result<Vec<u8>, FormatError> { pub(crate) fn pcodec_compress(data: &[u8], element_size: usize) -> Result<Vec<u8>, FormatError> {
use pco::ChunkConfig; use pco::ChunkConfig;
use pco::standalone::simple_compress; use pco::standalone::simple_compress;
let config = ChunkConfig::default(); let config = ChunkConfig::default();
@@ -2758,6 +2758,10 @@ mod tests {
#[cfg(feature = "pcodec")] #[cfg(feature = "pcodec")]
fn pcodec_uses_private_id_and_reads_legacy_32023() { fn pcodec_uses_private_id_and_reads_legacy_32023() {
use crate::chunked_write::ChunkOptions; use crate::chunked_write::ChunkOptions;
#[cfg(feature = "std")]
let _guard = crate::filter_registry::tests::ID_32023
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let opts = ChunkOptions { let opts = ChunkOptions {
pcodec: true, pcodec: true,
..Default::default() ..Default::default()