pub mod attrs;
pub mod data;
pub mod file;
pub mod graph;
pub mod ops;
mod window;
pub use attrs::AttrValue;
pub use hdf5::{Dataset, File, Group, Location};
pub fn init() {
static PLUGINS: std::sync::Once = std::sync::Once::new();
hdf5::sync::sync(|| {
unsafe {
medh5_sys::register_blosc2_filter();
medh5_sys::register_zstd_filter();
}
PLUGINS.call_once(|| {
let path = std::env::var_os(plugins::SEARCH_PATH);
let allow = std::env::var_os(plugins::ALLOW);
plugins::settle(path.as_deref(), allow.as_deref());
});
});
}
pub(crate) mod plugins {
use std::ffi::{CStr, OsStr};
use std::os::raw::{c_int, c_uint};
use crate::h5sys::h5pl;
pub(crate) const SEARCH_PATH: &str = "HDF5_PLUGIN_PATH";
pub(crate) const ALLOW: &str = "MEDH5_ALLOW_PLUGINS";
const STANDARD: &CStr =
if cfg!(windows) { c"%ALLUSERSPROFILE%\\hdf5\\lib\\plugin" } else { c"/usr/local/hdf5/lib/plugin" };
unsafe extern "C" {
fn H5PLset_loading_state(plugin_control_mask: c_uint) -> c_int;
#[cfg(test)]
fn H5PLget_loading_state(plugin_control_mask: *mut c_uint) -> c_int;
}
pub(crate) fn wanted(search_path: Option<&OsStr>, allow: Option<&OsStr>) -> bool {
search_path.is_some_and(|p| !p.is_empty()) || allow.is_some_and(|a| !a.is_empty() && a != "0")
}
pub(super) fn settle(search_path: Option<&OsStr>, allow: Option<&OsStr>) {
if search_path.is_some_and(|p| !p.is_empty()) {
return;
}
let wanted = wanted(search_path, allow);
unsafe {
if !wanted {
H5PLset_loading_state(0);
}
let mut n: c_uint = 0;
if h5pl::H5PLsize(&mut n) >= 0 {
for _ in 0..n {
h5pl::H5PLremove(0);
}
}
if wanted {
h5pl::H5PLappend(STANDARD.as_ptr());
}
}
}
#[cfg(test)]
pub(crate) fn state() -> (u32, Vec<String>) {
super::locked(|| {
let mut mask: c_uint = 0;
let mut n: c_uint = 0;
unsafe {
H5PLget_loading_state(&mut mask);
h5pl::H5PLsize(&mut n);
let paths = (0..n)
.filter_map(|i| {
let len = h5pl::H5PLget(i, std::ptr::null_mut(), 0);
let mut buf = vec![0u8; usize::try_from(len).ok()? + 1];
h5pl::H5PLget(i, buf.as_mut_ptr().cast(), buf.len());
CStr::from_bytes_until_nul(&buf).ok().map(|s| s.to_string_lossy().into_owned())
})
.collect();
(mask, paths)
}
})
}
}
pub const CLOSED: &str = "the file this object was read from has been closed";
pub fn alive(obj: &hdf5::Location) -> crate::Result<()> {
if obj.is_valid() {
Ok(())
} else {
Err(crate::Error::File(CLOSED.into()))
}
}
pub(crate) fn locked<T>(f: impl FnOnce() -> T) -> T {
hdf5::sync::sync(f)
}
#[doc(hidden)]
pub fn spawn(command: &mut std::process::Command) -> std::io::Result<std::process::Child> {
locked(|| {
let mut child = command.spawn()?;
settle(&mut child);
Ok(child)
})
}
#[cfg(target_os = "linux")]
fn settle(child: &mut std::process::Child) {
use std::os::unix::io::RawFd;
use std::path::PathBuf;
let entries = |dir: &str| -> Vec<(RawFd, PathBuf)> {
let Ok(read) = std::fs::read_dir(dir) else { return Vec::new() };
read.flatten()
.filter_map(|e| Some((e.file_name().to_str()?.parse().ok()?, std::fs::read_link(e.path()).ok()?)))
.collect()
};
let ours: Vec<(RawFd, PathBuf)> = entries("/proc/self/fd")
.into_iter()
.filter(|(fd, _)| unsafe { libc::fcntl(*fd, libc::F_GETFD) } & libc::FD_CLOEXEC != 0)
.collect();
let theirs = format!("/proc/{}/fd", child.id());
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
while std::time::Instant::now() < deadline && !matches!(child.try_wait(), Ok(Some(_)) | Err(_)) {
if !entries(&theirs).iter().any(|entry| ours.contains(entry)) {
return;
}
std::thread::sleep(std::time::Duration::from_micros(50));
}
}
#[cfg(not(target_os = "linux"))]
fn settle(_: &mut std::process::Child) {}
pub fn relative_name(name: &str, root: &str) -> String {
let base = if root == "/" { "/".to_string() } else { format!("{}/", root.trim_end_matches('/')) };
match name.strip_prefix(&base) {
Some(rest) => rest.to_string(),
None => name.trim_start_matches('/').to_string(),
}
}
pub fn basename(name: &str) -> &str {
name.rsplit('/').next().unwrap_or(name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
const CHILD: &str = "MEDH5_TEST_PLUGINS_CHILD";
#[test]
fn s14_2_dynamically_loaded_filters_are_off_unless_asked_for() {
let set = |value: &'static str| Some(OsStr::new(value));
assert!(!plugins::wanted(None, None));
assert!(!plugins::wanted(set(""), set("")));
assert!(!plugins::wanted(None, set("0")));
assert!(plugins::wanted(None, set("1")));
assert!(plugins::wanted(set("/opt/plugins"), None));
init();
let path = std::env::var_os(plugins::SEARCH_PATH);
let allow = std::env::var_os(plugins::ALLOW);
if !plugins::wanted(path.as_deref(), allow.as_deref()) {
assert_eq!(plugins::state(), (0, Vec::new()));
}
}
#[test]
fn s14_2_plugins_asked_for_load_from_the_standard_directory_only() {
let standard = if cfg!(windows) {
format!("{}\\hdf5\\lib\\plugin", std::env::var("ALLUSERSPROFILE").unwrap_or_default())
} else {
"/usr/local/hdf5/lib/plugin".to_string()
};
if std::env::var_os(CHILD).is_some() {
init();
let (mask, paths) = plugins::state();
assert_ne!(mask, 0);
assert_eq!(paths, vec![standard]);
return;
}
let name = "h5::tests::s14_2_plugins_asked_for_load_from_the_standard_directory_only";
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.args(["--exact", name, "--test-threads=1"])
.env(CHILD, "1")
.env(plugins::ALLOW, "1")
.env_remove(plugins::SEARCH_PATH)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let run = super::spawn(&mut command).unwrap().wait_with_output().unwrap();
let out = String::from_utf8_lossy(&run.stdout);
assert!(run.status.success() && out.contains("1 passed"), "{out}{}", String::from_utf8_lossy(&run.stderr));
}
#[test]
fn s14_2_a_filter_nobody_registered_is_a_read_error() {
use crate::h5sys::{h5d, h5p, h5s, h5t, h5z};
init();
let dir = tempfile::tempdir().unwrap();
let file = file::create_truncate(&dir.path().join("unknown.h5")).unwrap();
let (dims, bytes) = ([4u64, 4], [7u8; 16]);
let id = locked(|| unsafe {
let dcpl = h5p::H5Pcreate(*h5p::H5P_CLS_DATASET_CREATE);
h5p::H5Pset_chunk(dcpl, 2, dims.as_ptr());
h5p::H5Pset_filter(dcpl, 40000, h5z::H5Z_FLAG_OPTIONAL, 0, std::ptr::null());
let space = h5s::H5Screate_simple(2, dims.as_ptr(), std::ptr::null());
let id = h5d::H5Dcreate2(
file.id(),
c"x".as_ptr(),
*h5t::H5T_NATIVE_UCHAR,
space,
h5p::H5P_DEFAULT,
dcpl,
h5p::H5P_DEFAULT,
);
h5s::H5Sclose(space);
h5p::H5Pclose(dcpl);
assert!(id >= 0);
let origin = [0u64, 0];
assert!(
h5d::H5Dwrite_chunk(id, h5p::H5P_DEFAULT, 0, origin.as_ptr(), bytes.len(), bytes.as_ptr().cast()) >= 0
);
h5d::H5Dclose(id)
});
assert!(id >= 0);
let err = data::read(&file.dataset("x").unwrap()).unwrap_err();
assert!(matches!(err, crate::Error::Io(_)), "{err:?}");
let path = std::env::var_os(plugins::SEARCH_PATH);
let allow = std::env::var_os(plugins::ALLOW);
if !plugins::wanted(path.as_deref(), allow.as_deref()) {
assert!(err.to_string().ends_with("filter plugins disabled"), "{err}");
}
}
}