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, PoisonError};
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 raw_link_type(&info) {
LINK_HARD => LinkKind::Hard,
LINK_SOFT => LinkKind::Soft,
LINK_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 new_group(parent: &hdf5::Group, name: &str) -> Result<hdf5::Group> {
Ok(parent.create_group_builder().obj_track_times(false).create(name)?)
}
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(),
}
}
type RawLinkType = std::os::raw::c_int;
const LINK_HARD: RawLinkType = h5l::H5L_type_t::H5L_TYPE_HARD as RawLinkType;
const LINK_SOFT: RawLinkType = h5l::H5L_type_t::H5L_TYPE_SOFT as RawLinkType;
const LINK_EXTERNAL: RawLinkType = h5l::H5L_type_t::H5L_TYPE_EXTERNAL as RawLinkType;
unsafe fn raw_link_type(info: *const h5l::H5L_info2_t) -> RawLinkType {
unsafe { std::ptr::addr_of!((*info).type_).cast::<RawLinkType>().read() }
}
unsafe extern "C" fn collect_link(
_group: h5i::hid_t,
name: *const c_char,
info: *const h5l::H5L_info2_t,
data: *mut c_void,
) -> h5::herr_t {
unsafe {
let links = &mut *(data as *mut Vec<(CString, RawLinkType)>);
links.push((CStr::from_ptr(name).to_owned(), raw_link_type(info)));
}
0
}
fn links_of(group: h5i::hid_t) -> Option<Vec<(CString, RawLinkType)>> {
let mut links: Vec<(CString, RawLinkType)> = Vec::new();
let status = unsafe {
h5l::H5Literate2(
group,
crate::h5sys::h5::H5_index_t::H5_INDEX_NAME,
crate::h5sys::h5::H5_iter_order_t::H5_ITER_INC,
std::ptr::null_mut(),
Some(collect_link),
(&mut links as *mut Vec<(CString, RawLinkType)>).cast(),
)
};
(status >= 0).then_some(links)
}
fn reads_outside(parent: h5i::hid_t, name: &CStr) -> Option<&'static str> {
unsafe {
let ds = h5d::H5Dopen2(parent, name.as_ptr(), h5p::H5P_DEFAULT);
if ds < 0 {
return None;
}
let mut found = None;
let plist = h5d::H5Dget_create_plist(ds);
if plist >= 0 {
if h5p::H5Pget_layout(plist) == h5d::H5D_layout_t::H5D_VIRTUAL {
found = Some("a virtual dataset");
} else if h5p::H5Pget_external_count(plist) > 0 {
found = Some("external raw-data storage");
}
h5p::H5Pclose(plist);
}
h5d::H5Dclose(ds);
found
}
}
struct Frame {
group: h5i::hid_t,
path: String,
links: Vec<(CString, RawLinkType)>,
next: usize,
}
impl Drop for Frame {
fn drop(&mut self) {
super::locked(|| unsafe { crate::h5sys::h5g::H5Gclose(self.group) });
}
}
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 unwalkable = || {
Error::File(format!("{} could not be walked to check that it is self-contained", repr_str(&handle.filename())))
};
super::locked(|| {
let root = cstring("/")?;
let group = unsafe { crate::h5sys::h5g::H5Gopen2(handle.id(), root.as_ptr(), h5p::H5P_DEFAULT) };
if group < 0 {
return Err(unwalkable());
}
let mut frame = Frame { group, path: String::new(), links: Vec::new(), next: 0 };
frame.links = links_of(group).ok_or_else(unwalkable)?;
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut info: h5o::H5O_info2_t = unsafe { std::mem::zeroed() };
if unsafe { h5o::H5Oget_info3(group, &mut info, h5o::H5O_INFO_BASIC) } >= 0 {
seen.insert(object_id(&info));
}
let mut stack = vec![frame];
let mut found = Vec::new();
while let Some(top) = stack.last_mut() {
let Some(link) = top.links.get_mut(top.next) else {
stack.pop();
continue;
};
let (name, kind) = std::mem::take(link);
top.next += 1;
let parent = top.group;
let shown = name.to_string_lossy();
let path = if top.path.is_empty() { shown.into_owned() } else { format!("{}/{shown}", top.path) };
if kind == LINK_EXTERNAL {
found.push((path, "an external link".to_string()));
continue;
}
if kind != LINK_HARD {
continue;
}
if unsafe {
h5o::H5Oget_info_by_name3(parent, name.as_ptr(), &mut info, h5o::H5O_INFO_BASIC, h5p::H5P_DEFAULT)
} < 0
{
return Err(unwalkable());
}
if !seen.insert(object_id(&info)) {
continue;
}
match info.type_ {
h5o::H5O_type_t::H5O_TYPE_DATASET => {
if let Some(what) = reads_outside(parent, &name) {
found.push((path, what.to_string()));
}
}
h5o::H5O_type_t::H5O_TYPE_GROUP => {
if stack.len() > MAX_DEPTH {
return Err(Error::File(format!(
"{} in {} is nested more than {MAX_DEPTH} groups deep; it is refused rather than \
followed",
repr_str(&format!("/{path}")),
repr_str(&handle.filename())
)));
}
let group = unsafe { crate::h5sys::h5g::H5Gopen2(parent, name.as_ptr(), h5p::H5P_DEFAULT) };
if group < 0 {
return Err(unwalkable());
}
let mut frame = Frame { group, path, links: Vec::new(), next: 0 };
frame.links = links_of(group).ok_or_else(unwalkable)?;
stack.push(frame);
}
_ => {}
}
}
Ok(found)
})
}
pub(crate) type FileIdentity = (u64, u128, u64, i128, i128);
pub(super) 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)]
pub(crate) fn identity(meta: &std::fs::Metadata) -> Option<FileIdentity> {
use std::os::unix::fs::MetadataExt;
Some((
meta.dev(),
u128::from(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(windows)]
fn windows_identity(handle: windows_sys::Win32::Foundation::HANDLE) -> Option<FileIdentity> {
use std::mem::{size_of, MaybeUninit};
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::{
FileBasicInfo, FileIdInfo, FileStandardInfo, GetFileInformationByHandle, GetFileInformationByHandleEx,
BY_HANDLE_FILE_INFORMATION, FILE_BASIC_INFO, FILE_ID_INFO, FILE_INFO_BY_HANDLE_CLASS, FILE_STANDARD_INFO,
};
unsafe fn info<T>(handle: HANDLE, class: FILE_INFO_BY_HANDLE_CLASS) -> Option<T> {
let mut out = MaybeUninit::<T>::zeroed();
let done =
unsafe { GetFileInformationByHandleEx(handle, class, out.as_mut_ptr().cast(), size_of::<T>() as u32) };
(done != 0).then(|| unsafe { out.assume_init() })
}
let basic: FILE_BASIC_INFO = unsafe { info(handle, FileBasicInfo)? };
let standard: FILE_STANDARD_INFO = unsafe { info(handle, FileStandardInfo)? };
let (volume, file) = match unsafe { info::<FILE_ID_INFO>(handle, FileIdInfo) } {
Some(id) => (id.VolumeSerialNumber, u128::from_le_bytes(id.FileId.Identifier)),
None => {
let mut by = MaybeUninit::<BY_HANDLE_FILE_INFORMATION>::zeroed();
if unsafe { GetFileInformationByHandle(handle, by.as_mut_ptr()) } == 0 {
return None;
}
let by = unsafe { by.assume_init() };
let index = u128::from(by.nFileIndexHigh) << 32 | u128::from(by.nFileIndexLow);
(u64::from(by.dwVolumeSerialNumber), index)
}
};
Some((
volume,
file,
u64::try_from(standard.EndOfFile).ok()?,
i128::from(basic.LastWriteTime),
i128::from(basic.ChangeTime),
))
}
#[cfg(windows)]
fn unshared(path: &Path, id: &FileIdentity) -> bool {
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
let Ok(probe) = std::fs::OpenOptions::new().read(true).share_mode(FILE_SHARE_READ).open(path) else {
return false;
};
windows_identity(probe.as_raw_handle()).is_some_and(|p| (p.0, p.1) == (id.0, id.1))
}
pub(crate) fn opened_identity(handle: &hdf5::File, path: Option<&Path>) -> Option<FileIdentity> {
#[cfg(not(windows))]
let _ = path;
let fd = super::file::descriptor(handle)?;
#[cfg(unix)]
{
use std::os::unix::io::FromRawFd;
let file = std::mem::ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) });
identity(&file.metadata().ok()?)
}
#[cfg(windows)]
{
let id = windows_identity(super::file::os_handle(fd)?)?;
path.is_some_and(|p| unshared(p, &id)).then_some(id)
}
#[cfg(not(any(unix, windows)))]
{
let _ = fd;
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_or_else(PoisonError::into_inner).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_or_else(PoisonError::into_inner);
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 = crate::h5::file::marked(|| 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 = crate::h5::file::marked(|| 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}");
}
fn nested(path: &Path, depth: usize) {
let file = crate::h5::file::create_truncate(path).unwrap();
let mut group = file.create_group("x_deep").unwrap();
for _ in 1..depth {
group = group.create_group("a").unwrap();
}
}
#[test]
fn s2_a_chain_of_groups_past_the_depth_limit_is_refused_quickly() {
let dir = tempfile::tempdir().unwrap();
let deep = dir.path().join("deep.medh5");
nested(&deep, 20_000);
let started = std::time::Instant::now();
let err = crate::h5::file::open_read(&deep).unwrap_err();
let elapsed = started.elapsed();
assert!(matches!(err, Error::File(_)), "{err:?}");
assert!(err.message().contains("nested more than 64 groups deep"), "{err}");
assert!(elapsed < std::time::Duration::from_secs(5), "{elapsed:?} to refuse it");
}
#[test]
fn s2_the_check_and_the_walk_refuse_the_same_depth() {
let dir = tempfile::tempdir().unwrap();
for (depth, opens) in [(MAX_DEPTH, true), (MAX_DEPTH + 1, false)] {
let path = dir.path().join(format!("d{depth}.medh5"));
nested(&path, depth);
let raw = crate::h5::file::marked(|| hdf5::File::open(&path)).unwrap();
let walked = visit(&raw.as_group().unwrap(), &mut |_, _| Ok(true));
assert_eq!(walked.is_ok(), opens, "{depth}: {walked:?}");
drop(raw);
let opened = crate::h5::file::open_read(&path);
assert_eq!(opened.is_ok(), opens, "{depth}: {:?}", opened.err());
}
}
#[test]
fn s2_the_walk_ends_on_a_cycle_and_finds_what_reads_outside() {
let dir = tempfile::tempdir().unwrap();
let other = dir.path().join("other.medh5");
plain(&other);
let path = dir.path().join("cycle.medh5");
plain(&path);
{
let file = crate::h5::file::marked(|| hdf5::File::open_rw(&path)).unwrap();
let inner = file.create_group("x_a").unwrap().create_group("b").unwrap();
inner.link_hard("/", "up").unwrap();
inner.link_external(&other.to_string_lossy(), "/data", "out").unwrap();
file.link_hard("x_a", "x_again").unwrap();
}
let found = outside_references(&crate::h5::file::marked(|| hdf5::File::open(&path)).unwrap()).unwrap();
assert_eq!(found, vec![("x_a/b/out".to_string(), "an external link".to_string())]);
let clean = dir.path().join("clean.medh5");
plain(&clean);
{
let file = crate::h5::file::marked(|| hdf5::File::open_rw(&clean)).unwrap();
file.create_group("x_a").unwrap().link_hard("/", "up").unwrap();
}
crate::h5::file::open_read(&clean).unwrap();
}
#[test]
fn s2_2_a_chunk_offset_has_one_coordinate_per_axis() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::marked(|| 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}");
}
#[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(windows)]
#[test]
fn s2_a_file_another_handle_may_write_is_not_remembered() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("clean.medh5");
plain(&path);
let writer = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
let handle = crate::h5::file::open_read(&path).unwrap();
assert!(opened_identity(&handle, Some(&path)).is_none());
drop(writer);
assert!(opened_identity(&handle, Some(&path)).is_some());
}
#[test]
fn s2_a_file_changed_in_place_is_checked_again() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("clean.medh5");
plain(&path);
let before = {
let handle = crate::h5::file::open_read(&path).unwrap();
opened_identity(&handle, Some(&path)).expect("a sec2 file has an identity")
};
let bytes = std::fs::read(&path).unwrap();
let modified = std::fs::metadata(&path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.write_all(&bytes).unwrap();
file.set_modified(modified).unwrap();
drop(file);
let handle = crate::h5::file::open_read(&path).unwrap();
let after = opened_identity(&handle, Some(&path)).unwrap();
assert_eq!((after.0, after.1, after.2, after.3), (before.0, before.1, before.2, before.3));
assert_ne!(after.4, before.4, "the change time moved");
assert!(checked_files().lock().unwrap().contains_key(&after), "checked under its new identity");
}
}