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()))
.with_fcpl(|p| p.obj_track_times(false))
.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 unique() -> u32 {
#[cfg(test)]
if let Some(next) = NEXT_UNIQUE.with(|queue| queue.borrow_mut().pop()) {
return next;
}
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
}
#[derive(Debug, Clone, Copy)]
enum Naming {
Hidden,
Suffixed,
}
fn temporary_name_as(target: &Path, naming: Naming) -> PathBuf {
let name = target.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let (pid, unique) = (std::process::id(), unique());
target.with_file_name(match naming {
Naming::Hidden => format!(".{name}.tmp-{pid}-{unique:08x}"),
Naming::Suffixed => format!(".{pid}-{unique:08x}-{name}"),
})
}
const NAME_TRIES: usize = 64;
#[derive(Debug)]
struct Staged {
given: PathBuf,
target: PathBuf,
tmp: PathBuf,
mode: Option<u32>,
owner: Option<(u32, u32)>,
}
impl Staged {
fn create(given: &Path, naming: Naming) -> Result<Staged> {
let target = resolve_links(given)?;
let mode = existing_mode(&target);
let owner = existing_owner(&target);
let named = |e: std::io::Error| crate::error::os_error(&e, given);
for _ in 0..NAME_TRIES {
let tmp = temporary_name_as(&target, naming);
match precreate(&tmp, mode) {
Ok(()) => return Ok(Staged { given: given.to_path_buf(), target, tmp, mode, owner }),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(named(e)),
}
}
Err(Error::File(format!(
"could not create a temporary file beside {}: {NAME_TRIES} names were taken",
repr_str(&given.to_string_lossy())
)))
}
fn commit(&mut self, before_replace: impl FnOnce() -> Result<()>) -> Result<()> {
let named = |e: std::io::Error| crate::error::os_error(&e, &self.given);
let result = (|| -> Result<()> {
fsync_path(&self.tmp).map_err(named)?;
if let Some(mode) = self.mode {
let mode = match self.owner {
Some(owner) => carry_owner(&self.tmp, owner, mode),
None => mode,
};
set_mode(&self.tmp, mode);
}
#[cfg(test)]
BEFORE_RENAME.with(|hook| {
if let Some(hook) = hook.borrow_mut().as_mut() {
hook(&self.target)
}
});
before_replace()?;
replace(&self.tmp, &self.target).map_err(named)?;
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
}
fn discard(&mut self) {
if !self.tmp.as_os_str().is_empty() {
remove_temporary(&self.tmp);
}
self.tmp = PathBuf::new();
}
}
impl Drop for Staged {
fn drop(&mut self) {
self.discard();
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct StagedFile {
staged: Staged,
}
impl StagedFile {
pub fn create(target: &Path) -> Result<StagedFile> {
Ok(StagedFile { staged: Staged::create(target, Naming::Suffixed)? })
}
pub fn path(&self) -> &Path {
&self.staged.tmp
}
pub fn target(&self) -> &Path {
&self.staged.target
}
pub fn commit(mut self) -> Result<()> {
self.staged.commit(|| Ok(()))
}
pub fn abort(mut self) {
self.staged.discard();
}
}
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 existing_owner(target: &Path) -> Option<(u32, u32)> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let meta = fs::metadata(target).ok()?;
Some((meta.uid(), meta.gid()))
}
#[cfg(not(unix))]
{
let _ = target;
None
}
}
#[cfg(unix)]
fn carry_owner(tmp: &Path, (uid, gid): (u32, u32), mode: u32) -> u32 {
use std::os::unix::fs::MetadataExt;
let Ok(meta) = fs::symlink_metadata(tmp) else { return narrowed(mode) };
let root = unsafe { libc::geteuid() } == 0;
let uid = (root && meta.uid() != uid).then_some(uid);
if uid.is_none() && meta.gid() == gid {
return mode;
}
match std::os::unix::fs::lchown(tmp, uid, Some(gid)) {
Ok(()) => mode,
Err(_) => narrowed(mode),
}
}
#[cfg(not(unix))]
fn carry_owner(_tmp: &Path, _owner: (u32, u32), mode: u32) -> u32 {
mode
}
#[cfg(unix)]
fn narrowed(mode: u32) -> u32 {
mode & !0o070 | (mode & 0o007) << 3
}
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)
}
#[doc(hidden)]
pub fn file_key(path: &Path) -> Option<String> {
identity(path).map(|id| format!("{id:?}"))
}
#[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)]
pub(crate) type RenameHook = Box<dyn FnMut(&Path)>;
#[cfg(test)]
pub(crate) fn before_rename(hook: Option<RenameHook>) {
BEFORE_RENAME.with(|slot| *slot.borrow_mut() = hook);
}
#[cfg(test)]
thread_local! {
static BEFORE_RENAME: std::cell::RefCell<Option<RenameHook>> = const { std::cell::RefCell::new(None) };
static NEXT_UNIQUE: std::cell::RefCell<Vec<u32>> = const { std::cell::RefCell::new(Vec::new()) };
static CLOSE_FAILS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Snapshot {
identity: Option<Identity>,
content_id: Option<String>,
reader: &'static str,
}
#[cfg(unix)]
type Identity = super::ops::FileIdentity;
#[cfg(not(unix))]
type Identity = (u64, u64);
#[cfg(unix)]
fn identity_of(meta: &fs::Metadata) -> Option<Identity> {
super::ops::identity(meta)
}
#[cfg(windows)]
fn identity_of(meta: &fs::Metadata) -> Option<Identity> {
use std::os::windows::fs::MetadataExt;
Some((meta.file_size(), meta.last_write_time()))
}
#[cfg(not(any(unix, windows)))]
fn identity_of(meta: &fs::Metadata) -> Option<Identity> {
let modified = meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?;
Some((meta.len(), modified.as_nanos() as u64))
}
fn declared_content_id(file: &hdf5::File) -> Result<Option<String>> {
Ok(match super::attrs::read(file, "content_id")? {
None => None,
Some(super::attrs::AttrValue::Str(s)) => Some(s),
Some(other) => Some(super::attrs::stringify_value(&other)),
})
}
impl Snapshot {
pub(crate) fn of(file: &hdf5::File, path: &Path, reader: &'static str) -> Result<Snapshot> {
#[cfg(unix)]
let identity = super::ops::opened_identity(file, Some(path)).or_else(|| identity_of(&fs::metadata(path).ok()?));
#[cfg(not(unix))]
let identity = fs::metadata(path).ok().and_then(|meta| identity_of(&meta));
Ok(Snapshot { identity, content_id: declared_content_id(file)?, reader })
}
pub(crate) fn check(&self, path: &Path) -> Result<()> {
let changed = || {
Error::File(format!(
"{} changed after this {} read it; nothing was written: {} it again",
repr_str(&path.to_string_lossy()),
self.reader,
if self.reader == "amendment" { "amend" } else { "run" },
))
};
let Some(identity) = fs::metadata(path).ok().and_then(|meta| identity_of(&meta)) else {
return Err(changed());
};
if self.identity.is_some_and(|seen| seen != identity) {
return Err(changed());
}
let file = open_read(path).map_err(|_| changed())?;
let declared = declared_content_id(&file);
close_everything(file, false)?;
if declared? != self.content_id {
return Err(changed());
}
Ok(())
}
}
pub struct AtomicFile {
staged: Staged,
handle: Option<hdf5::File>,
unchanged: Option<(PathBuf, Snapshot)>,
}
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 staged = Staged::create(target, Naming::Hidden)?;
let handle = create_truncate(&staged.tmp)?;
Ok(AtomicFile { staged, handle: Some(handle), unchanged: None })
}
pub fn handle(&self) -> &hdf5::File {
self.handle.as_ref().expect("an AtomicFile is open until committed")
}
pub fn target(&self) -> &Path {
&self.staged.target
}
pub fn temporary(&self) -> &Path {
&self.staged.tmp
}
pub(crate) fn expect_unchanged(&mut self, source: PathBuf, seen: Snapshot) {
self.unchanged = Some((source, seen));
}
pub fn commit(mut self) -> Result<()> {
if let Some(handle) = self.handle.take() {
close_everything(handle, true)?;
}
let unchanged = self.unchanged.take();
self.staged.commit(|| match &unchanged {
Some((source, seen)) => seen.check(source),
None => Ok(()),
})
}
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 mut fresh = Staged::create(&self.staged.given, Naming::Hidden)?;
fresh.target = self.staged.target.clone();
fresh.mode = self.staged.mode;
let built = (|| -> Result<hdf5::File> {
let src = open_read(&self.staged.tmp)?;
let dst = create_truncate(&fresh.tmp)?;
copy(&src.as_group()?, &dst.as_group()?)?;
close_everything(src, false)?;
Ok(dst)
})();
let handle = built?;
self.staged.discard();
self.staged = fresh;
self.handle = Some(handle);
Ok(self)
}
pub fn abort(mut self) {
self.discard();
}
fn discard(&mut self) {
if let Some(handle) = self.handle.take() {
let _ = close_everything(handle, false);
}
self.staged.discard();
}
}
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 seen = Snapshot::of(&src, source, "rewrite")?;
let mut out = AtomicFile::create(dst_path)?;
out.expect_unchanged(source.to_path_buf(), seen);
let value = build(&src, out.handle())?;
close_everything(src, false)?;
out.commit()?;
Ok(value)
}
#[doc(hidden)]
pub fn write_atomic(target: &Path, bytes: &[u8]) -> Result<()> {
let mut staged = Staged::create(target, Naming::Hidden)?;
fs::write(&staged.tmp, bytes).map_err(|e| crate::error::os_error(&e, target))?;
staged.commit(|| Ok(()))
}
#[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);
}
#[cfg(unix)]
fn another_group() -> Option<u32> {
let (egid, euid) = unsafe { (libc::getegid(), libc::geteuid()) };
let count = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
let mut groups = vec![0; usize::try_from(count).unwrap_or(0)];
let count = unsafe { libc::getgroups(count, groups.as_mut_ptr()) };
groups.truncate(usize::try_from(count).unwrap_or(0));
let nobody = if egid == 65534 { 65533 } else { 65534 };
groups.into_iter().find(|g| *g != egid).or((euid == 0).then_some(nobody))
}
#[cfg(unix)]
#[test]
fn s14_4_an_amend_keeps_the_files_group() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let Some(group) = another_group() else { return };
let dir = tempfile::tempdir().unwrap();
let (sample, report) = (dir.path().join("shared.medh5"), dir.path().join("shared.json"));
write_value(&sample, 1).unwrap();
write_atomic(&report, b"1").unwrap();
for path in [&sample, &report] {
std::os::unix::fs::chown(path, None, Some(group)).unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o640)).unwrap();
}
write_value(&sample, 2).unwrap();
write_atomic(&report, b"2").unwrap();
for path in [&sample, &report] {
let meta = fs::metadata(path).unwrap();
assert_eq!((meta.gid(), meta.mode() & 0o7777), (group, 0o640), "{path:?}");
}
assert_eq!((read_value(&sample), fs::read(&report).unwrap()), (2, b"2".to_vec()));
assert!(no_temporaries(dir.path()));
}
#[cfg(unix)]
#[test]
fn s14_4_a_group_not_given_is_granted_what_others_are() {
assert_eq!(narrowed(0o640), 0o600);
assert_eq!(narrowed(0o664), 0o644);
assert_eq!(narrowed(0o2770), 0o2700);
for mode in 0..0o10000 {
let n = narrowed(mode);
assert_eq!(n & !0o070, mode & !0o070, "{mode:o}");
assert_eq!(n >> 3 & 0o7, mode & 0o7, "{mode:o}");
}
let dir = tempfile::tempdir().unwrap();
assert_eq!(carry_owner(&dir.path().join("gone"), (0, 0), 0o660), 0o600);
}
#[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_as(Path::new("x.medh5"), Naming::Hidden)).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]);
}
fn replaced_by_another(path: &Path, value: i32) {
let other = path.with_extension("other");
fs::copy(path, &other).unwrap();
open_rw(&other).unwrap().dataset("v").unwrap().write(&[value]).unwrap();
fs::rename(&other, path).unwrap();
}
#[test]
fn s14_4_a_repack_refuses_a_source_that_changed() {
for in_place in [false, true] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rw.h5");
write_value(&path, 1).unwrap();
before_rename(Some(Box::new(move |target: &Path| {
if in_place {
open_rw(target).unwrap().dataset("v").unwrap().write(&[2]).unwrap();
} else {
replaced_by_another(target, 2);
}
})));
let rewritten = atomic_rewrite(&path, None, |src, dst| {
let value = src.dataset("v")?.read_raw::<i32>()?;
dst.new_dataset::<i32>().shape([1]).create("v")?.write(&value)?;
Ok(())
});
before_rename(None);
let err = rewritten.unwrap_err();
assert!(matches!(err, Error::File(_)), "{err:?}");
let shown = repr_str(&path.to_string_lossy());
assert_eq!(
err.to_string(),
format!("{shown} changed after this rewrite read it; nothing was written: run it again")
);
assert_eq!(read_value(&path), 2, "in place: {in_place}");
assert!(no_temporaries(dir.path()));
}
}
#[test]
fn s14_4_a_rewrite_of_an_unchanged_source_commits() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rw.h5");
write_value(&path, 1).unwrap();
atomic_rewrite(&path, None, |src, dst| {
let value = src.dataset("v")?.read_raw::<i32>()?;
dst.new_dataset::<i32>().shape([1]).create("v")?.write(&[value[0] + 1])?;
Ok(())
})
.unwrap();
assert_eq!(read_value(&path), 2);
}
#[cfg(unix)]
#[test]
fn s14_4_a_link_at_the_next_temporary_name_is_passed_over() {
let dir = tempfile::tempdir().unwrap();
let other = dir.path().join("other.txt");
fs::write(&other, b"someone else's").unwrap();
let target = dir.path().join("image.nii.gz");
let planted = dir.path().join(format!(".{}-00000000-image.nii.gz", std::process::id()));
std::os::unix::fs::symlink(&other, &planted).unwrap();
NEXT_UNIQUE.with(|queue| *queue.borrow_mut() = vec![1, 0]);
let staged = StagedFile::create(&target).unwrap();
NEXT_UNIQUE.with(|queue| queue.borrow_mut().clear());
assert_eq!(staged.path(), dir.path().join(format!(".{}-00000001-image.nii.gz", std::process::id())));
assert!(staged.path().to_string_lossy().ends_with(".nii.gz"));
fs::write(staged.path(), b"the image").unwrap();
staged.commit().unwrap();
assert_eq!(fs::read(&target).unwrap(), b"the image");
assert_eq!(fs::read(&other).unwrap(), b"someone else's");
}
#[cfg(unix)]
#[test]
fn s14_4_every_replace_keeps_the_link_and_the_mode() {
use std::os::unix::fs::PermissionsExt;
type Write = fn(&Path) -> Result<()>;
let writes: [(&str, Write); 3] = [
("write_atomic", |t| write_atomic(t, b"one\ntwo\n")),
("staged", |t| {
let staged = StagedFile::create(t)?;
fs::write(staged.path(), b"one\ntwo\n")?;
staged.commit()
}),
("hdf5", |t| write_value(t, 7)),
];
for (name, write) in writes {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("file");
fs::write(&file, b"old").unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o600)).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink("file", &link).unwrap();
write(&link).unwrap();
assert!(fs::symlink_metadata(&link).unwrap().file_type().is_symlink(), "{name}");
assert_eq!(fs::metadata(&file).unwrap().permissions().mode() & 0o777, 0o600, "{name}");
if name != "hdf5" {
assert_eq!(fs::read(&file).unwrap(), b"one\ntwo\n", "{name}");
} else {
assert_eq!(read_value(&file), 7);
}
let mut left: Vec<String> = fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
left.sort();
assert_eq!(left, ["file", "link"], "{name}");
}
}
#[test]
fn s14_4_an_aborted_staged_file_changes_nothing() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("r.json");
fs::write(&target, b"before").unwrap();
let staged = StagedFile::create(&target).unwrap();
fs::write(staged.path(), b"after").unwrap();
staged.abort();
drop(StagedFile::create(&target).unwrap());
assert_eq!(fs::read(&target).unwrap(), b"before");
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
}
}