use hdf5::File;
use hdf5::filters::Filter;
use crate::errors::ScDataErrors;
use crate::h5_10x_io::TenxVersion;
use crate::h5ad_io::{H5ADFormat, RawDataSlot};
pub fn h5_filter_name(id: i32) -> &'static str {
match id {
1 => "deflate/gzip",
2 => "shuffle",
3 => "fletcher32",
4 => "szip",
5 => "nbit",
6 => "scaleoffset",
307 => "bzip2",
32000 => "lzf",
32001 => "blosc",
32004 => "lz4",
32008 => "bitshuffle",
32013 => "zfp",
32015 => "zstd",
32017 => "sz",
32026 => "blosc2",
_ => "unknown",
}
}
pub fn unsupported_filters(filters: &[Filter]) -> Vec<i32> {
filters
.iter()
.filter(|f| !Filter::get_info(f.id()).decode_enabled)
.map(|f| f.id())
.collect()
}
pub fn check_h5_filters(file: &File, paths: &[&str]) -> Result<(), ScDataErrors> {
for path in paths {
let Ok(ds) = file.dataset(path) else {
continue;
};
let Ok(dcpl) = ds.dcpl() else {
continue;
};
if dcpl.all_filters_avail() {
continue;
}
if let Some(&id) = unsupported_filters(&dcpl.filters()).first() {
return Err(ScDataErrors::UnsupportedH5Filter {
dataset: (*path).to_string(),
filter_id: id,
filter_name: h5_filter_name(id).to_string(),
});
}
}
Ok(())
}
pub fn check_h5ad_filters(
file: &File,
raw_slot: &RawDataSlot,
format: &H5ADFormat,
) -> Result<(), ScDataErrors> {
match format {
H5ADFormat::Csr | H5ADFormat::Csc => check_h5_filters(
file,
&[
raw_slot.get_data(),
raw_slot.get_indices(),
raw_slot.get_indptr(),
],
),
H5ADFormat::DenseRow | H5ADFormat::DenseCol => {
check_h5_filters(file, &[raw_slot.get_dense_path()])
}
}
}
pub fn check_tenx_filters(file: &File, version: &TenxVersion) -> Result<(), ScDataErrors> {
check_h5_filters(
file,
&[
version.get_data(),
version.get_indices(),
version.get_indptr(),
],
)
}
#[cfg(test)]
mod tests {
use super::*;
struct TempH5(std::path::PathBuf);
impl Drop for TempH5 {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
impl TempH5 {
fn new(name: &str) -> Self {
Self(std::env::temp_dir().join(format!("bixverse_h5_filters_{name}.h5")))
}
}
#[test]
fn test_unsupported_filters_flags_unavailable_ids() {
let pipeline = vec![Filter::Shuffle, Filter::User(32015, vec![])];
assert_eq!(unsupported_filters(&pipeline), vec![32015]);
}
#[test]
fn test_unsupported_filters_accepts_built_ins() {
let pipeline = vec![Filter::Shuffle, Filter::Deflate(4), Filter::Fletcher32];
assert!(unsupported_filters(&pipeline).is_empty());
}
#[test]
fn test_filter_names_cover_the_hdf5plugin_codecs() {
for id in [32000, 32001, 32004, 32008, 32015, 32026] {
assert_ne!(h5_filter_name(id), "unknown", "filter {id}");
}
assert_eq!(h5_filter_name(31999), "unknown");
}
#[test]
fn test_check_h5_filters_accepts_gzip() {
let temp = TempH5::new("gzip");
{
let file = File::create(&temp.0).expect("create");
file.new_dataset_builder()
.shuffle()
.deflate(4)
.chunk(16)
.with_data(&(0..64_i32).collect::<Vec<_>>())
.create("X")
.expect("write dataset");
}
let file = File::open(&temp.0).expect("open");
assert!(check_h5_filters(&file, &["X"]).is_ok());
}
#[test]
fn test_check_h5_filters_skips_missing_datasets() {
let temp = TempH5::new("missing");
{
let file = File::create(&temp.0).expect("create");
file.new_dataset_builder()
.with_data(&[1_i32, 2, 3])
.create("X")
.expect("write dataset");
}
let file = File::open(&temp.0).expect("open");
assert!(check_h5_filters(&file, &["layers/counts/data"]).is_ok());
}
}