use std::collections::HashMap;
use std::ffi::CString;
use super::attrs;
use super::ops::{self, LinkKind, NodeKind, ObjectId};
use crate::h5sys::{h5o, h5p};
use crate::json::repr_str;
use crate::{Error, Result};
pub trait CopyHooks {
fn dataset(&mut self, src: &hdf5::Group, name: &str, dst: &hdf5::Group) -> Result<()>;
fn enter(&mut self, _src: &hdf5::Group) -> Result<()> {
Ok(())
}
fn leave(&mut self, _src: &hdf5::Group) {}
}
pub struct Raw;
impl CopyHooks for Raw {
fn dataset(&mut self, src: &hdf5::Group, name: &str, dst: &hdf5::Group) -> Result<()> {
copy_stored(src, name, dst)
}
}
pub fn copy_stored(src: &hdf5::Group, name: &str, dst: &hdf5::Group) -> Result<()> {
let cname = CString::new(name).map_err(|_| Error::Value(format!("name {} contains a NUL byte", repr_str(name))))?;
super::locked(|| unsafe {
let ocpypl = h5p::H5Pcreate(*crate::h5sys::h5p::H5P_CLS_OBJECT_COPY);
h5p::H5Pset_copy_object(ocpypl, h5o::H5O_COPY_MERGE_COMMITTED_DTYPE_FLAG);
let status = h5o::H5Ocopy(src.id(), cname.as_ptr(), dst.id(), cname.as_ptr(), ocpypl, h5p::H5P_DEFAULT);
h5p::H5Pclose(ocpypl);
if status < 0 {
return Err(Error::Io(format!("could not copy {} into {}", repr_str(name), dst.name())));
}
Ok(())
})
}
pub struct GraphCopy {
copied: HashMap<ObjectId, String>,
src_root: String,
dst_root: String,
}
impl GraphCopy {
pub fn new(src_root: &hdf5::Group, dst_root: &hdf5::Group) -> GraphCopy {
let mut copy = GraphCopy { copied: HashMap::new(), src_root: src_root.name(), dst_root: dst_root.name() };
copy.note(src_root, &dst_root.name());
copy
}
fn note(&mut self, src: &hdf5::Location, dst_path: &str) {
if let Some(id) = ops::object_id_of(src) {
self.copied.insert(id, dst_path.to_string());
}
}
pub fn members(
&mut self,
src: &hdf5::Group,
dst: &hdf5::Group,
skip: &[&str],
hooks: &mut dyn CopyHooks,
) -> Result<()> {
hooks.enter(src)?;
let result = self.walk(src, dst, skip, 0, hooks);
hooks.leave(src);
result
}
fn walk(
&mut self,
src: &hdf5::Group,
dst: &hdf5::Group,
skip: &[&str],
depth: usize,
hooks: &mut dyn CopyHooks,
) -> Result<()> {
if depth > ops::MAX_DEPTH {
return Err(Error::File(format!(
"{} is nested more than {} groups deep; it is refused rather than followed",
repr_str(&src.name()),
ops::MAX_DEPTH
)));
}
let base = dst.name();
let base = base.trim_end_matches('/');
for name in ops::members(src)? {
if skip.contains(&name.as_str()) {
continue;
}
match ops::link_kind(src, &name) {
Some(LinkKind::Soft) => {
let target = self.rebase(&ops::soft_link_target(src, &name)?, &src.name(), &name)?;
dst.link_soft(&target, &name)?;
continue;
}
Some(LinkKind::Hard) => {
if let Some(first) = ops::object_id_by_name(src, &name).and_then(|id| self.copied.get(&id)) {
dst.link_hard(first, &name)?;
continue;
}
}
other => {
return Err(Error::File(format!(
"{}/{} is {}, which a copy cannot keep",
src.name().trim_end_matches('/'),
name,
match other {
Some(LinkKind::External) => "an external link",
_ => "a link of a kind HDF5 defines for another library",
}
)))
}
}
let path = format!("{base}/{name}");
match ops::node_kind(src, &name) {
Some(NodeKind::Group) => {
let source = src.group(&name)?;
let child = ops::new_group(dst, &name)?;
for key in attrs::names(&source)? {
attrs::copy_raw(&source, &child, &key)?;
}
self.note(&source, &path);
hooks.enter(&source)?;
let walked = self.walk(&source, &child, &[], depth + 1, hooks);
hooks.leave(&source);
walked?;
}
Some(NodeKind::Dataset) => {
let id = ops::object_id_by_name(src, &name);
hooks.dataset(src, &name, dst)?;
if let Some(id) = id {
self.copied.insert(id, path);
}
}
_ => {
let id = ops::object_id_by_name(src, &name);
copy_stored(src, &name, dst)?;
if let Some(id) = id {
self.copied.insert(id, path);
}
}
}
}
Ok(())
}
fn rebase(&self, target: &str, group: &str, name: &str) -> Result<String> {
if !target.starts_with('/') {
return Ok(target.to_string());
}
let src = self.src_root.trim_end_matches('/');
let dst = self.dst_root.trim_end_matches('/');
let rest = if src.is_empty() {
Some(target)
} else if target == src {
Some("/")
} else {
target.strip_prefix(src).filter(|rest| rest.starts_with('/'))
};
let Some(rest) = rest else {
return Err(Error::File(format!(
"the soft link {}/{name} points at {}, outside its sample root {}; a copy of the sample cannot \
keep it pointing at the same object",
group.trim_end_matches('/'),
repr_str(target),
repr_str(&self.src_root)
)));
};
Ok(if dst.is_empty() {
rest.to_string()
} else if rest == "/" {
dst.to_string()
} else {
format!("{dst}{rest}")
})
}
}
pub fn copy_root_raw(src: &hdf5::Group, dst: &hdf5::Group, skip: &[&str]) -> Result<()> {
GraphCopy::new(src, dst).members(src, dst, skip, &mut Raw)?;
for key in attrs::names(src)? {
attrs::copy_raw(src, dst, &key)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn source(path: &std::path::Path) -> hdf5::File {
let file = crate::h5::file::create_truncate(path).unwrap();
let images = file.create_group("images").unwrap();
images.new_dataset::<u8>().shape([4]).create("CT").unwrap().write(&[1u8, 2, 3, 4]).unwrap();
file.link_hard("/images/CT", "zzz_alias").unwrap();
file.link_hard("/", "zzz_loop").unwrap();
file.link_soft("/images/CT", "zzz_soft").unwrap();
file.link_soft("CT", "images/relative").unwrap();
file
}
#[test]
fn s2_2_a_copy_keeps_aliases_cycles_and_soft_links() {
let dir = tempfile::tempdir().unwrap();
let src = source(&dir.path().join("src.h5"));
let packed = crate::h5::file::create_truncate(&dir.path().join("packed.h5")).unwrap();
let member = packed.create_group("samples").unwrap().create_group("k").unwrap();
copy_root_raw(&src.as_group().unwrap(), &member, &[]).unwrap();
let id = |group: &hdf5::Group, name: &str| ops::object_id_by_name(group, name).unwrap();
assert_eq!(id(&member, "zzz_alias"), id(&member, "images/CT"));
assert_eq!(id(&member, "zzz_loop"), ops::object_id_of(&member).unwrap());
assert_eq!(ops::soft_link_target(&member, "zzz_soft").unwrap(), "/samples/k/images/CT");
assert_eq!(ops::soft_link_target(&member.group("images").unwrap(), "relative").unwrap(), "CT");
assert!(crate::integrity::subtrees_identical(&src.as_group().unwrap(), &member).unwrap().is_empty());
let unpacked = crate::h5::file::create_truncate(&dir.path().join("unpacked.h5")).unwrap();
let root = unpacked.as_group().unwrap();
copy_root_raw(&member, &root, &[]).unwrap();
assert_eq!(ops::soft_link_target(&root, "zzz_soft").unwrap(), "/images/CT");
assert_eq!(id(&root, "zzz_alias"), id(&root, "images/CT"));
assert!(crate::integrity::subtrees_identical(&src.as_group().unwrap(), &root).unwrap().is_empty());
}
#[test]
fn s2_2_a_soft_link_out_of_its_sample_root_is_refused() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::create_truncate(&dir.path().join("c.h5")).unwrap();
let samples = file.create_group("samples").unwrap();
let a = samples.create_group("a").unwrap();
a.new_dataset::<u8>().shape([1]).create("x").unwrap();
let b = samples.create_group("b").unwrap();
b.link_soft("/samples/a/x", "elsewhere").unwrap();
let out = crate::h5::file::create_truncate(&dir.path().join("out.h5")).unwrap();
let err = copy_root_raw(&b, &out.as_group().unwrap(), &[]).unwrap_err();
assert!(err.to_string().contains("outside its sample root"), "{err}");
}
#[test]
fn s2_2_datasets_sharing_a_committed_type_share_its_copy() {
use crate::h5sys::{h5d, h5i, h5s, h5t};
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::create_truncate(&dir.path().join("t.h5")).unwrap();
crate::h5::locked(|| unsafe {
let tid = h5t::H5Tcopy(*h5t::H5T_NATIVE_INT32);
let name = CString::new("types_t").unwrap();
assert!(
h5t::H5Tcommit2(file.id(), name.as_ptr(), tid, h5p::H5P_DEFAULT, h5p::H5P_DEFAULT, h5p::H5P_DEFAULT)
>= 0
);
let dims = [2u64];
for ds in ["a", "b"] {
let space = h5s::H5Screate_simple(1, dims.as_ptr(), std::ptr::null());
let cname = CString::new(ds).unwrap();
let id = h5d::H5Dcreate2(
file.id(),
cname.as_ptr(),
tid,
space,
h5p::H5P_DEFAULT,
h5p::H5P_DEFAULT,
h5p::H5P_DEFAULT,
);
assert!(id >= 0);
h5d::H5Dclose(id);
h5s::H5Sclose(space);
}
h5t::H5Tclose(tid);
});
let out = crate::h5::file::create_truncate(&dir.path().join("o.h5")).unwrap();
copy_root_raw(&file.as_group().unwrap(), &out.as_group().unwrap(), &[]).unwrap();
let type_of = |name: &str| {
let ds = out.dataset(name).unwrap();
crate::h5::locked(|| unsafe {
let tid = h5d::H5Dget_type(ds.id());
assert!(h5t::H5Tcommitted(tid) > 0, "{name} lost its committed type");
let mut info: h5o::H5O_info2_t = std::mem::zeroed();
h5o::H5Oget_info3(tid, &mut info, h5o::H5O_INFO_BASIC);
h5i::H5Idec_ref(tid);
std::mem::transmute::<h5o::H5O_token_t, [u8; 16]>(info.token)
})
};
assert_eq!(type_of("a"), type_of("b"));
}
}