use std::fs::{File, TryLockError};
use std::path::Path;
use crate::error::Error;
use crate::superblock::Superblock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FileLocking {
#[default]
Enabled,
Disabled,
BestEffort,
}
fn parse_env(value: &str) -> Option<FileLocking> {
let v = value.trim();
if v.eq_ignore_ascii_case("FALSE")
|| v == "0"
|| v.eq_ignore_ascii_case("NO")
|| v.eq_ignore_ascii_case("OFF")
{
Some(FileLocking::Disabled)
} else if v.eq_ignore_ascii_case("BEST_EFFORT") {
Some(FileLocking::BestEffort)
} else if v.eq_ignore_ascii_case("TRUE")
|| v == "1"
|| v.eq_ignore_ascii_case("YES")
|| v.eq_ignore_ascii_case("ON")
{
Some(FileLocking::Enabled)
} else {
None
}
}
fn resolve(requested: FileLocking) -> FileLocking {
std::env::var("HDF5_USE_FILE_LOCKING")
.ok()
.and_then(|v| parse_env(&v))
.unwrap_or(requested)
}
pub(crate) fn acquire_exclusive(
handle: &File,
requested: FileLocking,
path: &Path,
) -> Result<(), Error> {
let mode = resolve(requested);
if mode == FileLocking::Disabled {
return Ok(());
}
match handle.try_lock() {
Ok(()) => Ok(()),
Err(TryLockError::WouldBlock) => Err(Error::FileLocked(format!(
"{}: file is already locked by another process. If a previous writer \
crashed, the OS lock is released automatically (try again); a leftover \
on-disk SWMR flag can be cleared with File::clear_swmr_flag. Set \
HDF5_USE_FILE_LOCKING=FALSE or pass FileLocking::Disabled to bypass locking.",
path.display(),
))),
Err(TryLockError::Error(e)) => match mode {
FileLocking::BestEffort => Ok(()),
_ => Err(Error::Io(e)),
},
}
}
pub(crate) const WRITE_ACCESS: u32 = 0x01;
pub(crate) const SWMR_WRITE_ACCESS: u32 = 0x04;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OpenIntent {
Read,
SwmrRead,
Write,
}
pub(crate) fn check_status_flags(
superblock: &Superblock,
intent: OpenIntent,
path: &Path,
) -> Result<(), Error> {
if superblock.version < 3 {
return Ok(());
}
let flags = superblock.consistency_flags;
let write = flags & WRITE_ACCESS != 0;
let swmr = flags & SWMR_WRITE_ACCESS != 0;
let reason = match intent {
OpenIntent::Write if write || swmr => format!(
"the superblock marks the file as open for write (status flags {flags:#04x}), so \
another writer holds it. Open it read-only, or — if a writer exited without \
closing the file — clear the flag with File::clear_swmr_flag"
),
OpenIntent::Read if write || swmr => format!(
"the superblock marks the file as open for write (status flags {flags:#04x}), so a \
snapshot read is not safe. Use File::open_swmr to follow a live SWMR writer, or \
File::from_bytes to read the bytes as they stand; if a writer exited without \
closing the file, clear the flag with File::clear_swmr_flag"
),
OpenIntent::SwmrRead if write != swmr => format!(
"the superblock's status flags disagree ({flags:#04x}): a SWMR reader needs a SWMR \
writer (both the write and SWMR-write bits) or a quiescent file (neither). Clear \
them with File::clear_swmr_flag if a writer exited without closing the file"
),
_ => return Ok(()),
};
Err(Error::FileMarkedInUse(format!(
"{}: {reason}.",
path.display(),
)))
}
pub(crate) fn clear_swmr_flag_at(path: &Path) -> Result<(), Error> {
use crate::signature;
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
let mut w = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
acquire_exclusive(&w, FileLocking::Enabled, path)?;
let mut data = Vec::new();
w.read_to_end(&mut data).map_err(Error::Io)?;
let sig = signature::find_signature(&data)?;
let mut sb = Superblock::parse(&data, sig)?;
if sb.version < 2 {
return Ok(());
}
if sb.consistency_flags == 0 {
return Ok(());
}
sb.consistency_flags = 0;
let bytes = sb.serialize();
w.seek(SeekFrom::Start(sig as u64)).map_err(Error::Io)?;
w.write_all(&bytes).map_err(Error::Io)?;
w.sync_data().map_err(Error::Io)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_env_recognizes_disable_values() {
for v in ["FALSE", "false", "0", "No", "off", " false "] {
assert_eq!(parse_env(v), Some(FileLocking::Disabled), "value {v:?}");
}
}
#[test]
fn parse_env_recognizes_enable_and_best_effort() {
for v in ["TRUE", "true", "1", "Yes", "on"] {
assert_eq!(parse_env(v), Some(FileLocking::Enabled), "value {v:?}");
}
assert_eq!(parse_env("BEST_EFFORT"), Some(FileLocking::BestEffort));
assert_eq!(parse_env("best_effort"), Some(FileLocking::BestEffort));
}
#[test]
fn parse_env_unrecognized_is_none() {
assert_eq!(parse_env(""), None);
assert_eq!(parse_env("maybe"), None);
assert_eq!(parse_env("2"), None);
}
#[test]
fn default_is_enabled() {
assert_eq!(FileLocking::default(), FileLocking::Enabled);
}
fn flagged(version: u8, flags: u32) -> Superblock {
Superblock {
version,
offset_size: 8,
length_size: 8,
base_address: 0,
eof_address: 0,
root_group_address: 0,
group_leaf_node_k: None,
group_internal_node_k: None,
indexed_storage_internal_node_k: None,
free_space_address: None,
driver_info_address: None,
consistency_flags: flags,
superblock_extension_address: None,
checksum: None,
}
}
fn allows(version: u8, flags: u32, intent: OpenIntent) -> bool {
check_status_flags(&flagged(version, flags), intent, Path::new("f.h5")).is_ok()
}
#[test]
fn status_flag_rules_per_intent() {
for flags in [0x00, WRITE_ACCESS, SWMR_WRITE_ACCESS, 0x05] {
let held = flags != 0;
assert_eq!(
allows(3, flags, OpenIntent::Write),
!held,
"a writer may open a file only when no flag claims it (flags {flags:#04x})"
);
assert_eq!(
allows(3, flags, OpenIntent::Read),
!held,
"a snapshot read is refused whenever a writer holds the file (flags {flags:#04x})"
);
assert_eq!(
allows(3, flags, OpenIntent::SwmrRead),
flags == 0x00 || flags == 0x05,
"a SWMR reader needs both bits or neither (flags {flags:#04x})"
);
}
}
#[test]
fn the_file_ok_bit_alone_refuses_nothing() {
for intent in [OpenIntent::Read, OpenIntent::SwmrRead, OpenIntent::Write] {
assert!(allows(3, 0x02, intent), "{intent:?} refused flags 0x02");
}
}
#[test]
fn an_older_superblock_is_not_checked() {
for version in [0, 1, 2] {
for intent in [OpenIntent::Read, OpenIntent::SwmrRead, OpenIntent::Write] {
assert!(
allows(version, 0x05, intent),
"v{version} superblock refused {intent:?} on flags 0x05"
);
}
}
}
#[test]
fn the_refusal_names_the_recovery() {
let err = check_status_flags(&flagged(3, 0x05), OpenIntent::Read, Path::new("d.h5"))
.expect_err("a flagged file is refused for a snapshot read");
let msg = err.to_string();
assert!(matches!(err, Error::FileMarkedInUse(_)), "got {err:?}");
for part in ["d.h5", "0x05", "clear_swmr_flag", "open_swmr", "from_bytes"] {
assert!(msg.contains(part), "refusal does not mention {part}: {msg}");
}
}
fn write_file_with(path: &Path, version: u8, flags: u32) {
let mut bytes = crate::writer::FileBuilder::new().finish().unwrap();
let off = crate::signature::find_signature(&bytes).unwrap();
let mut sb = Superblock::parse(&bytes, off).unwrap();
assert_eq!(sb.version, 3, "this writer emits a v3 superblock");
sb.version = version;
sb.consistency_flags = flags;
let patched = sb.serialize();
bytes[off..off + patched.len()].copy_from_slice(&patched);
std::fs::write(path, &bytes).unwrap();
}
#[test]
fn a_flagged_v2_file_still_opens() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v2.h5");
write_file_with(&path, 2, WRITE_ACCESS | SWMR_WRITE_ACCESS);
let file = crate::File::open(&path).expect("a v2 file's status flags are not checked");
assert_eq!(file.superblock().consistency_flags, 0x05);
}
#[test]
fn the_swmr_writer_refuses_a_superblock_the_gate_would_skip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v2.h5");
write_file_with(&path, 2, 0);
let err = crate::File::open_swmr_writer(&path)
.expect_err("SWMR writing requires a v3 superblock");
assert!(
matches!(err, Error::SwmrAppendUnsupported(_)),
"got {err:?}"
);
let bytes = std::fs::read(&path).unwrap();
let off = crate::signature::find_signature(&bytes).unwrap();
assert_eq!(
bytes[off + 11],
0,
"a refused writer must not have flagged the file on its way out"
);
}
#[test]
fn a_swmr_reader_refuses_write_access_without_the_swmr_bit() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("half.h5");
write_file_with(&path, 3, WRITE_ACCESS);
let err = crate::File::open_swmr(&path)
.expect_err("a SWMR reader needs a SWMR writer, not a plain one");
assert!(matches!(err, Error::FileMarkedInUse(_)), "got {err:?}");
assert!(
crate::File::open(&path).is_err(),
"a snapshot read is refused too"
);
write_file_with(&path, 3, WRITE_ACCESS | SWMR_WRITE_ACCESS);
crate::File::open_swmr(&path).expect("the full pair is the writer it follows");
}
}