hdf5_pure/file_lock.rs
1//! The two guards that decide who may open a file: OS advisory locking for the
2//! in-place editor (issue #73), and the superblock's durable status-flags byte
3//! (issue #245).
4//!
5//! Locking is the crash-safe half of HDF5's concurrency model and the `hdf5-pure`
6//! analogue of `H5Pset_file_locking` / the `HDF5_USE_FILE_LOCKING` environment
7//! variable. It is deliberately distinct from the *superblock consistency flag*
8//! (the durable `status_flags` byte a SWMR writer sets; see [`crate::File::open_swmr_writer`]):
9//!
10//! - An **OS lock** is owned by the kernel and tied to the open file. It is
11//! released automatically when the process exits *for any reason* — clean exit,
12//! panic, `SIGKILL`, even power loss — so it never leaves stale state and is
13//! the authoritative signal for "a writer is alive *right now*".
14//! - The **on-disk flag** is just a byte; only userspace code at clean shutdown
15//! can reset it, so a crash freezes it set. Recover it with
16//! [`crate::File::clear_swmr_flag`] (the `h5clear -s` equivalent).
17//! A crash freezing it set is also what makes it useful beyond SWMR — see
18//! [`WRITE_ACCESS`], which a page-buffered session raises for its lifetime.
19//!
20//! Both are enforced here: [`acquire_exclusive`] takes the lock, and
21//! [`check_status_flags`] refuses an open the on-disk byte says is unsafe
22//! (issue #245). The two cover different windows — the lock catches a live
23//! writer in this or another process, the flag catches one that is live *or*
24//! crashed, including a SWMR writer that holds no lock at all.
25//!
26//! ## Lock scope: the in-place editor only
27//!
28//! Only [`crate::File::open_rw`] (and the [`crate::File::clear_swmr_flag`]
29//! recovery rewrite) take a lock — an **exclusive** one — so a second editor or
30//! a concurrent writer cannot open the file. [`crate::File::open_swmr_writer`] and the
31//! readers ([`crate::File::open`] and friends) take **no** lock, on purpose:
32//!
33//! - SWMR is single-writer-*by-contract* and is designed for concurrent reads;
34//! the reference library itself runs SWMR with file locking disabled. Holding
35//! a lock would defeat the "multiple-reader" half.
36//! - Crucially, [`std::fs::File`] locking is **advisory on Unix** (`flock`) but
37//! **mandatory on Windows** (`LockFileEx`): a held lock there blocks *reads* by
38//! every other handle, not just other lock attempts. A whole-file lock on a
39//! SWMR writer would therefore make the file unreadable to its readers on
40//! Windows. Confining locking to the exclusive editor keeps reads working on
41//! every platform. (One consequence: while an editor holds the lock, a
42//! concurrent read of the same file is permitted on Unix but blocked by the OS
43//! on Windows — drop the editor before reading the file back.)
44//!
45//! Locking uses the cross-platform [`std::fs::File`] lock API, so it adds no
46//! dependency, and it lives only in the already `std`-gated edit path, so
47//! `no_std`/`wasm` builds are unaffected.
48
49use std::fs::{File, TryLockError};
50use std::path::Path;
51
52use crate::error::Error;
53use crate::superblock::Superblock;
54
55/// Policy for OS advisory file locking when opening a file for editing.
56///
57/// The default is [`FileLocking::Enabled`]. The `HDF5_USE_FILE_LOCKING`
58/// environment variable, when set to a recognized value, overrides the requested
59/// policy (matching the reference HDF5 library): `FALSE`/`0`/`NO`/`OFF` disable
60/// locking, `BEST_EFFORT` selects best-effort, and `TRUE`/`1`/`YES`/`ON` enable
61/// it.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum FileLocking {
64 /// Acquire the lock, and fail the open with [`Error::Io`] if the filesystem
65 /// does not support locking. A lock held by another process always fails the
66 /// open with [`Error::FileLocked`].
67 #[default]
68 Enabled,
69 /// Do not attempt to lock the file at all.
70 Disabled,
71 /// Attempt to lock, but proceed *without* a lock when the filesystem reports
72 /// that locking is unavailable (e.g. some NFS / network mounts). A lock that
73 /// is genuinely *held* by another process still fails the open. Mirrors the
74 /// reference library's `BEST_EFFORT` / `ignore_disabled_locks`.
75 BestEffort,
76}
77
78/// Parse a recognized `HDF5_USE_FILE_LOCKING` value into a policy, or `None` for
79/// an unrecognized value (in which case the requested policy is kept).
80///
81/// Pure (no environment access) so it can be unit-tested without the
82/// process-global, edition-2024-`unsafe` env mutators.
83fn parse_env(value: &str) -> Option<FileLocking> {
84 let v = value.trim();
85 if v.eq_ignore_ascii_case("FALSE")
86 || v == "0"
87 || v.eq_ignore_ascii_case("NO")
88 || v.eq_ignore_ascii_case("OFF")
89 {
90 Some(FileLocking::Disabled)
91 } else if v.eq_ignore_ascii_case("BEST_EFFORT") {
92 Some(FileLocking::BestEffort)
93 } else if v.eq_ignore_ascii_case("TRUE")
94 || v == "1"
95 || v.eq_ignore_ascii_case("YES")
96 || v.eq_ignore_ascii_case("ON")
97 {
98 Some(FileLocking::Enabled)
99 } else {
100 None
101 }
102}
103
104/// Apply the `HDF5_USE_FILE_LOCKING` environment override to a requested policy.
105/// The environment variable, when set to a recognized value, takes precedence.
106fn resolve(requested: FileLocking) -> FileLocking {
107 std::env::var("HDF5_USE_FILE_LOCKING")
108 .ok()
109 .and_then(|v| parse_env(&v))
110 .unwrap_or(requested)
111}
112
113/// Acquire an **exclusive** advisory lock on `handle` for a writer open.
114///
115/// Non-blocking: if another process holds a conflicting lock, this returns
116/// [`Error::FileLocked`] immediately rather than waiting. The lock is released
117/// when `handle` is dropped (or the process exits, including on a crash).
118pub(crate) fn acquire_exclusive(
119 handle: &File,
120 requested: FileLocking,
121 path: &Path,
122) -> Result<(), Error> {
123 let mode = resolve(requested);
124 if mode == FileLocking::Disabled {
125 return Ok(());
126 }
127 match handle.try_lock() {
128 Ok(()) => Ok(()),
129 // A conflicting lock is genuinely held by another process: the file is
130 // in use. `BestEffort` does not soften this — only *unavailable* locking
131 // is tolerated, not active contention.
132 Err(TryLockError::WouldBlock) => Err(Error::FileLocked(format!(
133 "{}: file is already locked by another process. If a previous writer \
134 crashed, the OS lock is released automatically (try again); a leftover \
135 on-disk SWMR flag can be cleared with File::clear_swmr_flag. Set \
136 HDF5_USE_FILE_LOCKING=FALSE or pass FileLocking::Disabled to bypass locking.",
137 path.display(),
138 ))),
139 // Locking failed for another reason — typically the filesystem does not
140 // support advisory locks (some NFS / network mounts).
141 Err(TryLockError::Error(e)) => match mode {
142 FileLocking::BestEffort => Ok(()),
143 _ => Err(Error::Io(e)),
144 },
145 }
146}
147
148/// Superblock status-flag bit 0 (`H5F_SUPER_WRITE_ACCESS`): the file is open
149/// for write access. The reference C library raises it for *any* writer; this
150/// crate raises it for two:
151///
152/// - [`crate::File::open_swmr_writer`], alongside [`SWMR_WRITE_ACCESS`];
153/// - a session given a page buffer
154/// ([`crate::FileAccessProperties::with_page_buffer_size`]), which raises this
155/// bit alone. That session holds dirty pages across the write engine's
156/// ordering barriers, so a process that died mid-flush could leave a file that
157/// reads clean and returns the wrong bytes; the mark makes it a file every
158/// reader refuses instead (issue #308), until one says with
159/// [`WriteMarkPolicy::AllowSnapshot`] that it knows what it is reading.
160///
161/// An ordinary [`crate::File::open_rw`] session raises nothing, and is guarded by
162/// the OS lock alone.
163pub(crate) const WRITE_ACCESS: u32 = 0x01;
164
165/// Superblock status-flag bit 2 (`H5F_SUPER_SWMR_WRITE_ACCESS`): the writer
166/// holding the file is a SWMR writer, so a SWMR reader may attach to it.
167pub(crate) const SWMR_WRITE_ACCESS: u32 = 0x04;
168
169/// Policy for a read-only open of a file whose superblock marks it as open for
170/// write by a writer that is *not* a SWMR writer — superblock status-flag bit 0
171/// (`H5F_SUPER_WRITE_ACCESS`) alone, the mark a page-buffered session
172/// ([`crate::FileAccessProperties::with_page_buffer_size`]) holds for its life.
173///
174/// Set it with
175/// [`FileAccessProperties::with_write_mark_policy`](crate::FileAccessProperties::with_write_mark_policy),
176/// which states when the assertion [`AllowSnapshot`](Self::AllowSnapshot) makes
177/// is true. It governs the read-only opens only: a SWMR pair is followed with
178/// [`crate::File::open_swmr`] whatever this says, and no value of it lets a
179/// second writer join a file a writer holds.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub enum WriteMarkPolicy {
182 /// Refuse the open with [`Error::FileMarkedInUse`]. The default, and what
183 /// `H5Fopen` does with the same byte.
184 #[default]
185 Refuse,
186 /// Read the file as it stands, on the caller's assertion that the writer
187 /// has flushed: it called [`crate::File::sync`], or it stopped after a flush
188 /// and the mark stands only because nothing cleared it. Every other refusal
189 /// stays in place.
190 AllowSnapshot,
191}
192
193/// What an open intends to do with the file, selecting which status-flag
194/// combinations [`check_status_flags`] refuses.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub(crate) enum OpenIntent {
197 /// A plain read — [`crate::File::open`] and
198 /// [`crate::File::open_streaming`], the C library's `H5F_ACC_RDONLY`.
199 ///
200 /// Carries the caller's [`WriteMarkPolicy`], which is the one intent it can
201 /// mean anything for: the other two have no snapshot to take.
202 Read(WriteMarkPolicy),
203 /// A SWMR read — [`crate::File::open_swmr`], the C library's
204 /// `H5F_ACC_RDONLY | H5F_ACC_SWMR_READ`.
205 SwmrRead,
206 /// An open that may modify the file — [`crate::File::open_rw`] and
207 /// [`crate::File::open_swmr_writer`], the C library's `H5F_ACC_RDWR` with or
208 /// without `H5F_ACC_SWMR_WRITE`.
209 Write,
210}
211
212/// What an open was asked to open, for the refusal to name.
213///
214/// The companion of [`OpenIntent`]: that says why the file was being opened,
215/// this says what the file *was*. Neither reaches the decision — the superblock
216/// makes that alone — but both shape what the error can usefully say, and the
217/// difference is not only cosmetic. Two of the three recoveries a refused
218/// snapshot read would otherwise be told to try, [`crate::File::open_swmr`] and
219/// [`crate::File::clear_swmr_flag`], need a filesystem path; naming them to a
220/// caller who opened a byte source would be advice it cannot take.
221///
222/// A closed set rather than a `&dyn Display`, so the wording every refusal
223/// leads with lives here beside the reasons it is concatenated with, and a
224/// third kind of open arrives as a variant a reviewer sees.
225#[derive(Debug, Clone, Copy)]
226pub(crate) enum OpenTarget<'a> {
227 /// A file opened by name, which the refusal reports and whose flag
228 /// [`crate::File::clear_swmr_flag`] can clear in place.
229 Path(&'a Path),
230 /// Bytes handed to [`crate::File::from_source`], which have no name and no
231 /// in-place recovery.
232 Source,
233}
234
235impl OpenTarget<'_> {
236 /// What a caller refused a snapshot read can do instead, which depends on
237 /// both the target and the mark.
238 ///
239 /// The target, because a source has no path for `open_swmr` to follow or for
240 /// `clear_swmr_flag` to write to. The mark, because `open_swmr` follows a
241 /// SWMR writer and nothing else: naming it to a caller refused by
242 /// [`WRITE_ACCESS`] alone would name a recovery that fails in its turn, the
243 /// mismatched pair a SWMR reader refuses (issue #419). That caller's opt-in
244 /// is [`WriteMarkPolicy::AllowSnapshot`], which is what this names instead.
245 ///
246 /// `swmr_claimed` is the SWMR bit, not the pair: a file carrying it without
247 /// [`WRITE_ACCESS`] claims a SWMR writer this crate's opt-in deliberately
248 /// does not unlock, so it is pointed at the SWMR reader — whose own refusal
249 /// then names the inconsistency — rather than at an opt-in that would refuse
250 /// it again.
251 fn read_recovery(self, swmr_claimed: bool) -> &'static str {
252 match (self, swmr_claimed) {
253 (Self::Path(_), true) => {
254 "Use File::open_swmr to follow a live SWMR writer, or File::from_bytes to read \
255 the bytes as they stand; if a writer exited without closing the file, clear \
256 the flag with File::clear_swmr_flag"
257 }
258 (Self::Source, true) => {
259 "Use File::from_bytes to read the bytes as they stand; following a live writer \
260 with File::open_swmr, and clearing a flag a writer left behind with \
261 File::clear_swmr_flag, both need a filesystem path"
262 }
263 (Self::Path(_), false) => {
264 "No SWMR writer holds it, so no reader can follow it as one. Pass \
265 FileAccessProperties::with_write_mark_policy(WriteMarkPolicy::AllowSnapshot) to \
266 read it as it stands, which is a consistent snapshot once the writer has called \
267 File::sync or closed the file, or use File::from_bytes to read the bytes as they \
268 stand; if a writer exited without closing the file, clear the flag with \
269 File::clear_swmr_flag"
270 }
271 (Self::Source, false) => {
272 "No SWMR writer holds it, so no reader can follow it as one. Pass \
273 FileAccessProperties::with_write_mark_policy(WriteMarkPolicy::AllowSnapshot) to \
274 read it as it stands, which is a consistent snapshot once the writer has called \
275 File::sync or closed the file, or use File::from_bytes to read the bytes as they \
276 stand; clearing a flag a writer left behind with File::clear_swmr_flag needs a \
277 filesystem path"
278 }
279 }
280 }
281}
282
283impl core::fmt::Display for OpenTarget<'_> {
284 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
285 match self {
286 Self::Path(path) => write!(f, "{}", path.display()),
287 Self::Source => f.write_str("a byte source"),
288 }
289 }
290}
291
292/// Refuse an open the superblock's status-flags byte says is unsafe, matching
293/// `H5F_open`'s check of the same byte (issue #245).
294///
295/// The byte records that a writer holds the file. It is *durable*, so it means
296/// either "a writer is active right now" or "a writer exited without clearing
297/// it" — the two are indistinguishable from the byte alone, which is why the
298/// refusal names [`crate::File::clear_swmr_flag`] as the recovery rather than
299/// guessing. The rules, one per intent:
300///
301/// - [`Write`](OpenIntent::Write) refuses either bit: a second writer must not
302/// join a file a writer already holds. This is the one case an OS lock does
303/// not already cover, because a SWMR writer takes no lock.
304/// - [`Read`](OpenIntent::Read) refuses either bit: a plain reader buffers a
305/// snapshot with no protocol for a writer mutating the file underneath it. To
306/// follow a live SWMR writer, use [`crate::File::open_swmr`]. The one way past
307/// it is [`WriteMarkPolicy::AllowSnapshot`], which the intent carries: it
308/// admits a read of [`WRITE_ACCESS`] *alone* — the mark a page-buffered
309/// session leaves, which no SWMR reader can follow (issue #419) — on the
310/// caller's assertion that the writer has flushed. It does not admit a SWMR
311/// pair, which has a reader of its own.
312/// - [`SwmrRead`](OpenIntent::SwmrRead) refuses only a *mismatched* pair — one
313/// bit without the other. Both bits is exactly the live SWMR writer it exists
314/// to follow, and neither is a quiescent file.
315///
316/// ## Why only a version-3 superblock
317///
318/// The check is gated to superblock version 3 and up because that is where the
319/// C library gates it, and a divergence in either direction is a real cost: the
320/// C library raises the write bit on a version-0/1/2 file too and never reads
321/// it back, so checking those versions would refuse files `H5Fopen` accepts —
322/// every file left behind by a crashed C writer that predates SWMR. Nothing is
323/// lost by matching it: both paths that raise a flag here require a version-3
324/// superblock — SWMR writing because both libraries do (see
325/// [`crate::File::open_swmr_writer`]), and a page buffer because it refuses to
326/// buffer behind a mark no reader would honor — so no flag this crate raises
327/// falls outside the gate.
328///
329/// `target` does not enter the decision, which the superblock and the intent's
330/// own policy make between them. It shapes the error: what to call the file that
331/// was refused, and which recoveries to name, since some of them need a
332/// filesystem path.
333pub(crate) fn check_status_flags(
334 superblock: &Superblock,
335 intent: OpenIntent,
336 target: OpenTarget<'_>,
337) -> Result<(), Error> {
338 if superblock.version < 3 {
339 return Ok(());
340 }
341 let flags = superblock.consistency_flags;
342 let write = flags & WRITE_ACCESS != 0;
343 let swmr = flags & SWMR_WRITE_ACCESS != 0;
344 // Each arm states the whole condition: "marked open for write" describes two
345 // of the three, and reporting the SWMR-read mismatch that way would misname
346 // the case where only the SWMR bit is set.
347 let reason = match intent {
348 OpenIntent::Write if write || swmr => format!(
349 "the superblock marks the file as open for write (status flags {flags:#04x}), so \
350 another writer holds it. Open it read-only, or — if a writer exited without \
351 closing the file — clear the flag with File::clear_swmr_flag"
352 ),
353 // The opt-in is checked inside the arm rather than in its guard so that
354 // the arm keeps stating the whole rule in one place: what the flags say,
355 // and the one assertion that overrides it.
356 OpenIntent::Read(policy) if write || swmr => {
357 if policy == WriteMarkPolicy::AllowSnapshot && !swmr {
358 return Ok(());
359 }
360 format!(
361 "the superblock marks the file as open for write (status flags {flags:#04x}), so \
362 a snapshot read is not safe. {}",
363 target.read_recovery(swmr)
364 )
365 }
366 OpenIntent::SwmrRead if write != swmr => format!(
367 "the superblock's status flags disagree ({flags:#04x}): a SWMR reader needs a SWMR \
368 writer (both the write and SWMR-write bits) or a quiescent file (neither). Clear \
369 them with File::clear_swmr_flag if a writer exited without closing the file"
370 ),
371 _ => return Ok(()),
372 };
373 Err(Error::FileMarkedInUse(format!("{target}: {reason}.")))
374}
375
376/// Clear a stale status flag left in `path` by a writer that exited without a
377/// clean close — the `h5clear -s` equivalent, behind
378/// [`File::clear_swmr_flag`](crate::File::clear_swmr_flag). Safe to call on a
379/// file whose flag is already clear.
380///
381/// It clears the byte whole, so it recovers a page-buffered session's crash mark
382/// ([`WRITE_ACCESS`]) as well as a SWMR writer's pair. What it recovers is
383/// *access*, not correctness: a page-buffered writer that crashed may have left
384/// the file inconsistent in ways no checksum shows, which is the whole reason
385/// the mark stands. `h5clear` makes the same trade.
386///
387/// This is the one recovery rewrite that takes the exclusive lock without going
388/// through the editor, which is why it lives beside the locking policy it
389/// depends on.
390pub(crate) fn clear_swmr_flag_at(path: &Path) -> Result<(), Error> {
391 use crate::signature;
392 use std::fs::OpenOptions;
393 use std::io::{Read, Seek, SeekFrom, Write};
394
395 let mut w = OpenOptions::new()
396 .read(true)
397 .write(true)
398 .open(path)
399 .map_err(Error::Io)?;
400 // Refuse to clear the flag out from under a live writer: an exclusive
401 // lock here fails with `FileLocked` if another writer still holds the
402 // file. A stale flag from a *crashed* writer has no live lock, so this
403 // succeeds and the recovery proceeds.
404 acquire_exclusive(&w, FileLocking::Enabled, path)?;
405 let mut data = Vec::new();
406 w.read_to_end(&mut data).map_err(Error::Io)?;
407 let sig = signature::find_signature(&data)?;
408 let mut sb = Superblock::parse(&data, sig)?;
409 if sb.version < 2 {
410 // `Superblock::serialize` emits the v2/v3 layout, so rewriting a
411 // v0/v1 superblock here would corrupt it. This crate never SWMR-flags
412 // a v0/v1 file, so there is nothing to clear; treat it as already
413 // clean rather than risk a destructive rewrite.
414 return Ok(());
415 }
416 if sb.consistency_flags == 0 {
417 return Ok(());
418 }
419 sb.consistency_flags = 0;
420 let bytes = sb.serialize();
421 w.seek(SeekFrom::Start(sig as u64)).map_err(Error::Io)?;
422 w.write_all(&bytes).map_err(Error::Io)?;
423 w.sync_data().map_err(Error::Io)?;
424 Ok(())
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 use crate::width::LengthWidth;
432 use crate::width::OffsetWidth;
433
434 #[test]
435 fn parse_env_recognizes_disable_values() {
436 for v in ["FALSE", "false", "0", "No", "off", " false "] {
437 assert_eq!(parse_env(v), Some(FileLocking::Disabled), "value {v:?}");
438 }
439 }
440
441 #[test]
442 fn parse_env_recognizes_enable_and_best_effort() {
443 for v in ["TRUE", "true", "1", "Yes", "on"] {
444 assert_eq!(parse_env(v), Some(FileLocking::Enabled), "value {v:?}");
445 }
446 assert_eq!(parse_env("BEST_EFFORT"), Some(FileLocking::BestEffort));
447 assert_eq!(parse_env("best_effort"), Some(FileLocking::BestEffort));
448 }
449
450 #[test]
451 fn parse_env_unrecognized_is_none() {
452 assert_eq!(parse_env(""), None);
453 assert_eq!(parse_env("maybe"), None);
454 assert_eq!(parse_env("2"), None);
455 }
456
457 #[test]
458 fn default_is_enabled() {
459 assert_eq!(FileLocking::default(), FileLocking::Enabled);
460 }
461
462 /// A minimal superblock carrying `version` and `flags`; every other field is
463 /// irrelevant to the status-flag rules.
464 fn flagged(version: u8, flags: u32) -> Superblock {
465 Superblock {
466 version,
467 offset_size: OffsetWidth::Eight.get(),
468 offset_width: OffsetWidth::Eight,
469 length_size: LengthWidth::Eight.get(),
470 length_width: LengthWidth::Eight,
471 base_address: crate::address::BaseAddress::ZERO,
472 eof_address: 0,
473 root_group_address: 0,
474 group_leaf_node_k: None,
475 group_internal_node_k: None,
476 indexed_storage_internal_node_k: None,
477 free_space_address: None,
478 driver_info_address: None,
479 consistency_flags: flags,
480 superblock_extension_address: None,
481 checksum: None,
482 }
483 }
484
485 /// A snapshot read under the default policy, which is what every rule
486 /// below is stated against unless it names the opt-in.
487 const READ: OpenIntent = OpenIntent::Read(WriteMarkPolicy::Refuse);
488
489 fn allows(version: u8, flags: u32, intent: OpenIntent) -> bool {
490 check_status_flags(
491 &flagged(version, flags),
492 intent,
493 OpenTarget::Path(Path::new("f.h5")),
494 )
495 .is_ok()
496 }
497
498 /// The full rule, stated per intent rather than per flag value, so a table
499 /// entry that moves has to move for a reason someone can name.
500 #[test]
501 fn status_flag_rules_per_intent() {
502 for flags in [0x00, WRITE_ACCESS, SWMR_WRITE_ACCESS, 0x05] {
503 let held = flags != 0;
504 assert_eq!(
505 allows(3, flags, OpenIntent::Write),
506 !held,
507 "a writer may open a file only when no flag claims it (flags {flags:#04x})"
508 );
509 assert_eq!(
510 allows(3, flags, READ),
511 !held,
512 "a snapshot read is refused whenever a writer holds the file (flags {flags:#04x})"
513 );
514 assert_eq!(
515 allows(3, flags, OpenIntent::SwmrRead),
516 flags == 0x00 || flags == 0x05,
517 "a SWMR reader needs both bits or neither (flags {flags:#04x})"
518 );
519 }
520 }
521
522 /// Bit 1 (`H5F_SUPER_FILE_OK`) is not one of the two the C library consults,
523 /// so a file carrying only it opens for any intent.
524 #[test]
525 fn the_file_ok_bit_alone_refuses_nothing() {
526 for intent in [READ, OpenIntent::SwmrRead, OpenIntent::Write] {
527 assert!(allows(3, 0x02, intent), "{intent:?} refused flags 0x02");
528 }
529 }
530
531 /// Versions below 3 are not checked at all: the C library raises the write
532 /// bit on them and never reads it back, so refusing one would refuse a file
533 /// `H5Fopen` accepts.
534 #[test]
535 fn an_older_superblock_is_not_checked() {
536 for version in [0, 1, 2] {
537 for intent in [READ, OpenIntent::SwmrRead, OpenIntent::Write] {
538 assert!(
539 allows(version, 0x05, intent),
540 "v{version} superblock refused {intent:?} on flags 0x05"
541 );
542 }
543 }
544 }
545
546 /// The refusal has to say what to do next: the path, the flags, and the
547 /// recovery a user would otherwise have to find in the C library's docs.
548 #[test]
549 fn the_refusal_names_the_recovery() {
550 let err = check_status_flags(&flagged(3, 0x05), READ, OpenTarget::Path(Path::new("d.h5")))
551 .expect_err("a flagged file is refused for a snapshot read");
552 let msg = err.to_string();
553 assert!(matches!(err, Error::FileMarkedInUse(_)), "got {err:?}");
554 // `from_bytes` is named because it is the only way through for a flagged
555 // file on a read-only mount, where `clear_swmr_flag` cannot get the write
556 // access it needs.
557 for part in ["d.h5", "0x05", "clear_swmr_flag", "open_swmr", "from_bytes"] {
558 assert!(msg.contains(part), "refusal does not mention {part}: {msg}");
559 }
560 }
561
562 /// The recovery a source is offered is the one a source can reach.
563 ///
564 /// Two of the three the path refusal names — `open_swmr` to follow the
565 /// writer, `clear_swmr_flag` to clear what one left behind — take a path,
566 /// so handing them to a caller that opened a byte source would be advice it
567 /// cannot take. The refusal has to say which half applies, and the check
568 /// below is that it does: the path wording contains none of this text, so
569 /// a `Source` target falling back to it fails here.
570 #[test]
571 fn a_source_is_told_which_recovery_it_can_reach() {
572 let err = check_status_flags(&flagged(3, 0x05), READ, OpenTarget::Source)
573 .expect_err("a flagged file is refused for a snapshot read");
574 let msg = err.to_string();
575 assert!(
576 msg.starts_with("file is marked in use: a byte source:"),
577 "the refusal does not name the source it refused: {msg}"
578 );
579 assert!(
580 msg.contains("File::from_bytes to read the bytes as they stand"),
581 "the refusal does not name the one recovery a source can reach: {msg}"
582 );
583 assert!(
584 msg.contains("both need a filesystem path"),
585 "the refusal offers path-only recoveries without saying they need a path: {msg}"
586 );
587 }
588
589 /// The mark a page-buffered writer leaves is the write bit alone, which no
590 /// SWMR reader can follow — so the refusal must not send the caller to
591 /// `File::open_swmr`, which refuses the same file in its turn (issue #419).
592 /// What applies instead is the opt-in, named in full so it can be pasted.
593 #[test]
594 fn a_write_only_mark_names_the_snapshot_opt_in() {
595 let err = check_status_flags(
596 &flagged(3, WRITE_ACCESS),
597 READ,
598 OpenTarget::Path(Path::new("buffered.h5")),
599 )
600 .expect_err("a marked file is refused for a snapshot read");
601 let msg = err.to_string();
602 assert!(matches!(err, Error::FileMarkedInUse(_)), "got {err:?}");
603 for part in [
604 "buffered.h5",
605 "0x01",
606 "FileAccessProperties::with_write_mark_policy(WriteMarkPolicy::AllowSnapshot)",
607 "File::sync",
608 "from_bytes",
609 "clear_swmr_flag",
610 ] {
611 assert!(msg.contains(part), "refusal does not mention {part}: {msg}");
612 }
613 assert!(
614 !msg.contains("open_swmr"),
615 "refusal names a reader that refuses this mark too: {msg}"
616 );
617 }
618
619 /// A source refused by the same mark is offered the opt-in, which it can
620 /// take — it is a property of the open, not of the path — and told that the
621 /// recovery it cannot take needs one.
622 #[test]
623 fn a_source_refused_by_a_write_only_mark_is_offered_the_opt_in() {
624 let err = check_status_flags(&flagged(3, WRITE_ACCESS), READ, OpenTarget::Source)
625 .expect_err("a marked file is refused for a snapshot read");
626 let msg = err.to_string();
627 assert!(
628 msg.contains("with_write_mark_policy(WriteMarkPolicy::AllowSnapshot)"),
629 "the refusal does not name the opt-in a source can take: {msg}"
630 );
631 assert!(
632 msg.contains("File::clear_swmr_flag needs a filesystem path"),
633 "the refusal offers a path-only recovery without saying it needs a path: {msg}"
634 );
635 assert!(!msg.contains("open_swmr"), "got {msg}");
636 }
637
638 /// The other half: the pair a SWMR writer leaves keeps the wording that
639 /// names its reader, and does not offer an opt-in that would refuse it.
640 #[test]
641 fn a_swmr_pair_names_its_reader_and_not_the_opt_in() {
642 let err = check_status_flags(
643 &flagged(3, WRITE_ACCESS | SWMR_WRITE_ACCESS),
644 READ,
645 OpenTarget::Path(Path::new("swmr.h5")),
646 )
647 .expect_err("a marked file is refused for a snapshot read");
648 let msg = err.to_string();
649 assert!(msg.contains("File::open_swmr"), "got {msg}");
650 assert!(
651 !msg.contains("write_mark_policy"),
652 "the opt-in does not admit a SWMR pair, so the refusal must not name it: {msg}"
653 );
654 }
655
656 /// What the opt-in admits, stated as the whole rule: the write bit alone,
657 /// and nothing carrying the SWMR bit — that file has a reader of its own.
658 /// The read-write opens cannot reach this at all, since only
659 /// [`OpenIntent::Read`] carries a policy.
660 #[test]
661 fn the_snapshot_opt_in_admits_the_write_mark_alone() {
662 for flags in [0x00, WRITE_ACCESS, SWMR_WRITE_ACCESS, 0x05] {
663 let allowed = flags & SWMR_WRITE_ACCESS == 0;
664 assert_eq!(
665 allows(3, flags, OpenIntent::Read(WriteMarkPolicy::AllowSnapshot)),
666 allowed,
667 "AllowSnapshot on flags {flags:#04x}"
668 );
669 }
670 }
671
672 /// Write a file at `path` whose superblock carries `version` and `flags`,
673 /// re-serializing the superblock so its checksum stays valid.
674 fn write_file_with(path: &Path, version: u8, flags: u32) {
675 let mut bytes = crate::writer::FileBuilder::new().finish().unwrap();
676 let off = crate::signature::find_signature(&bytes).unwrap();
677 let mut sb = Superblock::parse(&bytes, off).unwrap();
678 assert_eq!(sb.version, 3, "this writer emits a v3 superblock");
679 sb.version = version;
680 sb.consistency_flags = flags;
681 let patched = sb.serialize();
682 bytes[off..off + patched.len()].copy_from_slice(&patched);
683 std::fs::write(path, &bytes).unwrap();
684 }
685
686 /// The version gate is checked against a real file, not only against a
687 /// hand-built `Superblock`: a v2 file whose write flag is set still opens
688 /// through `File::open`, which is the C-library parity the gate exists for.
689 /// (v2 and v3 superblocks share a byte layout, so the rewrite above is the
690 /// whole difference between them.)
691 #[test]
692 fn a_flagged_v2_file_still_opens() {
693 let dir = tempfile::tempdir().unwrap();
694 let path = dir.path().join("v2.h5");
695 write_file_with(&path, 2, WRITE_ACCESS | SWMR_WRITE_ACCESS);
696
697 let file = crate::File::open(&path).expect("a v2 file's status flags are not checked");
698 assert_eq!(file.superblock().consistency_flags, 0x05);
699 }
700
701 /// The other half of the version gate: nothing this crate does raises a flag
702 /// on a superblock the gate skips, because the SWMR writer — the only path
703 /// that raises one — refuses a pre-v3 file outright, as the C library does.
704 #[test]
705 fn the_swmr_writer_refuses_a_superblock_the_gate_would_skip() {
706 let dir = tempfile::tempdir().unwrap();
707 let path = dir.path().join("v2.h5");
708 write_file_with(&path, 2, 0);
709
710 let err = crate::File::open_swmr_writer(&path)
711 .expect_err("SWMR writing requires a v3 superblock");
712 assert!(
713 matches!(err, Error::SwmrAppendUnsupported(_)),
714 "got {err:?}"
715 );
716 let bytes = std::fs::read(&path).unwrap();
717 let off = crate::signature::find_signature(&bytes).unwrap();
718 assert_eq!(
719 bytes[off + 11],
720 0,
721 "a refused writer must not have flagged the file on its way out"
722 );
723 }
724
725 /// Half a flag pair is what a *plain* (non-SWMR) C-library writer leaves, and
726 /// a SWMR reader has no protocol for following one, so `File::open_swmr`
727 /// refuses it where it accepts the full pair. Exercised on a real file
728 /// because the mismatch rule is the one branch a caller can reach only
729 /// through a file another library wrote.
730 #[test]
731 fn a_swmr_reader_refuses_write_access_without_the_swmr_bit() {
732 let dir = tempfile::tempdir().unwrap();
733 let path = dir.path().join("half.h5");
734 write_file_with(&path, 3, WRITE_ACCESS);
735
736 let err = crate::File::open_swmr(&path)
737 .expect_err("a SWMR reader needs a SWMR writer, not a plain one");
738 assert!(matches!(err, Error::FileMarkedInUse(_)), "got {err:?}");
739 assert!(
740 crate::File::open(&path).is_err(),
741 "a snapshot read is refused too"
742 );
743
744 write_file_with(&path, 3, WRITE_ACCESS | SWMR_WRITE_ACCESS);
745 crate::File::open_swmr(&path).expect("the full pair is the writer it follows");
746 }
747}