use std::fs;
use std::path::{Path, PathBuf};
use hdf5::file::LibraryVersion;
use super::ops::check_self_contained;
use crate::json::repr_str;
use crate::{Error, Result};
pub fn open_read(path: &Path) -> Result<hdf5::File> {
super::init();
let failed = |why: &dyn std::fmt::Display| {
Error::File(format!("failed to open {}: {why}", repr_str(&path.to_string_lossy())))
};
match fs::metadata(path) {
Ok(meta) if meta.is_dir() => return Err(failed(&"it is a directory, not a file")),
Ok(meta) if !meta.is_file() => return Err(failed(&"it is not a regular file")),
Ok(_) => {}
Err(err) => return Err(failed(&os_reason(&err))),
}
let handle = marked(|| hdf5::File::open(path)).map_err(|e| failed(&e))?;
check_self_contained(&handle, Some(path))?;
Ok(handle)
}
pub(crate) fn marked(open: impl FnOnce() -> hdf5::Result<hdf5::File>) -> hdf5::Result<hdf5::File> {
super::locked(|| {
let file = open()?;
close_on_exec(&file);
Ok(file)
})
}
pub(crate) fn open_rw(path: &Path) -> Result<hdf5::File> {
super::init();
Ok(marked(|| hdf5::File::open_rw(path))?)
}
pub(crate) fn descriptor(file: &hdf5::File) -> Option<std::os::raw::c_int> {
use hdf5::file::FileDriver;
use crate::h5sys::{h5f, h5p};
if !matches!(file.access_plist().and_then(|fapl| fapl.get_driver()), Ok(FileDriver::Sec2)) {
return None;
}
super::locked(|| {
let mut handle: *mut std::ffi::c_void = std::ptr::null_mut();
unsafe {
if h5f::H5Fget_vfd_handle(file.id(), h5p::H5P_DEFAULT, &mut handle) < 0 || handle.is_null() {
return None;
}
Some(*(handle as *const std::os::raw::c_int))
}
})
}
#[cfg(windows)]
pub(crate) fn os_handle(fd: std::os::raw::c_int) -> Option<windows_sys::Win32::Foundation::HANDLE> {
extern "C" {
fn _get_osfhandle(fd: std::os::raw::c_int) -> isize;
}
let os = unsafe { _get_osfhandle(fd) };
(os != -1 && os != -2).then_some(os as windows_sys::Win32::Foundation::HANDLE)
}
pub(crate) fn close_on_exec(file: &hdf5::File) {
let Some(fd) = descriptor(file) else { return };
#[cfg(unix)]
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags >= 0 {
libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
#[cfg(windows)]
if let Some(handle) = os_handle(fd) {
use windows_sys::Win32::Foundation::{SetHandleInformation, HANDLE_FLAG_INHERIT};
unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) };
}
#[cfg(not(any(unix, windows)))]
let _ = fd;
}
fn os_reason(err: &std::io::Error) -> String {
if err.kind() == std::io::ErrorKind::NotFound {
return "no such file or directory".into();
}
let full = err.to_string();
match err.raw_os_error() {
Some(code) => full.strip_suffix(&format!(" (os error {code})")).unwrap_or(&full).to_string(),
None => full,
}
}
pub fn create_truncate(path: &Path) -> Result<hdf5::File> {
super::init();
marked(|| {
hdf5::File::with_options()
.with_fapl(|p| p.libver_bounds(LibraryVersion::V110, LibraryVersion::latest()))
.create(path)
})
.map_err(|e| Error::File(format!("failed to create {}: {}", repr_str(&path.to_string_lossy()), e)))
}
const MAX_LINKS: usize = 40;
fn resolve_links(target: &Path) -> Result<PathBuf> {
let mut path = target.to_path_buf();
for _ in 0..MAX_LINKS {
match fs::symlink_metadata(&path) {
Ok(meta) if meta.file_type().is_symlink() => {
if !may_follow(&path, &meta) {
return Err(Error::File(format!(
"{} is a symbolic link another user owns, in a directory anyone can write to; it is not \
followed, as the system would not follow it to open the file",
repr_str(&path.to_string_lossy())
)));
}
let leads_to = fs::read_link(&path).map_err(|e| crate::error::os_error(&e, &path))?;
path = match path.parent() {
Some(dir) => dir.join(leads_to),
None => leads_to,
};
}
_ => return Ok(path),
}
}
Err(Error::File(format!(
"{} leads through more than {MAX_LINKS} symbolic links",
repr_str(&target.to_string_lossy())
)))
}
#[cfg(unix)]
fn may_follow(link: &Path, meta: &fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
let dir = link.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(Path::new("."));
let Ok(dir_meta) = fs::metadata(dir) else { return true };
let user = unsafe { libc::geteuid() };
dir_meta.mode() & 0o1002 != 0o1002 || meta.uid() == user || meta.uid() == dir_meta.uid()
}
#[cfg(not(unix))]
fn may_follow(_link: &Path, _meta: &fs::Metadata) -> bool {
true
}
fn temporary_name(target: &Path) -> PathBuf {
let name = target.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let unique: u32 = {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let mut h = RandomState::new().build_hasher();
h.write_u128(
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0),
);
h.finish() as u32
};
target.with_file_name(format!(".{name}.tmp-{}-{unique:08x}", std::process::id()))
}
fn existing_mode(target: &Path) -> Option<u32> {
let meta = fs::metadata(target).ok()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(meta.permissions().mode() & 0o7777)
}
#[cfg(not(unix))]
{
Some(if meta.permissions().readonly() { 0o444 } else { 0o666 })
}
}
fn precreate(tmp: &Path, mode: Option<u32>) -> std::io::Result<()> {
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(if mode.is_none() { 0o666 } else { 0o600 });
}
let _ = mode;
options.open(tmp)?;
Ok(())
}
fn set_mode(path: &Path, mode: u32) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(mode));
}
#[cfg(not(unix))]
{
if let Ok(meta) = fs::metadata(path) {
let mut perms = meta.permissions();
perms.set_readonly(mode & 0o222 == 0);
let _ = fs::set_permissions(path, perms);
}
}
}
fn fsync_path(path: &Path) -> std::io::Result<()> {
let file = fs::OpenOptions::new().read(true).write(cfg!(windows)).open(path)?;
file.sync_all()?;
Ok(())
}
fn replace(from: &Path, to: &Path) -> std::io::Result<()> {
#[cfg(windows)]
{
if let Ok(meta) = fs::metadata(to) {
let kept = meta.permissions();
if kept.readonly() {
let mut writable = kept.clone();
#[allow(clippy::permissions_set_readonly_false)]
writable.set_readonly(false);
fs::set_permissions(to, writable)?;
let renamed = fs::rename(from, to);
if renamed.is_err() {
let _ = fs::set_permissions(to, kept);
}
return renamed;
}
}
}
fs::rename(from, to)
}
fn remove_temporary(path: &Path) {
#[cfg(windows)]
{
if let Ok(meta) = fs::metadata(path) {
let mut writable = meta.permissions();
if writable.readonly() {
#[allow(clippy::permissions_set_readonly_false)]
writable.set_readonly(false);
let _ = fs::set_permissions(path, writable);
}
}
}
let _ = fs::remove_file(path);
}
fn fsync_dir(dir: &Path) {
#[cfg(unix)]
{
if let Ok(handle) = fs::File::open(dir) {
let _ = handle.sync_all();
}
}
let _ = dir;
}
#[doc(hidden)]
pub fn same_file(a: &Path, b: &Path) -> bool {
matches!((identity(a), identity(b)), (Some(x), Some(y)) if x == y)
}
#[cfg(unix)]
fn identity(path: &Path) -> Option<(u64, u64)> {
use std::os::unix::fs::MetadataExt;
let meta = fs::metadata(path).ok()?;
Some((meta.dev(), meta.ino()))
}
#[cfg(windows)]
fn identity(path: &Path) -> Option<(u32, u64)> {
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
};
let file = fs::OpenOptions::new().access_mode(0).custom_flags(FILE_FLAG_BACKUP_SEMANTICS).open(path).ok()?;
let mut info = BY_HANDLE_FILE_INFORMATION::default();
if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
return None;
}
Some((info.dwVolumeSerialNumber, u64::from(info.nFileIndexHigh) << 32 | u64::from(info.nFileIndexLow)))
}
#[cfg(not(any(unix, windows)))]
fn identity(path: &Path) -> Option<PathBuf> {
fs::canonicalize(path).ok()
}
pub fn close_everything(file: hdf5::File, flush: bool) -> Result<()> {
use crate::h5sys::h5f::{self, H5F_scope_t};
let fid = file.id();
std::mem::forget(file);
super::locked(|| unsafe {
let flushed = !flush || h5f::H5Fflush(fid, H5F_scope_t::H5F_SCOPE_LOCAL) >= 0;
if !flushed {
let why = super::ops::hdf5_error_text();
release_all(fid);
return Err(Error::Io(format!("could not flush the file: {why}")));
}
match release_all(fid) {
None => Ok(()),
Some(why) => Err(Error::Io(format!("could not close the file: {why}"))),
}
})
}
unsafe fn release_all(fid: crate::h5sys::h5i::hid_t) -> Option<String> {
use crate::h5sys::h5f;
use crate::h5sys::h5i;
let mut failed = None;
let mut release = |id: h5i::hid_t| unsafe {
while h5i::H5Iis_valid(id) > 0 {
let left = h5i::H5Idec_ref(id);
if left < 0 && failed.is_none() {
failed = Some(super::ops::hdf5_error_text());
}
if left <= 0 {
break;
}
}
};
let opened = |types: std::os::raw::c_uint| unsafe {
let count = h5f::H5Fget_obj_count(fid, types);
if count <= 0 {
return Vec::new();
}
let mut ids = vec![0 as h5i::hid_t; count as usize];
let found = h5f::H5Fget_obj_ids(fid, types, ids.len(), ids.as_mut_ptr());
ids.truncate(found.max(0) as usize);
ids
};
for id in opened(
h5f::H5F_OBJ_LOCAL | h5f::H5F_OBJ_DATASET | h5f::H5F_OBJ_GROUP | h5f::H5F_OBJ_DATATYPE | h5f::H5F_OBJ_ATTR,
) {
release(id);
}
for id in opened(h5f::H5F_OBJ_LOCAL | h5f::H5F_OBJ_FILE) {
release(id);
}
release(fid);
#[cfg(test)]
if CLOSE_FAILS.with(|fails| fails.replace(false)) {
failed.get_or_insert_with(|| "H5Fclose(): the close failed (a test made it)".into());
}
failed
}
#[cfg(test)]
type RenameHook = Box<dyn FnMut(&Path)>;
#[cfg(test)]
thread_local! {
static BEFORE_RENAME: std::cell::RefCell<Option<RenameHook>> = const { std::cell::RefCell::new(None) };
static CLOSE_FAILS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub struct AtomicFile {
target: PathBuf,
tmp: PathBuf,
mode: Option<u32>,
handle: Option<hdf5::File>,
}
impl AtomicFile {
pub fn create(target: &Path) -> Result<AtomicFile> {
if let Some(parent) = target.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
let target = resolve_links(target)?;
let tmp = temporary_name(&target);
let mode = existing_mode(&target);
precreate(&tmp, mode)?;
match create_truncate(&tmp) {
Ok(handle) => Ok(AtomicFile { target, tmp, mode, handle: Some(handle) }),
Err(err) => {
let _ = fs::remove_file(&tmp);
Err(err)
}
}
}
pub fn handle(&self) -> &hdf5::File {
self.handle.as_ref().expect("an AtomicFile is open until committed")
}
pub fn target(&self) -> &Path {
&self.target
}
pub fn temporary(&self) -> &Path {
&self.tmp
}
pub fn commit(mut self) -> Result<()> {
if let Some(handle) = self.handle.take() {
close_everything(handle, true)?;
}
let result = (|| -> Result<()> {
fsync_path(&self.tmp)?;
if let Some(mode) = self.mode {
set_mode(&self.tmp, mode);
}
#[cfg(test)]
BEFORE_RENAME.with(|hook| {
if let Some(hook) = hook.borrow_mut().as_mut() {
hook(&self.target)
}
});
replace(&self.tmp, &self.target)?;
if let Some(parent) = self.target.parent() {
fsync_dir(if parent.as_os_str().is_empty() { Path::new(".") } else { parent });
}
Ok(())
})();
if result.is_err() {
remove_temporary(&self.tmp);
}
self.tmp = PathBuf::new();
result
}
pub fn rebuild(mut self, copy: impl FnOnce(&hdf5::Group, &hdf5::Group) -> Result<()>) -> Result<AtomicFile> {
if let Some(handle) = self.handle.take() {
close_everything(handle, true)?;
}
let fresh = temporary_name(&self.target);
precreate(&fresh, self.mode)?;
let built = (|| -> Result<hdf5::File> {
let src = open_read(&self.tmp)?;
let dst = create_truncate(&fresh)?;
copy(&src.as_group()?, &dst.as_group()?)?;
close_everything(src, false)?;
Ok(dst)
})();
match built {
Ok(handle) => {
let _ = fs::remove_file(&self.tmp);
self.tmp = fresh;
self.handle = Some(handle);
Ok(self)
}
Err(err) => {
let _ = fs::remove_file(&fresh);
Err(err)
}
}
}
pub fn abort(mut self) {
self.discard();
}
fn discard(&mut self) {
if let Some(handle) = self.handle.take() {
let _ = close_everything(handle, false);
}
if !self.tmp.as_os_str().is_empty() && self.tmp.exists() {
let _ = fs::remove_file(&self.tmp);
}
self.tmp = PathBuf::new();
}
}
impl Drop for AtomicFile {
fn drop(&mut self) {
self.discard();
}
}
pub fn atomic_rewrite<T>(
source: &Path,
target: Option<&Path>,
build: impl FnOnce(&hdf5::File, &hdf5::File) -> Result<T>,
) -> Result<T> {
let dst_path = target.unwrap_or(source);
let src = open_read(source)?;
let out = AtomicFile::create(dst_path)?;
let value = build(&src, out.handle())?;
close_everything(src, false)?;
out.commit()?;
Ok(value)
}
pub(crate) fn write_atomic(target: &Path, bytes: &[u8]) -> Result<()> {
let given = target;
let target = resolve_links(target)?;
let tmp = temporary_name(&target);
let mode = existing_mode(&target);
let named = |e: std::io::Error| crate::error::os_error(&e, given);
precreate(&tmp, mode).map_err(named)?;
let result = (|| -> std::io::Result<()> {
fs::write(&tmp, bytes)?;
fsync_path(&tmp)?;
if let Some(mode) = mode {
set_mode(&tmp, mode);
}
replace(&tmp, &target)?;
if let Some(parent) = target.parent() {
fsync_dir(if parent.as_os_str().is_empty() { Path::new(".") } else { parent });
}
Ok(())
})();
if result.is_err() {
remove_temporary(&tmp);
}
result.map_err(named)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn s14_4_close_everything_releases_what_was_opened_through_the_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.h5");
{
let file = create_truncate(&path).unwrap();
file.new_dataset::<i32>().shape([3]).create("d").unwrap().write(&[1, 2, 3]).unwrap();
}
let file = open_read(&path).unwrap();
let kept = file.dataset("d").unwrap();
let group = file.as_group().unwrap();
close_everything(file, false).unwrap();
assert!(!kept.is_valid() && !group.is_valid());
assert!(super::super::alive(&kept).is_err());
let again = crate::h5::file::marked(|| hdf5::File::open_rw(&path)).unwrap();
assert_eq!(again.dataset("d").unwrap().read_raw::<i32>().unwrap(), vec![1, 2, 3]);
drop((kept, group));
}
fn write_value(target: &Path, value: i32) -> Result<()> {
let out = AtomicFile::create(target)?;
out.handle().new_dataset::<i32>().shape([1]).create("v")?.write(&[value])?;
out.commit()
}
fn read_value(path: &Path) -> i32 {
open_read(path).unwrap().dataset("v").unwrap().read_raw::<i32>().unwrap()[0]
}
fn no_temporaries(dir: &Path) -> bool {
fs::read_dir(dir).unwrap().all(|e| !e.unwrap().file_name().to_string_lossy().contains(".tmp-"))
}
#[test]
fn s14_4_a_missing_file_is_reported_the_same_on_every_run() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("missing.medh5");
let first = open_read(&missing).unwrap_err().to_string();
let second = open_read(&missing).unwrap_err().to_string();
assert_eq!(first, second);
assert_eq!(
first,
format!("failed to open {}: no such file or directory", repr_str(&missing.to_string_lossy()))
);
let err = open_read(dir.path()).unwrap_err();
assert!(matches!(err, Error::File(_)));
assert!(err.to_string().ends_with(": it is a directory, not a file"), "{err}");
}
#[cfg(unix)]
#[test]
fn s2_a_fifo_is_refused_before_hdf5_opens_it() {
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir().unwrap();
let fifo = dir.path().join("pipe.medh5");
let name = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(name.as_ptr(), 0o600) }, 0);
let (tx, rx) = std::sync::mpsc::channel();
let opening = fifo.clone();
std::thread::spawn(move || tx.send(open_read(&opening).map(drop).map_err(|e| e.to_string())).unwrap());
let err = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("the open blocked").unwrap_err();
assert!(err.ends_with(": it is not a regular file"), "{err}");
}
#[cfg(unix)]
#[test]
fn s14_4_a_replace_through_a_symlink_keeps_the_link() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("data");
let farm = dir.path().join("farm");
fs::create_dir_all(&data).unwrap();
fs::create_dir_all(&farm).unwrap();
let real = data.join("real.medh5");
write_value(&real, 1).unwrap();
let absolute = farm.join("absolute.medh5");
symlink(&real, &absolute).unwrap();
let relative = farm.join("relative.medh5");
symlink("../data/real.medh5", &relative).unwrap();
let chained = farm.join("chained.medh5");
symlink("relative.medh5", &chained).unwrap();
for (value, link) in [(2, &absolute), (3, &relative), (4, &chained)] {
let inode = fs::metadata(&real).unwrap();
write_value(link, value).unwrap();
assert!(fs::symlink_metadata(link).unwrap().file_type().is_symlink(), "{link:?} is no longer a link");
assert_eq!(read_value(&real), value, "{link:?}");
assert_eq!(read_value(link), value, "{link:?}");
use std::os::unix::fs::MetadataExt;
assert_ne!(fs::metadata(&real).unwrap().ino(), inode.ino(), "replaced, not written in place");
assert!(no_temporaries(&data) && no_temporaries(&farm));
}
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&real, fs::Permissions::from_mode(0o640)).unwrap();
write_value(&chained, 5).unwrap();
assert_eq!(fs::metadata(&real).unwrap().permissions().mode() & 0o777, 0o640);
}
#[cfg(unix)]
#[test]
fn s14_4_a_dangling_link_is_created_through_and_a_loop_refused() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let link = dir.path().join("link.medh5");
symlink("made.medh5", &link).unwrap();
write_value(&link, 7).unwrap();
assert!(fs::symlink_metadata(&link).unwrap().file_type().is_symlink());
assert_eq!(read_value(&dir.path().join("made.medh5")), 7);
let (a, b) = (dir.path().join("a.medh5"), dir.path().join("b.medh5"));
symlink("b.medh5", &a).unwrap();
symlink("a.medh5", &b).unwrap();
let err = write_value(&a, 8).unwrap_err();
assert!(err.to_string().contains("more than 40 symbolic links"), "{err}");
assert!(no_temporaries(dir.path()));
}
#[cfg(unix)]
#[test]
fn s14_4_a_link_another_user_planted_in_a_shared_directory_is_not_followed() {
use std::os::unix::fs::{symlink, PermissionsExt};
let dir = tempfile::tempdir().unwrap();
let shared = dir.path().join("shared");
fs::create_dir(&shared).unwrap();
fs::set_permissions(&shared, fs::Permissions::from_mode(0o1777)).unwrap();
let mine = dir.path().join("mine.medh5");
write_value(&mine, 1).unwrap();
let link = shared.join("out.medh5");
symlink(&mine, &link).unwrap();
write_value(&link, 2).unwrap();
assert_eq!(read_value(&mine), 2, "the user's own link is followed");
if unsafe { libc::geteuid() } == 0 {
let path = std::ffi::CString::new(link.as_os_str().as_encoded_bytes()).unwrap();
assert_eq!(unsafe { libc::lchown(path.as_ptr(), 4242, 4242) }, 0);
let err = write_value(&link, 3).unwrap_err();
assert!(err.to_string().contains("another user owns"), "{err}");
assert_eq!(read_value(&mine), 2);
assert!(no_temporaries(&shared) && no_temporaries(dir.path()));
}
}
#[test]
fn s14_4_a_read_only_file_is_replaced_and_stays_read_only() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ro.medh5");
write_value(&path, 1).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_readonly(true);
fs::set_permissions(&path, perms).unwrap();
write_value(&path, 2).unwrap();
assert_eq!(read_value(&path), 2);
assert!(fs::metadata(&path).unwrap().permissions().readonly());
assert!(no_temporaries(dir.path()));
set_mode(&path, 0o644);
}
#[test]
fn s14_4_a_failed_close_is_not_renamed_into_place() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.medh5");
write_value(&path, 1).unwrap();
CLOSE_FAILS.with(|fails| fails.set(true));
let err = write_value(&path, 2).unwrap_err();
assert!(err.to_string().starts_with("could not close the file: "), "{err}");
assert_eq!(read_value(&path), 1);
assert!(no_temporaries(dir.path()));
CLOSE_FAILS.with(|fails| fails.set(true));
let file = create_truncate(&dir.path().join("d.h5")).unwrap();
assert!(close_everything(file, false).is_err());
}
#[test]
fn s14_4_temporary_names_are_unique_within_a_process() {
let names: std::collections::HashSet<PathBuf> = (0..50).map(|_| temporary_name(Path::new("x.medh5"))).collect();
assert_eq!(names.len(), 50);
assert!(names.iter().all(|n| n.to_string_lossy().starts_with(".x.medh5.tmp-")));
}
#[test]
fn fsync_leaves_a_trailing_control_z_alone() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ctrlz.bin");
let payload: Vec<u8> = (0..=255u8).chain([0x1a]).collect();
fs::write(&path, &payload).unwrap();
fsync_path(&path).unwrap();
assert_eq!(fs::read(&path).unwrap(), payload);
}
fn long_lived_child() -> std::process::Child {
let mut command = if cfg!(windows) {
let mut c = std::process::Command::new("ping");
c.args(["-n", "30", "127.0.0.1"]);
c
} else {
let mut c = std::process::Command::new("sleep");
c.arg("30");
c
};
super::super::spawn(command.stdout(std::process::Stdio::null())).unwrap()
}
#[test]
fn s14_4_a_child_process_inherits_no_open_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.h5");
let spare = dir.path().join("spare.h5");
create_truncate(&path).unwrap();
for door in [create_truncate, open_read, open_rw] {
let file = door(&path).unwrap();
let mut child = long_lived_child();
drop(file);
let again = create_truncate(&path).map(drop);
fs::write(&spare, b"x").unwrap();
let renamed = fs::rename(&spare, &path);
child.kill().unwrap();
child.wait().unwrap();
again.unwrap();
renamed.unwrap();
create_truncate(&path).unwrap();
}
}
#[test]
fn s14_4_the_descriptor_is_the_files_own() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.h5");
let file = create_truncate(&path).unwrap();
let fd = descriptor(&file).expect("sec2 hands back a descriptor");
#[cfg(unix)]
{
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
assert_eq!(unsafe { libc::fstat(fd, &mut stat) }, 0);
assert_eq!(Some((stat.st_dev as u64, stat.st_ino as u64)), identity(&path));
}
#[cfg(windows)]
{
use windows_sys::Win32::Storage::FileSystem::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION};
let handle = os_handle(fd).expect("the descriptor names a handle");
let mut info = BY_HANDLE_FILE_INFORMATION::default();
assert_ne!(unsafe { GetFileInformationByHandle(handle, &mut info) }, 0);
let own = (info.dwVolumeSerialNumber, u64::from(info.nFileIndexHigh) << 32 | u64::from(info.nFileIndexLow));
assert_eq!(Some(own), identity(&path));
}
#[cfg(not(any(unix, windows)))]
let _ = fd;
}
#[test]
fn s14_4_a_child_starts_only_between_opens() {
let (held_tx, held_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
let holder = std::thread::spawn(move || {
super::super::locked(|| {
held_tx.send(()).unwrap();
release_rx.recv().unwrap();
})
});
held_rx.recv().unwrap();
let (started_tx, started_rx) = std::sync::mpsc::channel();
let starter = std::thread::spawn(move || {
let mut command = if cfg!(windows) {
let mut c = std::process::Command::new("cmd");
c.args(["/C", "exit 0"]);
c
} else {
std::process::Command::new("true")
};
let child = super::super::spawn(&mut command);
started_tx.send(()).unwrap();
child.unwrap().wait().unwrap();
});
let waited = started_rx.recv_timeout(std::time::Duration::from_millis(200));
assert!(waited.is_err(), "a child started while HDF5's lock was held");
release_tx.send(()).unwrap();
holder.join().unwrap();
started_rx.recv_timeout(std::time::Duration::from_secs(10)).expect("the child starts once the lock is free");
starter.join().unwrap();
}
#[test]
fn s14_4_same_file_knows_every_name_of_one_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("a.medh5");
let twin = dir.path().join("b.medh5");
fs::write(&file, b"same bytes").unwrap();
fs::write(&twin, b"same bytes").unwrap();
let hard = dir.path().join("hard.medh5");
fs::hard_link(&file, &hard).unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
let dotted = dir.path().join("sub").join("..").join("a.medh5");
let missing = dir.path().join("missing.medh5");
assert!(same_file(&file, &file));
assert!(same_file(&file, &hard));
assert!(same_file(&dotted, &file));
assert!(!same_file(&file, &twin), "equal bytes are not one file");
assert!(!same_file(&file, &missing));
assert!(!same_file(&missing, &missing), "a path naming nothing names no file");
#[cfg(unix)]
{
let link = dir.path().join("link.medh5");
std::os::unix::fs::symlink(&file, &link).unwrap();
assert!(same_file(&link, &hard));
}
}
fn open_on(path: &Path) -> usize {
use crate::h5sys::h5f;
use crate::h5sys::h5i::hid_t;
let want = path.to_string_lossy().into_owned();
super::super::locked(|| unsafe {
let all = h5f::H5F_OBJ_ALL as hid_t;
let count = h5f::H5Fget_obj_count(all, h5f::H5F_OBJ_FILE);
let mut ids = vec![0 as hid_t; count.max(0) as usize];
let found = h5f::H5Fget_obj_ids(all, h5f::H5F_OBJ_FILE, ids.len(), ids.as_mut_ptr());
ids.iter()
.take(found.max(0) as usize)
.filter(|&&id| {
let mut buf = vec![0u8; 4096];
let n = h5f::H5Fget_name(id, buf.as_mut_ptr().cast(), buf.len());
n > 0 && buf[..n as usize] == *want.as_bytes()
})
.count()
})
}
#[test]
fn s14_4_the_source_is_closed_before_the_replace() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rw.h5");
{
let file = create_truncate(&path).unwrap();
file.new_dataset::<i32>().shape([3]).create("d").unwrap().write(&[1, 2, 3]).unwrap();
}
let seen = std::rc::Rc::new(std::cell::Cell::new(None));
let record = seen.clone();
BEFORE_RENAME.with(|hook| *hook.borrow_mut() = Some(Box::new(move |target| record.set(Some(open_on(target))))));
let kept = std::cell::RefCell::new(None);
atomic_rewrite(&path, None, |src, dst| {
assert_eq!(open_on(&path), 1, "the source is open while the build runs");
let data = src.dataset("d")?;
dst.new_dataset::<i32>().shape([3]).create("d")?.write(&data.read_raw::<i32>()?)?;
*kept.borrow_mut() = Some(data);
Ok(())
})
.unwrap();
BEFORE_RENAME.with(|hook| *hook.borrow_mut() = None);
assert_eq!(seen.get(), Some(0), "the source was still open at the rename");
assert!(!kept.into_inner().unwrap().is_valid());
let again = open_read(&path).unwrap();
assert_eq!(again.dataset("d").unwrap().read_raw::<i32>().unwrap(), vec![1, 2, 3]);
}
}