use std::collections::{HashMap, HashSet};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::path::Path;
use std::sync::Mutex;
use crate::h5sys::{h5, h5a, h5d, h5i, h5l, h5o, h5p, h5t};
use crate::json::repr_str;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
Group,
Dataset,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkKind {
Hard,
Soft,
External,
Other,
}
fn cstring(name: &str) -> Result<CString> {
CString::new(name).map_err(|_| Error::Value(format!("name {} contains a NUL byte", repr_str(name))))
}
pub fn link_kind(group: &hdf5::Group, name: &str) -> Option<LinkKind> {
let cname = cstring(name).ok()?;
super::locked(|| unsafe {
if h5l::H5Lexists(group.id(), cname.as_ptr(), h5p::H5P_DEFAULT) <= 0 {
return None;
}
let mut info: h5l::H5L_info2_t = std::mem::zeroed();
if h5l::H5Lget_info2(group.id(), cname.as_ptr(), &mut info, h5p::H5P_DEFAULT) < 0 {
return None;
}
Some(match info.type_ {
h5l::H5L_type_t::H5L_TYPE_HARD => LinkKind::Hard,
h5l::H5L_type_t::H5L_TYPE_SOFT => LinkKind::Soft,
h5l::H5L_type_t::H5L_TYPE_EXTERNAL => LinkKind::External,
_ => LinkKind::Other,
})
})
}
pub fn node_kind(group: &hdf5::Group, name: &str) -> Option<NodeKind> {
match link_kind(group, name)? {
LinkKind::External => return Some(NodeKind::Other),
LinkKind::Other => return Some(NodeKind::Other),
_ => {}
}
match group.loc_type_by_name(name) {
Ok(hdf5::LocationType::Group) => Some(NodeKind::Group),
Ok(hdf5::LocationType::Dataset) => Some(NodeKind::Dataset),
Ok(_) => Some(NodeKind::Other),
Err(_) => None,
}
}
pub fn exists(group: &hdf5::Group, name: &str) -> bool {
link_kind(group, name).is_some()
}
pub fn is_group(group: &hdf5::Group, name: &str) -> bool {
node_kind(group, name) == Some(NodeKind::Group)
}
pub fn is_dataset(group: &hdf5::Group, name: &str) -> bool {
node_kind(group, name) == Some(NodeKind::Dataset)
}
pub fn members(group: &hdf5::Group) -> Result<Vec<String>> {
super::alive(group)?;
let mut names = group.member_names()?;
names.sort();
Ok(names)
}
pub fn child_group(group: &hdf5::Group, name: &str) -> Option<hdf5::Group> {
if is_group(group, name) {
group.group(name).ok()
} else {
None
}
}
pub fn child_dataset(group: &hdf5::Group, name: &str) -> Option<hdf5::Dataset> {
if is_dataset(group, name) {
group.dataset(name).ok()
} else {
None
}
}
pub fn resolve_dataset(group: &hdf5::Group, path: &str) -> Option<hdf5::Dataset> {
let (parent, leaf) = match path.trim_matches('/').rsplit_once('/') {
Some((parent, leaf)) => (resolve_group(group, parent)?, leaf),
None => (group.clone(), path.trim_matches('/')),
};
child_dataset(&parent, leaf)
}
fn resolve_group(group: &hdf5::Group, path: &str) -> Option<hdf5::Group> {
path.split('/').filter(|p| !p.is_empty()).try_fold(group.clone(), |at, name| child_group(&at, name))
}
pub enum Node {
Group(hdf5::Group),
Dataset(hdf5::Dataset),
}
pub type ObjectId = (std::os::raw::c_ulong, [u8; 16]);
const _: () = assert!(std::mem::size_of::<h5o::H5O_token_t>() == 16);
fn object_id(info: &h5o::H5O_info2_t) -> ObjectId {
let token = unsafe { std::mem::transmute::<h5o::H5O_token_t, [u8; 16]>(info.token) };
(info.fileno, token)
}
pub fn object_id_by_name(group: &hdf5::Group, name: &str) -> Option<ObjectId> {
let cname = cstring(name).ok()?;
super::locked(|| unsafe {
let mut info: h5o::H5O_info2_t = std::mem::zeroed();
if h5o::H5Oget_info_by_name3(group.id(), cname.as_ptr(), &mut info, h5o::H5O_INFO_BASIC, h5p::H5P_DEFAULT) < 0 {
return None;
}
Some(object_id(&info))
})
}
pub fn object_id_of(obj: &hdf5::Location) -> Option<ObjectId> {
super::locked(|| unsafe {
let mut info: h5o::H5O_info2_t = std::mem::zeroed();
if h5o::H5Oget_info3(obj.id(), &mut info, h5o::H5O_INFO_BASIC) < 0 {
return None;
}
Some(object_id(&info))
})
}
pub const MAX_DEPTH: usize = 64;
pub fn visit(group: &hdf5::Group, f: &mut dyn FnMut(&str, &Node) -> Result<bool>) -> Result<()> {
visit_links(group, false, f)
}
pub fn visit_resolving(group: &hdf5::Group, f: &mut dyn FnMut(&str, &Node) -> Result<bool>) -> Result<()> {
visit_links(group, true, f)
}
fn visit_links(group: &hdf5::Group, soft: bool, f: &mut dyn FnMut(&str, &Node) -> Result<bool>) -> Result<()> {
fn walk(
group: &hdf5::Group,
prefix: &str,
depth: usize,
soft: bool,
seen: &mut HashSet<ObjectId>,
f: &mut dyn FnMut(&str, &Node) -> Result<bool>,
) -> Result<bool> {
if depth > MAX_DEPTH {
return Err(Error::File(format!(
"{} is nested more than {MAX_DEPTH} groups deep; it is refused rather than followed",
repr_str(&group.name())
)));
}
for name in members(group)? {
let kind = link_kind(group, &name);
if kind != Some(LinkKind::Hard) && !(soft && kind == Some(LinkKind::Soft)) {
continue;
}
match object_id_by_name(group, &name) {
Some(id) if !seen.insert(id) => continue,
None if kind == Some(LinkKind::Soft) => continue,
_ => {}
}
let path = if prefix.is_empty() { name.clone() } else { format!("{prefix}/{name}") };
match node_kind(group, &name) {
Some(NodeKind::Group) => {
let child = group.group(&name)?;
if !f(&path, &Node::Group(child.clone()))? {
return Ok(false);
}
if !walk(&child, &path, depth + 1, soft, seen, f)? {
return Ok(false);
}
}
Some(NodeKind::Dataset) => {
let ds = group.dataset(&name)?;
if !f(&path, &Node::Dataset(ds))? {
return Ok(false);
}
}
_ => {}
}
}
Ok(true)
}
let mut seen: HashSet<ObjectId> = object_id_of(group).into_iter().collect();
walk(group, "", 0, soft, &mut seen, f)?;
Ok(())
}
pub fn datasets(group: &hdf5::Group) -> Result<Vec<(String, hdf5::Dataset)>> {
let mut out = Vec::new();
visit(group, &mut |path, node| {
if let Node::Dataset(ds) = node {
out.push((path.to_string(), ds.clone()));
}
Ok(true)
})?;
Ok(out)
}
pub fn datasets_resolving(group: &hdf5::Group) -> Result<Vec<(String, hdf5::Dataset)>> {
let mut out = Vec::new();
visit_resolving(group, &mut |path, node| {
if let Node::Dataset(ds) = node {
out.push((path.to_string(), ds.clone()));
}
Ok(true)
})?;
Ok(out)
}
pub fn soft_link_target(group: &hdf5::Group, name: &str) -> Result<String> {
let cname = cstring(name)?;
super::locked(|| unsafe {
let mut info: h5l::H5L_info2_t = std::mem::zeroed();
if h5l::H5Lget_info2(group.id(), cname.as_ptr(), &mut info, h5p::H5P_DEFAULT) < 0
|| !matches!(info.type_, h5l::H5L_type_t::H5L_TYPE_SOFT)
{
return Err(Error::Value(format!("{} is not a soft link", repr_str(name))));
}
let size = *info.u.val_size();
let mut buf = vec![0u8; size.max(1)];
if h5l::H5Lget_val(group.id(), cname.as_ptr(), buf.as_mut_ptr().cast(), buf.len(), h5p::H5P_DEFAULT) < 0 {
return Err(Error::Io(format!("could not read the soft link {}", repr_str(name))));
}
let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
Ok(String::from_utf8_lossy(&buf[..end]).into_owned())
})
}
pub fn unlink(group: &hdf5::Group, name: &str) -> Result<()> {
if exists(group, name) {
group.unlink(name)?;
}
Ok(())
}
pub fn hdf5_error_text() -> String {
match hdf5::Error::query() {
Ok(error) => error.to_string(),
Err(_) => "HDF5 gave no reason".to_string(),
}
}
struct VisitState {
found: Vec<(String, String)>,
}
unsafe extern "C" fn visit_link(
group: h5i::hid_t,
name: *const c_char,
info: *const h5l::H5L_info2_t,
data: *mut c_void,
) -> h5::herr_t {
unsafe {
let state = &mut *(data as *mut VisitState);
let text = CStr::from_ptr(name).to_string_lossy().into_owned();
match (*info).type_ {
h5l::H5L_type_t::H5L_TYPE_EXTERNAL => {
state.found.push((text, "an external link".into()));
return 0;
}
h5l::H5L_type_t::H5L_TYPE_HARD => {}
_ => return 0,
}
let mut oinfo: h5o::H5O_info2_t = std::mem::zeroed();
if h5o::H5Oget_info_by_name3(group, name, &mut oinfo, h5o::H5O_INFO_BASIC, h5p::H5P_DEFAULT) < 0 {
return 0;
}
if oinfo.type_ != h5o::H5O_type_t::H5O_TYPE_DATASET {
return 0;
}
let ds = h5d::H5Dopen2(group, name, h5p::H5P_DEFAULT);
if ds < 0 {
return 0;
}
let plist = h5d::H5Dget_create_plist(ds);
if plist >= 0 {
let layout = h5p::H5Pget_layout(plist);
let external = h5p::H5Pget_external_count(plist);
if layout == h5d::H5D_layout_t::H5D_VIRTUAL {
state.found.push((text, "a virtual dataset".into()));
} else if external > 0 {
state.found.push((text, "external raw-data storage".into()));
}
h5p::H5Pclose(plist);
}
h5d::H5Dclose(ds);
0
}
}
fn holds_reference(tid: h5i::hid_t) -> bool {
use h5t::H5T_class_t as Class;
unsafe {
match h5t::H5Tget_class(tid) {
Class::H5T_REFERENCE => true,
Class::H5T_COMPOUND => {
let members = h5t::H5Tget_nmembers(tid).max(0) as u32;
(0..members).any(|i| {
let member = h5t::H5Tget_member_type(tid, i);
if member < 0 {
return false;
}
let found = holds_reference(member);
h5t::H5Tclose(member);
found
})
}
Class::H5T_ARRAY | Class::H5T_VLEN => {
let base = h5t::H5Tget_super(tid);
if base < 0 {
return false;
}
let found = holds_reference(base);
h5t::H5Tclose(base);
found
}
_ => false,
}
}
}
pub fn reference_carriers(root: &hdf5::Group) -> Result<Vec<String>> {
fn committed(tid: h5i::hid_t) -> Option<hdf5::CommittedDatatype> {
if tid < 0 {
return None;
}
let is_committed = super::locked(|| unsafe { h5t::H5Tcommitted(tid) }) > 0;
if !is_committed {
super::locked(|| unsafe { h5t::H5Tclose(tid) });
return None;
}
unsafe { hdf5::from_id::<hdf5::CommittedDatatype>(tid) }.ok()
}
fn type_of(tid: h5i::hid_t, path: &str, types: &mut HashSet<ObjectId>, found: &mut Vec<String>) -> Result<()> {
let Some(named) = committed(tid) else { return Ok(()) };
if object_id_of(&named).is_some_and(|id| !types.insert(id)) {
return Ok(());
}
attributes(&named, &format!("{path} (type)"), types, found)
}
fn attributes(
obj: &hdf5::Location,
path: &str,
types: &mut HashSet<ObjectId>,
found: &mut Vec<String>,
) -> Result<()> {
for name in obj.attr_names()? {
let attr = obj.attr(&name)?;
let dtype = attr.dtype()?;
if super::locked(|| holds_reference(dtype.id())) {
found.push(format!("{path}@{name}"));
}
let tid = super::locked(|| unsafe { h5a::H5Aget_type(attr.id()) });
type_of(tid, &format!("{path}@{name}"), types, found)?;
}
Ok(())
}
fn named_types(
group: &hdf5::Group,
prefix: &str,
types: &mut HashSet<ObjectId>,
found: &mut Vec<String>,
) -> Result<()> {
for name in members(group)? {
if link_kind(group, &name) != Some(LinkKind::Hard)
|| group.loc_type_by_name(&name).ok() != Some(hdf5::LocationType::NamedDatatype)
{
continue;
}
if object_id_by_name(group, &name).is_some_and(|id| !types.insert(id)) {
continue;
}
let path = if prefix.is_empty() { format!("/{name}") } else { format!("/{prefix}/{name}") };
let named = group.committed_datatype(&name)?;
attributes(&named, &path, types, found)?;
}
Ok(())
}
let mut found = Vec::new();
let mut types = HashSet::new();
attributes(root, "/", &mut types, &mut found)?;
named_types(root, "", &mut types, &mut found)?;
visit(root, &mut |name, node| {
let path = format!("/{name}");
match node {
Node::Dataset(ds) => {
let dtype = ds.dtype()?;
if super::locked(|| holds_reference(dtype.id())) {
found.push(path.clone());
}
attributes(ds, &path, &mut types, &mut found)?;
let tid = super::locked(|| unsafe { h5d::H5Dget_type(ds.id()) });
type_of(tid, &path, &mut types, &mut found)?;
}
Node::Group(g) => {
attributes(g, &path, &mut types, &mut found)?;
named_types(g, name, &mut types, &mut found)?;
}
}
Ok(true)
})?;
Ok(found)
}
pub fn refuse_references(root: &hdf5::Group, action: &str) -> Result<()> {
let found = reference_carriers(root)?;
if found.is_empty() {
return Ok(());
}
let named = found.iter().take(5).cloned().collect::<Vec<_>>().join(", ");
let more = if found.len() > 5 { format!(" (and {} more)", found.len() - 5) } else { String::new() };
Err(Error::File(format!(
"{} holds HDF5 references ({named}{more}). A reference is an address in the file that holds it, and \
{action} writes a new file, where it would point at whatever lands at that address; MEDH5 stores no \
references and does not rewrite another tool's, so the file is refused --- remove them, or rewrite it \
with the tool that wrote them",
repr_str(&root.filename())
)))
}
pub fn outside_references(handle: &hdf5::File) -> Result<Vec<(String, String)>> {
let mut state = VisitState { found: Vec::new() };
let root = cstring("/")?;
let status = super::locked(|| unsafe {
h5l::H5Lvisit_by_name2(
handle.id(),
root.as_ptr(),
crate::h5sys::h5::H5_index_t::H5_INDEX_NAME,
crate::h5sys::h5::H5_iter_order_t::H5_ITER_INC,
Some(visit_link),
(&mut state as *mut VisitState).cast(),
h5p::H5P_DEFAULT,
)
});
if status < 0 {
return Err(Error::File(format!(
"{} could not be walked to check that it is self-contained",
repr_str(&handle.filename())
)));
}
Ok(state.found)
}
type FileIdentity = (u64, u64, u64, i128, i128);
fn checked_files() -> &'static Mutex<HashMap<FileIdentity, ()>> {
static CHECKED: std::sync::OnceLock<Mutex<HashMap<FileIdentity, ()>>> = std::sync::OnceLock::new();
CHECKED.get_or_init(|| Mutex::new(HashMap::new()))
}
const CHECKED_LIMIT: usize = 65_536;
#[cfg(unix)]
fn identity(meta: &std::fs::Metadata) -> Option<FileIdentity> {
use std::os::unix::fs::MetadataExt;
Some((
meta.dev(),
meta.ino(),
meta.size(),
meta.mtime() as i128 * 1_000_000_000 + meta.mtime_nsec() as i128,
meta.ctime() as i128 * 1_000_000_000 + meta.ctime_nsec() as i128,
))
}
#[cfg(unix)]
fn descriptor(handle: &hdf5::File) -> Option<std::os::raw::c_int> {
use hdf5::file::FileDriver;
if !matches!(handle.access_plist().ok()?.get_driver().ok()?, FileDriver::Sec2) {
return None;
}
super::locked(|| unsafe {
let mut found: *mut c_void = std::ptr::null_mut();
if crate::h5sys::h5f::H5Fget_vfd_handle(handle.id(), h5p::H5P_DEFAULT, &mut found) < 0 || found.is_null() {
return None;
}
Some(*found.cast::<std::os::raw::c_int>())
})
}
fn opened_identity(handle: &hdf5::File, path: Option<&Path>) -> Option<FileIdentity> {
#[cfg(unix)]
{
use std::os::unix::io::FromRawFd;
let _ = path;
let fd = descriptor(handle)?;
let file = std::mem::ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) });
identity(&file.metadata().ok()?)
}
#[cfg(not(unix))]
{
let _ = (handle, path);
None
}
}
pub fn check_self_contained(handle: &hdf5::File, path: Option<&Path>) -> Result<()> {
let key = opened_identity(handle, path);
if let Some(k) = key {
if checked_files().lock().unwrap().contains_key(&k) {
return Ok(());
}
}
let found = outside_references(handle)?;
if !found.is_empty() {
let named: Vec<String> = found.iter().take(5).map(|(n, w)| format!("/{n} is {w}")).collect();
let more = if found.len() > 5 { format!(" (and {} more)", found.len() - 5) } else { String::new() };
let where_ = path.map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| handle.filename());
return Err(Error::File(format!(
"{} is not self-contained: {}{more}. A MEDH5 file holds its own bytes (ยง2); following \
these would read files on this machine that the file's author chose, so the file is refused",
repr_str(&where_),
named.join("; ")
)));
}
if let Some(k) = key {
let mut map = checked_files().lock().unwrap();
if map.len() >= CHECKED_LIMIT {
map.clear();
}
map.insert(k, ());
}
Ok(())
}
pub fn read_raw_chunk(ds: &hdf5::Dataset, offset: &[u64]) -> Result<(u32, Vec<u8>)> {
super::alive(ds)?;
if offset.len() != ds.ndim() {
return Err(Error::Value(format!(
"a chunk offset names one coordinate per axis: {} has {} axes, the offset {offset:?} has {}",
ds.name(),
ds.ndim(),
offset.len()
)));
}
if ds.chunk().is_none() {
return Err(Error::Value(format!("{} is not chunked, so it stores no chunks", ds.name())));
}
let file = ds.file()?;
super::locked(|| unsafe {
let mut mask: u32 = 0;
let mut addr: u64 = 0;
let mut nbytes: u64 = 0;
if h5d::H5Dget_chunk_info_by_coord(ds.id(), offset.as_ptr(), &mut mask, &mut addr, &mut nbytes) < 0 {
return Err(Error::Io(format!("no stored chunk at {offset:?} in {}", ds.name())));
}
let file_bytes = file.size();
let mut buf = Vec::new();
let reserved = usize::try_from(nbytes)
.ok()
.filter(|n| nbytes <= file_bytes && super::data::holdable(&[*n], 1) && buf.try_reserve_exact(*n).is_ok());
let Some(nbytes) = reserved else {
return Err(Error::Io(format!(
"the chunk at {offset:?} in {} declares {nbytes} stored bytes, more than the file's {file_bytes}",
ds.name()
)));
};
buf.resize(nbytes, 0);
let mut filter_mask: u32 = 0;
let mut size = nbytes;
if h5d::H5Dread_chunk(
ds.id(),
h5p::H5P_DEFAULT,
offset.as_ptr(),
&mut filter_mask,
buf.as_mut_ptr().cast(),
&mut size,
) < 0
{
return Err(Error::Io(format!("could not read the chunk at {offset:?} in {}", ds.name())));
}
buf.truncate(size);
Ok((filter_mask, buf))
})
}
struct ChunkOffsets {
rank: usize,
found: Vec<Vec<u64>>,
}
unsafe extern "C" fn chunk_offset(
offset: *const u64,
_filter_mask: std::os::raw::c_uint,
_addr: u64,
_size: u64,
data: *mut c_void,
) -> std::os::raw::c_int {
unsafe {
let state = &mut *(data as *mut ChunkOffsets);
state.found.push(std::slice::from_raw_parts(offset, state.rank).to_vec());
}
0
}
pub fn stored_chunks(ds: &hdf5::Dataset) -> Result<Vec<Vec<u64>>> {
super::alive(ds)?;
let mut state = ChunkOffsets { rank: ds.ndim(), found: Vec::new() };
let status = super::locked(|| unsafe {
h5d::H5Dchunk_iter(ds.id(), h5p::H5P_DEFAULT, Some(chunk_offset), (&mut state as *mut ChunkOffsets).cast())
});
if status < 0 {
return Err(Error::Io(format!("could not list the chunks of {}", ds.name())));
}
Ok(state.found)
}
#[cfg(test)]
mod tests {
use super::*;
fn plain(path: &Path) {
let file = crate::h5::file::create_truncate(path).unwrap();
file.new_dataset::<u8>().shape([4]).create("data").unwrap().write(&[1u8, 2, 3, 4]).unwrap();
}
fn linked(path: &Path, other: &Path) {
plain(path);
let file = hdf5::File::open_rw(path).unwrap();
file.link_external(&other.to_string_lossy(), "/data", "x_link").unwrap();
}
#[cfg(unix)]
#[test]
fn s2_the_check_is_remembered_for_the_file_it_read() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.medh5");
let bad = dir.path().join("bad.medh5");
plain(&target);
linked(&bad, &target);
let handle = hdf5::File::open(&target).unwrap();
std::fs::rename(&bad, &target).unwrap(); check_self_contained(&handle, Some(&target)).unwrap(); drop(handle);
let err = crate::h5::file::open_read(&target).unwrap_err();
assert!(err.to_string().contains("not self-contained"), "{err}");
assert!(err.to_string().contains("external link"), "{err}");
}
#[test]
fn s2_2_a_chunk_offset_has_one_coordinate_per_axis() {
let dir = tempfile::tempdir().unwrap();
let file = hdf5::File::create(dir.path().join("c.h5")).unwrap();
let ds = file.new_dataset::<u8>().chunk((2, 2)).shape((4, 4)).create("c").unwrap();
ds.write(&ndarray::Array2::<u8>::ones((4, 4))).unwrap();
for wrong in [&[][..], &[0][..], &[0, 0, 0][..]] {
assert!(read_raw_chunk(&ds, wrong).is_err(), "{wrong:?}");
}
assert_eq!(read_raw_chunk(&ds, &[0, 2]).unwrap().1, vec![1u8; 4]);
let flat = file.new_dataset::<u8>().shape([4]).create("flat").unwrap();
assert!(read_raw_chunk(&flat, &[0]).is_err(), "a contiguous dataset stores no chunks");
}
#[test]
fn s14_1_the_stored_chunks_are_listed_in_one_walk() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::create_truncate(&dir.path().join("c.h5")).unwrap();
let ds = file.new_dataset::<u8>().chunk((1, 1)).shape((100, 100)).create("c").unwrap();
ds.write(&ndarray::Array2::<u8>::ones((100, 100))).unwrap();
let sparse = file.new_dataset::<u8>().chunk((2, 2)).shape((6, 6)).create("sparse").unwrap();
sparse.write_slice(&ndarray::Array2::<u8>::ones((2, 2)), (2..4, 4..6)).unwrap();
let started = std::time::Instant::now();
let all = stored_chunks(&ds).unwrap();
let elapsed = started.elapsed();
assert_eq!(all.len(), 10_000);
let distinct: HashSet<Vec<u64>> = all.into_iter().collect();
assert_eq!(distinct.len(), 10_000);
assert!(distinct.contains(&vec![99, 0]) && distinct.contains(&vec![0, 99]));
assert!(elapsed < std::time::Duration::from_secs(3), "{elapsed:?} to list 10 000 chunks");
assert_eq!(stored_chunks(&sparse).unwrap(), vec![vec![2, 4]]);
}
#[test]
fn the_text_of_an_hdf5_error_names_the_failed_call() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::create_truncate(&dir.path().join("e.h5")).unwrap();
let name = cstring("absent").unwrap();
let text = super::super::locked(|| {
let id = unsafe { h5d::H5Dopen2(file.id(), name.as_ptr(), h5p::H5P_DEFAULT) };
assert!(id < 0);
hdf5_error_text()
});
assert!(text.contains("H5Dopen2"), "{text}");
}
#[cfg(unix)]
#[test]
fn s2_an_unchanged_file_is_remembered() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("clean.medh5");
plain(&path);
let handle = crate::h5::file::open_read(&path).unwrap();
let key = opened_identity(&handle, Some(&path)).expect("a sec2 file has an identity");
assert!(checked_files().lock().unwrap().contains_key(&key));
}
#[cfg(not(unix))]
#[test]
fn s2_without_an_identity_every_open_is_checked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("clean.medh5");
plain(&path);
let handle = crate::h5::file::open_read(&path).unwrap();
assert!(opened_identity(&handle, Some(&path)).is_none());
}
}