use std::path::{Path, PathBuf};
use hdf5::filters::Filter;
use super::build::{base, mutate, SHAPE};
use crate::array::{DType, NdArray};
use crate::h5::attrs::{self, AttrValue};
use crate::h5::data::{self, Layout};
use crate::h5::ops::{self, MAX_DEPTH};
use crate::h5sys::{h5d, h5p, h5s, h5t, h5z};
use crate::{Error, Result};
const GROUP: &str = "x_robustness";
fn any_digest() -> AttrValue {
AttrValue::Str(format!("sha256:{}", "0".repeat(64)))
}
type Plant = fn(&hdf5::Group) -> Result<()>;
#[doc(hidden)]
pub fn write(dir: &Path) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(dir)?;
let fixtures: [(&str, Plant); 4] = [
("deep-groups", deep_groups),
("unregistered-filter", unregistered_filter),
("zstd-frame-larger-than-its-chunk", zstd_bomb),
("blosc2-chunk-larger-than-its-dataset-chunk", blosc2_overclaim),
];
let mut out = Vec::new();
for (name, plant) in fixtures {
let path = dir.join(format!("{name}.medh5"));
if path.exists() {
std::fs::remove_file(&path)?;
}
base(&path, &SHAPE)?;
mutate(&path, plant)?;
out.push(path);
}
Ok(out)
}
fn deep_groups(root: &hdf5::Group) -> Result<()> {
let mut group = ops::new_group(root, GROUP)?;
for _ in 0..64 * MAX_DEPTH {
group = ops::new_group(&group, "a")?;
}
Ok(())
}
fn planted(root: &hdf5::Group, len: usize, filters: Vec<Filter>) -> Result<hdf5::Dataset> {
let group = ops::new_group(root, GROUP)?;
let layout = Layout { chunks: Some(vec![len]), filters };
let ds = data::create_empty(&group, "data", DType::U8, &[len], &layout)?;
attrs::write(&ds, "digest", &any_digest())?;
Ok(ds)
}
fn write_raw_chunk(ds: &hdf5::Dataset, bytes: &[u8]) -> Result<()> {
let offset = [0u64];
let status = crate::h5::locked(|| unsafe {
h5d::H5Dwrite_chunk(ds.id(), h5p::H5P_DEFAULT, 0, offset.as_ptr(), bytes.len(), bytes.as_ptr().cast())
});
if status < 0 {
return Err(Error::Io(format!("could not write the chunk of {}", ds.name())));
}
Ok(())
}
fn unregistered_filter(root: &hdf5::Group) -> Result<()> {
let group = ops::new_group(root, GROUP)?;
let dims = [4096u64];
let id = crate::h5::locked(|| unsafe {
let dcpl = h5p::H5Pcreate(*h5p::H5P_CLS_DATASET_CREATE);
h5p::H5Pset_chunk(dcpl, 1, dims.as_ptr());
h5p::H5Pset_filter(dcpl, 40000, h5z::H5Z_FLAG_OPTIONAL, 0, std::ptr::null());
let space = h5s::H5Screate_simple(1, dims.as_ptr(), std::ptr::null());
let id = h5d::H5Dcreate2(
group.id(),
c"data".as_ptr(),
*h5t::H5T_NATIVE_UCHAR,
space,
h5p::H5P_DEFAULT,
dcpl,
h5p::H5P_DEFAULT,
);
h5s::H5Sclose(space);
h5p::H5Pclose(dcpl);
if id >= 0 {
h5d::H5Dclose(id);
}
id
});
if id < 0 {
return Err(Error::Io("could not create a dataset naming filter 40000".into()));
}
let ds = group.dataset("data")?;
attrs::write(&ds, "digest", &any_digest())?;
write_raw_chunk(&ds, &[0x5A; 4096])
}
fn zstd_bomb(root: &hdf5::Group) -> Result<()> {
const BLOCK: u32 = 128 * 1024;
const BLOCKS: usize = 16_384;
let ds = planted(root, 64 * 1024, vec![Filter::user(medh5_sys::ZSTD_FILTER_ID as _, &[3])])?;
let mut frame = 0xFD2F_B528u32.to_le_bytes().to_vec();
frame.extend([0x00, 0x38]);
for i in 0..BLOCKS {
let header = u32::from(i + 1 == BLOCKS) | (1 << 1) | (BLOCK << 3);
frame.extend(&header.to_le_bytes()[..3]);
frame.push(0x41);
}
write_raw_chunk(&ds, &frame)
}
fn blosc2_overclaim(root: &hdf5::Group) -> Result<()> {
let blosc2 = || vec![Filter::user(medh5_sys::BLOSC2_FILTER_ID as _, &[0, 0, 0, 0, 5, 1, 0])];
let large = 4 << 20;
let scratch = ops::new_group(root, "x_scratch")?;
let zeros = NdArray::from_vec(&[large], vec![0f64; large])?.astype(DType::U8);
let source = data::create(&scratch, "zeros", &zeros, &Layout { chunks: Some(vec![large]), filters: blosc2() })?;
let (_, chunk) = ops::read_raw_chunk(&source, &[0])?;
drop(source);
drop(scratch);
root.unlink("x_scratch")?;
let ds = planted(root, 4096, blosc2())?;
write_raw_chunk(&ds, &chunk)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_robustness_fixture_validates_to_an_error() {
let dir = tempfile::tempdir().unwrap();
let paths = write(dir.path()).unwrap();
assert_eq!(paths.len(), 4);
for path in paths {
let started = std::time::Instant::now();
let report = crate::validate::validate_file(&path, "strict", None).unwrap();
let elapsed = started.elapsed();
let errors: Vec<_> = report.diagnostics.iter().filter(|d| d.severity == "error").collect();
let name = path.file_stem().unwrap().to_string_lossy();
assert!(
errors.iter().any(|d| d.code == "E001" || d.location.contains(GROUP) || d.message.contains(GROUP)),
"{name}: {:?}",
report.diagnostics
);
assert!(elapsed < std::time::Duration::from_secs(30), "{name}: {elapsed:?}");
}
}
}