fs_transaction/fs.rs
1//! The filesystem port — the seam every transaction lands through.
2//!
3//! This crate is generic over *where* files live. Rather than depend on any one
4//! concrete backend — `std::fs`, `tokio::fs`, or a browser filesystem like
5//! OPFS/IndexedDB — it asks only for a small async trait that mirrors the slice
6//! of [`std::fs`] a transaction needs. Integrators implement it over whatever
7//! backend they have; [`ChangeSet`](crate::ChangeSet) never learns which one.
8//!
9//! This is the classic *ports and adapters* seam. The traits use native
10//! `async fn` (no boxed futures), so callers keep the backend's real future
11//! types and their `Send`-ness: a backend whose futures are `Send` composes into
12//! multithreaded runtimes unchanged, and one whose futures are not — a
13//! browser backend on a single-threaded executor — is not forced to pretend
14//! otherwise. The method set mirrors [`std::fs`] names exactly, so an adapter is
15//! mechanical to write.
16//!
17//! ## The read/write split
18//!
19//! [`ReadStorage`] is everything that cannot change a byte; [`Storage`] adds the
20//! writes, the mutations, and the durability vocabulary. The split is not
21//! decoration — a consumer generic over `ReadStorage` is a *provably* read-only
22//! consumer, checked by the compiler rather than by review. Only [`Storage`]
23//! can drive a transaction.
24//!
25//! ## Durability is declared, not assumed
26//!
27//! Backends keep very different crash promises: `std::fs` has atomic rename and
28//! `fsync` on every major OS, OPFS has a flush primitive but a weak rename,
29//! IndexedDB has its own multi-object transactions. Rather than assume the
30//! strongest and silently lie on the weakest, a backend *declares* what it can
31//! keep through [`Capabilities`], and the crash-safety machinery adapts. Every
32//! durability member defaults to the pessimistic answer, so an adapter that
33//! forgets to override one degrades to the most defensive path rather than to a
34//! false promise.
35
36use std::io;
37use std::path::{Path, PathBuf};
38use std::sync::Arc;
39use std::time::SystemTime;
40
41pub mod memory;
42
43pub use memory::InMemoryFs;
44
45/// The read half of an async filesystem backend: everything the traversal core
46/// needs, and nothing that can change a byte on disk.
47///
48/// The split from [`Storage`] is not decoration — it is what lets a tree be
49/// depended on by a consumer that must not, and cannot, write: a language
50/// server, a renderer, a browser viewer. A backend that implements only this
51/// is a *provably* read-only view, checked by the compiler rather than by
52/// review.
53///
54/// Each method mirrors the [`std::fs`] function of the same name.
55/// [`try_exists`] has a default in terms of [`metadata`].
56///
57/// [`try_exists`]: ReadStorage::try_exists
58/// [`metadata`]: ReadStorage::metadata
59pub trait ReadStorage {
60 /// Read the entire contents of a file as bytes. Mirrors [`std::fs::read`].
61 fn read(&self, path: &Path) -> impl Future<Output = io::Result<Vec<u8>>>;
62
63 /// Read the entire contents of a file as a string. Mirrors
64 /// [`std::fs::read_to_string`].
65 fn read_to_string(&self, path: &Path) -> impl Future<Output = io::Result<String>>;
66
67 /// Return the entries in a directory (non-recursive). Mirrors
68 /// [`std::fs::read_dir`], but yields a `Vec` since async iterators are not
69 /// yet stable.
70 fn read_dir(&self, path: &Path) -> impl Future<Output = io::Result<Vec<DirEntry>>>;
71
72 /// Return metadata about the entry at `path`. Mirrors
73 /// [`std::fs::metadata`]; follows symlinks.
74 fn metadata(&self, path: &Path) -> impl Future<Output = io::Result<Metadata>>;
75
76 /// Returns `Ok(true)` if the path exists, `Ok(false)` if it does not, and
77 /// `Err(_)` if the check itself failed. Mirrors `std::fs::try_exists`.
78 fn try_exists(&self, path: &Path) -> impl Future<Output = io::Result<bool>> {
79 async move {
80 match self.metadata(path).await {
81 Ok(_) => Ok(true),
82 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
83 Err(e) => Err(e),
84 }
85 }
86 }
87
88 /// Whether the file at `path` can be run — or `None` where that is not a
89 /// thing this backend has.
90 ///
91 /// `None` is the load-bearing answer, and it is *declined*, never guessed:
92 /// a backend that cannot observe the bit — Windows, [`InMemoryFs`], a
93 /// document provider handing over opaque blobs — must not answer `false`,
94 /// because "not executable" and "I do not model this" are different facts,
95 /// and a consumer restoring modes across two machines must be able to tell
96 /// them apart rather than take turns flipping a bit neither can see. The
97 /// default is the decline, so a backend with no opinion is already correct.
98 ///
99 /// Follows symlinks, like [`metadata`](ReadStorage::metadata).
100 fn executable(&self, path: &Path) -> impl Future<Output = io::Result<Option<bool>>> {
101 async move {
102 let _ = path;
103 Ok(None)
104 }
105 }
106
107 /// What the symbolic link at `path` points at, **read rather than
108 /// followed** — or `None` where this backend models no links at all.
109 /// Mirrors [`std::fs::read_link`], with the decline folded in.
110 ///
111 /// `Ok(None)` is reserved for the decline, exactly as
112 /// [`executable`](ReadStorage::executable)'s is: it means *there is no such
113 /// thing here*, it is the default's answer, and an implementation that does
114 /// model links must never give it. Such an implementation answers with the
115 /// target, or with an error where the path holds no link — which is what
116 /// `readlink` already does, and what lets one call settle whether links
117 /// exist at all. Reading the link is what makes recording one safe;
118 /// following it would make the thing at the other end look like a file of
119 /// this tree.
120 fn read_link(&self, path: &Path) -> impl Future<Output = io::Result<Option<PathBuf>>> {
121 async move {
122 let _ = path;
123 Ok(None)
124 }
125 }
126}
127
128/// A borrowed [`ReadStorage`] is itself a [`ReadStorage`] — so an owned backend
129/// can be lent to something generic over `S: ReadStorage` without moving it or
130/// wrapping it in an `Arc` the caller doesn't otherwise need.
131///
132/// Every member is forwarded explicitly. The matching [`Storage`] forwarding
133/// below does the same for the durability members, where leaving any to
134/// inherit the trait's defaults would silently downgrade a real backend's
135/// guarantees the moment it was borrowed.
136impl<S: ReadStorage + ?Sized> ReadStorage for &S {
137 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
138 (**self).read(path).await
139 }
140
141 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
142 (**self).read_to_string(path).await
143 }
144
145 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
146 (**self).read_dir(path).await
147 }
148
149 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
150 (**self).metadata(path).await
151 }
152
153 async fn try_exists(&self, path: &Path) -> io::Result<bool> {
154 (**self).try_exists(path).await
155 }
156
157 async fn executable(&self, path: &Path) -> io::Result<Option<bool>> {
158 (**self).executable(path).await
159 }
160
161 async fn read_link(&self, path: &Path) -> io::Result<Option<PathBuf>> {
162 (**self).read_link(path).await
163 }
164}
165
166/// An `Arc<S>` is itself a [`ReadStorage`] on the same terms as `&S` above — so
167/// a backend shared across several owners (several open handles, a
168/// multi-tab web client) still carries its real capabilities through the
169/// `Arc`, rather than an adapter that forgot to unwrap it silently degrading
170/// to the pessimistic defaults.
171///
172/// `Arc<S>` derefs to `S` exactly like `&S` does, so the same explicit,
173/// every-member forwarding applies for the same reason: the trait's defaults
174/// must never be reached by accident.
175impl<S: ReadStorage + ?Sized> ReadStorage for Arc<S> {
176 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
177 (**self).read(path).await
178 }
179
180 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
181 (**self).read_to_string(path).await
182 }
183
184 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
185 (**self).read_dir(path).await
186 }
187
188 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
189 (**self).metadata(path).await
190 }
191
192 async fn try_exists(&self, path: &Path) -> io::Result<bool> {
193 (**self).try_exists(path).await
194 }
195
196 async fn executable(&self, path: &Path) -> io::Result<Option<bool>> {
197 (**self).executable(path).await
198 }
199
200 async fn read_link(&self, path: &Path) -> io::Result<Option<PathBuf>> {
201 (**self).read_link(path).await
202 }
203}
204
205/// One entry returned by [`ReadStorage::read_dir`].
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct DirEntry {
208 path: PathBuf,
209 file_type: FileType,
210}
211
212impl DirEntry {
213 /// Construct an entry from its path and type.
214 pub fn new(path: impl Into<PathBuf>, file_type: FileType) -> Self {
215 Self {
216 path: path.into(),
217 file_type,
218 }
219 }
220
221 /// The full path to the entry.
222 pub fn path(&self) -> &Path {
223 &self.path
224 }
225
226 /// The final component of the entry's path.
227 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
228 self.path.file_name()
229 }
230
231 /// The entry's type.
232 pub fn file_type(&self) -> FileType {
233 self.file_type
234 }
235}
236
237/// Metadata about a filesystem entry — the subset a transaction needs.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct Metadata {
240 file_type: FileType,
241 len: u64,
242 modified: Option<SystemTime>,
243}
244
245impl Metadata {
246 /// Construct metadata from its parts.
247 pub fn new(file_type: FileType, len: u64, modified: Option<SystemTime>) -> Self {
248 Self {
249 file_type,
250 len,
251 modified,
252 }
253 }
254
255 /// The entry's type.
256 pub fn file_type(&self) -> FileType {
257 self.file_type
258 }
259
260 /// Whether the entry is a regular file.
261 pub fn is_file(&self) -> bool {
262 self.file_type.is_file()
263 }
264
265 /// Whether the entry is a directory.
266 pub fn is_dir(&self) -> bool {
267 self.file_type.is_dir()
268 }
269
270 /// Size in bytes.
271 pub fn len(&self) -> u64 {
272 self.len
273 }
274
275 /// Whether the entry is empty.
276 pub fn is_empty(&self) -> bool {
277 self.len == 0
278 }
279
280 /// Last-modified time, if the backend reports one. Mirrors
281 /// [`std::fs::Metadata::modified`], returning [`io::ErrorKind::Unsupported`]
282 /// when unavailable.
283 pub fn modified(&self) -> io::Result<SystemTime> {
284 self.modified
285 .ok_or_else(|| io::Error::new(io::ErrorKind::Unsupported, "modified time unavailable"))
286 }
287}
288
289/// [`ReadStorage`] over the process filesystem (`std::fs`).
290///
291/// The reference adapter, and the one [`ChangeSet`](crate::ChangeSet) is
292/// tuned for: it implements [`Storage`] too, and reports
293/// [`Capabilities::LOCAL_FS`].
294///
295/// The traits are async so that genuinely async backends (network, OPFS) fit;
296/// this adapter's futures are immediately ready, so any executor — including
297/// the dependency-free [`crate::exec::block_on`] — drives them to completion
298/// in a single poll.
299#[derive(Debug, Clone, Copy, Default)]
300pub struct StdFs;
301
302impl ReadStorage for StdFs {
303 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
304 std::fs::read(path)
305 }
306
307 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
308 std::fs::read_to_string(path)
309 }
310
311 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
312 std::fs::read_dir(path)?
313 .map(|entry| {
314 let entry = entry?;
315 Ok(DirEntry::new(
316 entry.path(),
317 convert_file_type(entry.file_type()?),
318 ))
319 })
320 .collect()
321 }
322
323 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
324 let md = std::fs::metadata(path)?;
325 Ok(Metadata::new(
326 convert_file_type(md.file_type()),
327 md.len(),
328 md.modified().ok(),
329 ))
330 }
331
332 #[cfg(unix)]
333 async fn executable(&self, path: &Path) -> io::Result<Option<bool>> {
334 use std::os::unix::fs::PermissionsExt as _;
335
336 // Any execute bit counts: the question is "can this be run", not
337 // "by whom" — the same one-bit grain `set_executable` writes.
338 let md = std::fs::metadata(path)?;
339 Ok(Some(md.permissions().mode() & 0o111 != 0))
340 }
341
342 // On a platform with no execute bit the trait's default — the decline —
343 // is already the honest answer, so only unix overrides it.
344
345 async fn read_link(&self, path: &Path) -> io::Result<Option<PathBuf>> {
346 // `read_link` reads the link and does not follow it; it errors where
347 // the path holds no link, which is what keeps `Ok(None)` meaning
348 // "this backend has no such thing" — an answer `StdFs` never gives,
349 // on Windows included, where links exist and can be read even though
350 // creating one takes a privilege `set_link` may not have.
351 std::fs::read_link(path).map(Some)
352 }
353}
354
355fn convert_file_type(ft: std::fs::FileType) -> FileType {
356 if ft.is_dir() {
357 FileType::DIR
358 } else if ft.is_file() {
359 FileType::FILE
360 } else {
361 FileType::SYMLINK
362 }
363}
364
365/// The type of a filesystem entry.
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub struct FileType {
368 is_dir: bool,
369 is_file: bool,
370 is_symlink: bool,
371}
372
373impl FileType {
374 /// A regular file.
375 pub const FILE: FileType = FileType {
376 is_dir: false,
377 is_file: true,
378 is_symlink: false,
379 };
380
381 /// A directory.
382 pub const DIR: FileType = FileType {
383 is_dir: true,
384 is_file: false,
385 is_symlink: false,
386 };
387
388 /// A symbolic link.
389 pub const SYMLINK: FileType = FileType {
390 is_dir: false,
391 is_file: false,
392 is_symlink: true,
393 };
394
395 /// Whether this is a regular file.
396 pub fn is_file(&self) -> bool {
397 self.is_file
398 }
399
400 /// Whether this is a directory.
401 pub fn is_dir(&self) -> bool {
402 self.is_dir
403 }
404
405 /// Whether this is a symbolic link.
406 pub fn is_symlink(&self) -> bool {
407 self.is_symlink
408 }
409}
410
411/// An async filesystem backend a transaction can drive — [`ReadStorage`] plus everything
412/// that changes bytes on disk.
413///
414/// Each method mirrors the [`std::fs`] function of the same name. Backends
415/// implement the write/mutate/durability surface here and the read surface on
416/// [`ReadStorage`].
417pub trait Storage: ReadStorage {
418 // ---- write ----
419
420 /// Write a file, replacing it if it already exists. Mirrors
421 /// [`std::fs::write`].
422 fn write(&self, path: &Path, contents: &[u8]) -> impl Future<Output = io::Result<()>>;
423
424 /// Create a file that must not already exist, and write `contents` to it.
425 /// Mirrors [`std::fs::File::create_new`] followed by a full write.
426 ///
427 /// The create and the test-for-existence are **one operation**, and that
428 /// indivisibility is the entire point: of two writers racing to the same
429 /// name, exactly one succeeds and the other is told
430 /// [`AlreadyExists`](io::ErrorKind::AlreadyExists) — with no window between
431 /// a check and a create for either to slip a half-written file through.
432 /// `AlreadyExists` is therefore a *load-bearing answer*, not a failure to
433 /// smooth over: a write-once consumer branches on it (typically by reading
434 /// back what is there and confirming it is what it meant to write), so an
435 /// implementation must report that kind and no other for an occupied path.
436 ///
437 /// Two things this deliberately does not do, both the caller's to ask for:
438 /// parents are not created ([`create_dir_all`](Storage::create_dir_all)
439 /// first, as [`std::fs::File::create_new`] would demand), and nothing is
440 /// flushed — a caller that needs the new file to survive a crash pairs
441 /// this with [`sync`](Storage::sync), which is what lets it choose the
442 /// *weakest* durability that is correct where a built-in flush would
443 /// impose the strongest everywhere.
444 ///
445 /// The default refuses with [`Unsupported`](io::ErrorKind::Unsupported),
446 /// and [`Capabilities::exclusive_create`] defaults to `false` to match: a
447 /// backend that can keep the exclusivity promise declares it and overrides
448 /// this, and one that cannot must not paper over the difference with a
449 /// check-then-write — the window in that emulation is exactly what a
450 /// caller reaching for this method cannot tolerate.
451 fn create_new(&self, path: &Path, contents: &[u8]) -> impl Future<Output = io::Result<()>> {
452 async move {
453 let _ = (path, contents);
454 Err(io::Error::new(
455 io::ErrorKind::Unsupported,
456 "this backend does not support exclusive create",
457 ))
458 }
459 }
460
461 /// Create a directory and all missing parents. Mirrors
462 /// [`std::fs::create_dir_all`].
463 fn create_dir_all(&self, path: &Path) -> impl Future<Output = io::Result<()>>;
464
465 // ---- mutate ----
466
467 /// Remove a regular file. Mirrors [`std::fs::remove_file`].
468 fn remove_file(&self, path: &Path) -> impl Future<Output = io::Result<()>>;
469
470 /// Recursively remove a directory and its contents. Mirrors
471 /// [`std::fs::remove_dir_all`].
472 fn remove_dir_all(&self, path: &Path) -> impl Future<Output = io::Result<()>>;
473
474 /// Rename or move a file or directory. Mirrors [`std::fs::rename`] — and
475 /// the load-bearing half of the mirror is that an occupied destination
476 /// *file* is replaced, as `std::fs::rename` replaces one on every platform
477 /// this crate targets. The default [`write_atomic`](Storage::write_atomic)
478 /// publishes by renaming a staged sibling over the target, so a backend
479 /// whose rename refuses an occupied file cannot take that default and must
480 /// override `write_atomic` with its own atomic replacement. A *directory*
481 /// destination is another matter — `std::fs::rename` itself is
482 /// platform-divergent there — and nothing in this crate renames onto one.
483 fn rename(&self, from: &Path, to: &Path) -> impl Future<Output = io::Result<()>>;
484
485 /// Give `to` the same access permissions `from` has. A `from` that does not
486 /// exist is not an error — there is no prior state to carry over, so the
487 /// call has nothing to do.
488 ///
489 /// This exists for [`write_atomic`](Storage::write_atomic), which publishes
490 /// its bytes by renaming a freshly-created sibling over the target. A new
491 /// file is born with the backend's default permissions, and a rename carries
492 /// those onto the name it replaces — so without this step, replacing a
493 /// document the user had deliberately restricted (`chmod 600` on a private
494 /// journal entry) silently widens it to whatever the umask allows. A
495 /// content replacement must not be a permission change.
496 ///
497 /// What this does *not* close is the window before it: the sibling holds the
498 /// new contents under default permissions from the moment it is written
499 /// until this call narrows it. Shutting that window means creating the file
500 /// with the final mode already on it, which is not something
501 /// [`write`](Storage::write) — a `std::fs::write` mirror — can express. The
502 /// sibling lives in the target's own directory throughout, so whatever gates
503 /// access to the document gates access to it too.
504 ///
505 /// The default is a no-op, which is the *correct* behavior for a backend
506 /// with no permission model at all — [`InMemoryFs`], OPFS, IndexedDB. There
507 /// is nothing there to preserve, and nothing is lost by not preserving it.
508 fn copy_permissions(&self, from: &Path, to: &Path) -> impl Future<Output = io::Result<()>> {
509 async move {
510 let _ = (from, to);
511 Ok(())
512 }
513 }
514
515 /// Make the file at `path` runnable, or not. One bit, not a mode: nothing
516 /// else about the file changes.
517 ///
518 /// The write half of [`executable`](ReadStorage::executable), and the
519 /// default follows from its decline: a backend whose `executable` answers
520 /// `None` has nothing to set, so doing nothing *is* the honest
521 /// implementation — the same reasoning as
522 /// [`copy_permissions`](Storage::copy_permissions), where a backend with no
523 /// permission model loses nothing by not preserving one. A backend that
524 /// does model the bit overrides both members together; answering one
525 /// without the other would let a change set read a bit it cannot restore,
526 /// or restore one it cannot read.
527 fn set_executable(
528 &self,
529 path: &Path,
530 executable: bool,
531 ) -> impl Future<Output = io::Result<()>> {
532 async move {
533 let _ = (path, executable);
534 Ok(())
535 }
536 }
537
538 /// Place a symbolic link at `path` pointing at `target`, replacing whatever
539 /// is there.
540 ///
541 /// The write half of [`read_link`](ReadStorage::read_link). Nothing here
542 /// opens or resolves `target`: a link may point outside the tree, at
543 /// nothing, or at itself, and the only consequence is an honest symlink
544 /// pointing where symlinks are allowed to point. The replacement addresses
545 /// the entry itself, never the entry's referent — a link at `path` is
546 /// removed and remade rather than written through, or a plain file there
547 /// gives way to the link.
548 ///
549 /// The default *refuses* with [`Unsupported`](io::ErrorKind::Unsupported),
550 /// where [`set_executable`](Storage::set_executable)'s default no-ops —
551 /// the asymmetry is deliberate. An execute bit not modeled costs nothing to
552 /// leave unset; a link not modeled has no honest substitute, because a
553 /// plain file holding the target's text would invent content nothing asked
554 /// to write. A backend with no links must say so, and the caller decides
555 /// what its absence means.
556 fn set_link(&self, path: &Path, target: &Path) -> impl Future<Output = io::Result<()>> {
557 async move {
558 let _ = (path, target);
559 Err(io::Error::new(
560 io::ErrorKind::Unsupported,
561 "this backend does not model symbolic links",
562 ))
563 }
564 }
565
566 // ---- durability ----
567 //
568 // This crate spans backends with very different crash guarantees — `std::fs`
569 // (atomic rename and fsync on every major OS), OPFS (a flush primitive but a
570 // weak rename), IndexedDB (its own multi-object transactions). Rather than
571 // assume the strongest of these and silently lie on the weakest, the crash-
572 // safety machinery *asks* what a backend can promise and adapts. These three
573 // members are defaulted to the pessimistic answer, so a backend gains a
574 // guarantee only by explicitly claiming it.
575
576 /// What durability guarantees this backend can make. Defaults to
577 /// [`Capabilities::NONE`] — a backend promises a guarantee only by saying so,
578 /// so an adapter that forgets to override this degrades to the most defensive
579 /// path rather than to a false promise.
580 fn capabilities(&self) -> Capabilities {
581 Capabilities::NONE
582 }
583
584 /// Flush `path` — and nothing else — to the strength `need` asks for.
585 ///
586 /// `path` names *one* object, and only that object is flushed. To make a
587 /// directory entry durable (the naming half of a create or a rename), sync
588 /// the directory itself: on a POSIX filesystem a directory is a thing that
589 /// can be opened and fsynced, and this crate's own
590 /// [`write_atomic`](Storage::write_atomic) does exactly that after its
591 /// rename. Folding the parent into every call instead would flush twice as
592 /// much as any single step needs, and would leave the caller unable to say
593 /// which of the two it actually meant.
594 ///
595 /// `need` is the *weakest* guarantee that is still correct at the call site,
596 /// not a wish. [`Durability::Ordered`] asks only that everything written to
597 /// `path` before this call land before anything written after it — enough to
598 /// stop a rename overtaking the bytes it publishes, and on some platforms far
599 /// cheaper than the real thing. [`Durability::Durable`] asks that the bytes
600 /// survive power loss. A backend may always answer with something stronger
601 /// than it was asked for; it may never answer with something weaker.
602 ///
603 /// The default is a no-op, which is the *correct* behavior for any backend
604 /// whose [`capabilities`](Storage::capabilities) report
605 /// [`SyncGuarantee::None`]: it cannot make the promise, so it must not
606 /// pretend to. A backend that can flush must both override this and report
607 /// the strongest request it genuinely honors — the two always travel
608 /// together, and [`SyncGuarantee::satisfies`] is how a caller asks.
609 fn sync(&self, path: &Path, need: Durability) -> impl Future<Output = io::Result<()>> {
610 async move {
611 let _ = (path, need);
612 Ok(())
613 }
614 }
615
616 /// Replace `path`'s contents with `contents` atomically — and *only*
617 /// atomically: no observer ever sees a splice of old and new bytes, but
618 /// nothing here outlives a power cut until the caller flushes it. The
619 /// atomic half of [`write_atomic`](Storage::write_atomic), split out so
620 /// that a protocol landing many replacements can batch one drain instead
621 /// of paying one per file — which is exactly what
622 /// [`ChangeSet`](crate::ChangeSet) and
623 /// [`OrderedBatch`](crate::OrderedBatch) do, settling the whole set's
624 /// flush debt once before the state is certified.
625 ///
626 /// The default composes the primitives into the staging protocol, whenever
627 /// [`capabilities`](Storage::capabilities) report `atomic_replace`:
628 ///
629 /// 1. write the bytes to a temporary sibling;
630 /// 2. [`sync`](Storage::sync) that sibling [`Ordered`](Durability::Ordered),
631 /// so the rename cannot be reordered ahead of the bytes it publishes;
632 /// 3. [`copy_permissions`](Storage::copy_permissions) from the target onto
633 /// that sibling, so the replacement carries the target's access
634 /// permissions rather than a fresh file's defaults;
635 /// 4. [`rename`](Storage::rename) it over the target — *this* is the atomic
636 /// instant.
637 ///
638 /// Steps 2 and 3 are in that order because a backend may implement `sync` by
639 /// opening the path, and a mode faithfully copied from the target can be one
640 /// that forbids opening it to read — `0o200` is replaceable but not readable.
641 /// The cost is that the mode change lands after the flush and so is not
642 /// itself durable: a crash in that window can leave the new contents under
643 /// the *default* permissions. That is precisely the outcome every write had
644 /// before step 3 existed, so the window is a smaller bad case, never a new
645 /// one.
646 ///
647 /// One flush, and it is a barrier, not a promise of survival: what it
648 /// rules out is the rename being seen before the bytes it publishes. The
649 /// bytes are never flushed under their final name, because a rename does
650 /// not move an inode — the file the target now names is the very one step
651 /// 2 flushed. The sibling's own directory entry is never flushed either,
652 /// because nobody is owed a temporary that survives a crash. What is owed
653 /// afterwards — the parent directory's entry, and durability itself — is
654 /// the caller's to settle, per file
655 /// ([`write_atomic`](Storage::write_atomic)) or batched.
656 ///
657 /// A backend that cannot rename atomically falls back to a plain,
658 /// unflushed write, which is *not* crash-atomic; the caller was told by
659 /// `capabilities`, and still owns every flush. A backend whose atomic
660 /// replacement is native — a locked in-memory swap, a transactional
661 /// store — overrides *this* method, and
662 /// [`write_atomic`](Storage::write_atomic)'s default composes on top of
663 /// the override. An override claiming `atomic_replace` inherits step 2's
664 /// obligation along with the method: its bytes must be ordered ahead of
665 /// whatever publishes them before the call returns, because both
666 /// `write_atomic`'s composed default and the batched protocols add only
667 /// directory flushes afterwards, never a second look at the bytes.
668 ///
669 /// The temporary is removed on any failure, so a torn attempt leaves the
670 /// target exactly as it was and no litter behind. It is a dotted sibling in
671 /// the target's own directory, so the follow-up rename stays within one
672 /// filesystem (a cross-device rename is neither atomic nor, often, even
673 /// permitted).
674 fn replace(&self, path: &Path, contents: &[u8]) -> impl Future<Output = io::Result<()>> {
675 async move {
676 if !self.capabilities().atomic_replace {
677 // No atomic rename to lean on: the honest best effort is a
678 // plain write, and the caller — told so by `capabilities` —
679 // owns whatever flushing it needs. A documented degrade, not
680 // a lie.
681 return self.write(path, contents).await;
682 }
683 let tmp = temp_sibling(path);
684 // Any failure past this point must not leave the staging file behind,
685 // and must never have touched the target — hence the whole dance
686 // happens on `tmp` and only the rename names `path`.
687 let staged = async {
688 self.write(&tmp, contents).await?;
689 self.sync(&tmp, Durability::Ordered).await?;
690 // The sibling was just created, so it carries default
691 // permissions rather than the target's. Carry the target's over
692 // before the rename publishes them — a replacement changes
693 // contents, never who may read them. A target that does not
694 // exist yet has nothing to carry, and this is a no-op.
695 self.copy_permissions(path, &tmp).await?;
696 self.rename(&tmp, path).await
697 }
698 .await;
699 match staged {
700 Ok(()) => Ok(()),
701 Err(e) => {
702 // Best-effort cleanup: if even this fails the target is still
703 // untouched, so the atomicity promise holds regardless — the
704 // worst case is one stray dotfile, not a torn document.
705 let _ = self.remove_file(&tmp).await;
706 Err(e)
707 }
708 }
709 }
710 }
711
712 /// Replace `path`'s contents with `contents` atomically and durably: no
713 /// observer — concurrent reader or post-crash survivor — ever sees a splice
714 /// of old and new bytes, and once this returns the new contents outlive a
715 /// power loss.
716 ///
717 /// [`replace`](Storage::replace) plus the flushes it leaves to its caller,
718 /// composed through `replace` itself so a backend's override carries: the
719 /// replacement lands, the parent directory is flushed
720 /// [`Durable`](Durability::Durable) to carry the rename through a power
721 /// cut, and on a backend without `atomic_replace` the plainly-written
722 /// bytes are flushed durable too, since there was no rename to fold their
723 /// naming into. The right call for a standalone save; a protocol landing
724 /// many files reaches for `replace` and batches the flushes instead.
725 ///
726 /// A backend that overrode this method wholesale under the old guidance
727 /// should move that override to [`replace`](Storage::replace): the
728 /// crate's own protocols now reach for `replace` directly, and an
729 /// override living only here is bypassed by every one of them.
730 fn write_atomic(&self, path: &Path, contents: &[u8]) -> impl Future<Output = io::Result<()>> {
731 async move {
732 self.replace(path, contents).await?;
733 if !self.capabilities().atomic_replace {
734 self.sync(path, Durability::Durable).await?;
735 }
736 match parent_dir(path) {
737 Some(dir) => self.sync(dir, Durability::Durable).await,
738 // A bare relative filename, whose directory is the process's
739 // current one — not a path this crate holds, nor one it owns.
740 None => Ok(()),
741 }
742 }
743 }
744}
745
746impl<S: Storage + ?Sized> Storage for &S {
747 async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
748 (**self).write(path, contents).await
749 }
750
751 async fn create_new(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
752 (**self).create_new(path, contents).await
753 }
754
755 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
756 (**self).create_dir_all(path).await
757 }
758
759 async fn remove_file(&self, path: &Path) -> io::Result<()> {
760 (**self).remove_file(path).await
761 }
762
763 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
764 (**self).remove_dir_all(path).await
765 }
766
767 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
768 (**self).rename(from, to).await
769 }
770
771 async fn copy_permissions(&self, from: &Path, to: &Path) -> io::Result<()> {
772 (**self).copy_permissions(from, to).await
773 }
774
775 async fn set_executable(&self, path: &Path, executable: bool) -> io::Result<()> {
776 (**self).set_executable(path, executable).await
777 }
778
779 async fn set_link(&self, path: &Path, target: &Path) -> io::Result<()> {
780 (**self).set_link(path, target).await
781 }
782
783 fn capabilities(&self) -> Capabilities {
784 (**self).capabilities()
785 }
786
787 async fn sync(&self, path: &Path, need: Durability) -> io::Result<()> {
788 (**self).sync(path, need).await
789 }
790
791 async fn replace(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
792 (**self).replace(path, contents).await
793 }
794
795 async fn write_atomic(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
796 (**self).write_atomic(path, contents).await
797 }
798}
799
800impl<S: Storage + ?Sized> Storage for Arc<S> {
801 async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
802 (**self).write(path, contents).await
803 }
804
805 async fn create_new(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
806 (**self).create_new(path, contents).await
807 }
808
809 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
810 (**self).create_dir_all(path).await
811 }
812
813 async fn remove_file(&self, path: &Path) -> io::Result<()> {
814 (**self).remove_file(path).await
815 }
816
817 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
818 (**self).remove_dir_all(path).await
819 }
820
821 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
822 (**self).rename(from, to).await
823 }
824
825 async fn copy_permissions(&self, from: &Path, to: &Path) -> io::Result<()> {
826 (**self).copy_permissions(from, to).await
827 }
828
829 async fn set_executable(&self, path: &Path, executable: bool) -> io::Result<()> {
830 (**self).set_executable(path, executable).await
831 }
832
833 async fn set_link(&self, path: &Path, target: &Path) -> io::Result<()> {
834 (**self).set_link(path, target).await
835 }
836
837 fn capabilities(&self) -> Capabilities {
838 (**self).capabilities()
839 }
840
841 async fn sync(&self, path: &Path, need: Durability) -> io::Result<()> {
842 (**self).sync(path, need).await
843 }
844
845 async fn replace(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
846 (**self).replace(path, contents).await
847 }
848
849 async fn write_atomic(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
850 (**self).write_atomic(path, contents).await
851 }
852}
853
854/// The durability guarantees a [`Storage`] backend can make — declared by the
855/// backend through [`Storage::capabilities`], honored by the crash-safety
856/// machinery in [`ChangeSet`](crate::ChangeSet).
857///
858/// The point of naming these explicitly is that a transaction must run correctly
859/// over backends that keep very different promises. Rather than assume a
860/// guarantee and corrupt data on the backend that cannot keep it, the apply path
861/// reads the capabilities and picks the strongest *protocol the backend actually
862/// supports*:
863/// a filesystem gets atomic-rename writes and a journal; a transactional store is
864/// handed the whole change set to commit itself; a backend that can promise
865/// neither still works, it simply cannot claim a write survives a crash.
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
867pub struct Capabilities {
868 /// The backend can replace an existing file's contents in one indivisible
869 /// step, so no crash exposes a half-written file — an observer sees the whole
870 /// old contents or the whole new. On a filesystem this is realized by
871 /// [`Storage::write_atomic`]'s write-temp-then-`rename`; a backend may
872 /// instead be atomic by nature.
873 pub atomic_replace: bool,
874
875 /// The backend can create a file *only if nothing is at its path yet*, as
876 /// one operation — [`Storage::create_new`]. The create and the
877 /// test-for-existence cannot be split: two writers racing to the same name
878 /// see exactly one succeed and the other told
879 /// [`AlreadyExists`](io::ErrorKind::AlreadyExists), with no window between
880 /// the test and the create for a half-written file to slip through. This is
881 /// the primitive an append-only, write-once consumer builds its whole
882 /// concurrency story on, which is why it is declared rather than emulated:
883 /// a check-then-write emulation has the window in it, and a backend that
884 /// cannot close the window must say so instead of pretending.
885 pub exclusive_create: bool,
886
887 /// How strong the backend's [`Storage::sync`] is: whether it can flush at
888 /// all, and if so whether a flush merely orders writes or carries them
889 /// through a power cut. `fsync` on `std::fs`, `FileSystemSyncAccessHandle
890 /// .flush()` on OPFS, the implicit durability of a committed IndexedDB
891 /// transaction.
892 pub sync_guarantee: SyncGuarantee,
893
894 /// The backend commits changes to *many* objects as one indivisible unit, so
895 /// this crate's write-ahead journal would be redundant and a caller should
896 /// defer to the backend instead. True for IndexedDB; false for a plain
897 /// filesystem, where multi-file atomicity is the journal's job to provide.
898 pub native_transactions: bool,
899}
900
901impl Capabilities {
902 /// Promises nothing — the safe assumption for an unknown backend, and the
903 /// [`Storage::capabilities`] default. Every field is the pessimistic value,
904 /// so code that checks a capability before relying on it takes the most
905 /// defensive branch unless a backend has explicitly earned a lighter one.
906 pub const NONE: Self = Self {
907 atomic_replace: false,
908 exclusive_create: false,
909 sync_guarantee: SyncGuarantee::None,
910 native_transactions: false,
911 };
912
913 /// A conventional local filesystem: atomic replacement by rename, exclusive
914 /// create (`O_CREAT|O_EXCL`, honored by every OS this crate targets), and
915 /// durable fsync — but no native multi-object transaction (that is the
916 /// journal's job). What [`StdFs`] reports on every platform this crate
917 /// targets.
918 pub const LOCAL_FS: Self = Self {
919 atomic_replace: true,
920 exclusive_create: true,
921 sync_guarantee: SyncGuarantee::Durable,
922 native_transactions: false,
923 };
924
925 /// An in-process, memory-only store ([`InMemoryFs`]): every mutation takes
926 /// the backend's single lock for its whole duration, so one write already
927 /// swaps old bytes for new as one indivisible step — no separate
928 /// temp-then-rename dance is needed for `atomic_replace` to be true. But
929 /// nothing here is backed by anything other than process memory, so its
930 /// `sync_guarantee` is [`SyncGuarantee::None`]: there is nothing to flush,
931 /// and the entire store evaporates the instant the process exits — it cannot
932 /// even promise ordering against a crash it will not survive.
933 /// `native_transactions`
934 /// is false too — the lock makes each *single* call atomic, not a batch of
935 /// several calls committed together, so a multi-file change set still
936 /// needs the write-ahead journal over this backend exactly as it would over
937 /// a real filesystem. `exclusive_create` is true on the same grounds as
938 /// `atomic_replace`: one locked call checks and inserts without anything
939 /// interleaving.
940 pub const IN_MEMORY: Self = Self {
941 atomic_replace: true,
942 exclusive_create: true,
943 sync_guarantee: SyncGuarantee::None,
944 native_transactions: false,
945 };
946}
947
948/// What a caller needs from one [`Storage::sync`] call — the *weakest* guarantee
949/// that is still correct at that point, so that a backend able to serve it
950/// cheaply is free to.
951#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
952pub enum Durability {
953 /// A barrier: everything this backend was asked to write before this call —
954 /// to the named path *or to any other* — must land before anything written
955 /// after it. It says nothing about *when*: a crash may still lose the lot,
956 /// only never a suffix without its prefix.
957 ///
958 /// The barrier is backend-wide on purpose, not scoped to the one path
959 /// named. [`Storage::write_atomic`] only needs the narrow reading — the
960 /// rename must not be seen before the bytes it publishes — but
961 /// [`crate::ordered`] builds on the wide one: a batch whose second tier
962 /// must never be seen without its first is ordering writes to *different*
963 /// files against each other, and a "barrier" that only ordered a file
964 /// against itself could not say that. Both `fsync` (which completes the
965 /// named writes outright) and Apple's `F_BARRIERFSYNC` (a queue barrier
966 /// the whole device honors) keep the wide promise; a primitive that
967 /// orders only one file's own writes — `sync_file_range` and its kin —
968 /// does not, and a backend with nothing stronger must declare
969 /// [`SyncGuarantee::None`] rather than a barrier it cannot keep. On Apple
970 /// platforms the distinction from [`Durable`](Durability::Durable) is the
971 /// difference between a barrier and draining the drive's write cache.
972 ///
973 /// The wide promise has a corollary the journal protocols lean on: once
974 /// any *later* write is durably on disk, everything ordered before it is
975 /// too — a barrier followed by one durable flush of what that later write
976 /// mutated makes the whole prefix durable, without flushing it piece by
977 /// piece.
978 Ordered,
979 /// Once the call returns, the bytes survive power loss.
980 ///
981 /// And not the named path's bytes alone: on a backend whose
982 /// [`Ordered`](Durability::Ordered) answers are true barriers rather than
983 /// flushes, a `Durable` answer is contractually a **drain** — everything
984 /// the backend accepted and barriered before this call lands durably with
985 /// it. This is the other half of the barrier's bargain, and it is not
986 /// derivable from ordering alone: a batch that barriers ten paths and
987 /// drains an eleventh has issued no write *after* the barriers for pure
988 /// ordering to hang the ten on. Both primitives this crate ships keep the
989 /// pair honestly — plain `fsync` because every "barrier" was a full flush
990 /// to begin with, `F_FULLFSYNC` because it drains the device's whole
991 /// cache — and a backend that can only drain the named object must answer
992 /// `Ordered` with a flush rather than a barrier, or declare
993 /// [`SyncGuarantee::None`]. The batched-flush protocols in this crate
994 /// (barriers capped by one drain) are licensed by this pairing.
995 Durable,
996}
997
998/// How strong a backend's [`Storage::sync`] actually is — the standing answer to
999/// a [`Durability`] request, declared once in [`Capabilities`] rather than
1000/// discovered per call.
1001///
1002/// Deliberately three-valued rather than the "can this backend flush?" boolean
1003/// it replaces, because that question has a common and useful middle answer it
1004/// could not express: a backend that orders writes against each other without
1005/// paying for a device-wide cache drain. Offered only `true` and `false`, such a
1006/// backend has to either overstate — claiming a durability it does not deliver —
1007/// or understate, claiming it cannot flush at all when ordering is precisely
1008/// what [`Storage::write_atomic`] asks it for.
1009#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1010pub enum SyncGuarantee {
1011 /// `sync` does nothing: an in-memory store, or a port with no flush
1012 /// primitive under it to call.
1013 None,
1014 /// `sync` orders writes against each other, but does not promise any of them
1015 /// outlives a power cut.
1016 Ordered,
1017 /// `sync` flushes through to durable storage.
1018 Durable,
1019}
1020
1021impl SyncGuarantee {
1022 /// Whether a backend making this guarantee can honor `need`.
1023 pub const fn satisfies(self, need: Durability) -> bool {
1024 match need {
1025 Durability::Ordered => !matches!(self, SyncGuarantee::None),
1026 Durability::Durable => matches!(self, SyncGuarantee::Durable),
1027 }
1028 }
1029}
1030
1031/// Create `dir` and every missing parent, returning the directories whose
1032/// entry set the creation changed: each directory made, plus the deepest
1033/// pre-existing ancestor, which received the topmost new name. Empty when
1034/// `dir` already existed.
1035///
1036/// The return value is the flush list a caller owes. A directory entry is its
1037/// own write, persisting separately from anything under it — so a
1038/// `create_dir_all` nobody flushes is a chain of names a power cut can take
1039/// back, leaving a durably-flushed file inside a directory that no longer
1040/// exists. [`crate::ordered`] flushes the list with each tier and the journal
1041/// flushes it before committing intent into a fresh
1042/// [home](crate::Journal::kept_in); the walk costs one existence probe per
1043/// ancestor, and nothing at all past the first one that already exists.
1044pub(crate) async fn create_dir_all_traced<FS: Storage>(
1045 fs: &FS,
1046 dir: &Path,
1047) -> io::Result<Vec<PathBuf>> {
1048 let mut changed = Vec::new();
1049 let mut cur = Some(dir);
1050 while let Some(d) = cur {
1051 if fs.try_exists(d).await? {
1052 // The deepest ancestor that already exists gains the topmost new
1053 // entry — but only if anything is being created at all.
1054 if !changed.is_empty() {
1055 changed.push(d.to_path_buf());
1056 }
1057 break;
1058 }
1059 changed.push(d.to_path_buf());
1060 cur = parent_dir(d);
1061 }
1062 if changed.is_empty() {
1063 return Ok(changed);
1064 }
1065 fs.create_dir_all(dir).await?;
1066 Ok(changed)
1067}
1068
1069/// Make every path in `paths` durable at the price of one drain: a barrier on
1070/// each, then one [`Durable`](Durability::Durable) flush of `anchor`.
1071///
1072/// This is the barrier-and-drain pairing [`Durability`] documents, cashed in:
1073/// on a backend whose `Ordered` answers are full flushes, every barrier here
1074/// already made its path durable and the cap adds nothing; on a backend whose
1075/// `Ordered` is a true barrier, the `Durable` answer is contractually a drain
1076/// that carries everything previously barriered with it. Either way the list
1077/// is durable for one drain instead of one per path.
1078///
1079/// `anchor` — not the last debt — takes the drain, and it must be a path that
1080/// **exists** and lives on the same filesystem as the debts: a tree's root, a
1081/// journal's home. A debt can be a name a later operation moved or removed
1082/// (a flipped file since renamed, a directory on a platform that declines
1083/// directory syncs), and [`Storage::sync`] treats a missing path as a
1084/// successful no-op — so a cap issued at whichever debt sorts last can
1085/// silently issue *nothing*, and the entire batch's durability with it. The
1086/// barriers tolerate that (a moved name's inode was barriered while the name
1087/// was live, or is covered elsewhere); the one drain must not. A path equal
1088/// to `anchor` is skipped in the barrier pass, since the cap covers it, and
1089/// an empty `paths` owes nothing at all — the anchor is not flushed for its
1090/// own sake.
1091pub(crate) async fn flush_all_durable<FS: Storage>(
1092 fs: &FS,
1093 paths: impl IntoIterator<Item = PathBuf>,
1094 anchor: &Path,
1095) -> io::Result<()> {
1096 let mut owed = false;
1097 for path in paths {
1098 owed = true;
1099 if path != anchor {
1100 fs.sync(&path, Durability::Ordered).await?;
1101 }
1102 }
1103 if owed {
1104 fs.sync(anchor, Durability::Durable).await?;
1105 }
1106 Ok(())
1107}
1108
1109/// The directory holding `path`, when there is one to name. `Path::parent`
1110/// answers `Some("")` for a bare relative filename like `index.md` — the
1111/// process's current directory, which this crate neither holds a path to nor owns —
1112/// and that empty path is not something a backend can open, so it is folded in
1113/// with "no parent" here rather than at each call site.
1114pub(crate) fn parent_dir(path: &Path) -> Option<&Path> {
1115 path.parent().filter(|p| !p.as_os_str().is_empty())
1116}
1117
1118/// The temporary sibling [`Storage::write_atomic`]'s default protocol stages a
1119/// write through before renaming it into place. A dotted, suffixed name in the
1120/// target's own directory: dotted and suffixed so it will not collide with a
1121/// real file, and a *sibling* so the rename that follows never crosses a
1122/// filesystem boundary.
1123pub(crate) fn temp_sibling(path: &Path) -> PathBuf {
1124 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
1125 path.with_file_name(format!(".{name}.fstx-tmp"))
1126}
1127
1128impl Storage for StdFs {
1129 async fn write(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
1130 std::fs::write(path, contents)
1131 }
1132
1133 async fn create_new(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
1134 use std::io::Write as _;
1135
1136 // `O_CREAT|O_EXCL` (`CREATE_NEW` on Windows) — the one place the OS
1137 // itself promises the create and the existence test are indivisible.
1138 let mut file = std::fs::File::create_new(path)?;
1139 file.write_all(contents)
1140 }
1141
1142 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1143 std::fs::create_dir_all(path)
1144 }
1145
1146 async fn remove_file(&self, path: &Path) -> io::Result<()> {
1147 std::fs::remove_file(path)
1148 }
1149
1150 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
1151 std::fs::remove_dir_all(path)
1152 }
1153
1154 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1155 std::fs::rename(from, to)
1156 }
1157
1158 #[cfg(unix)]
1159 async fn set_executable(&self, path: &Path, executable: bool) -> io::Result<()> {
1160 use std::os::unix::fs::PermissionsExt as _;
1161
1162 let mut permissions = std::fs::metadata(path)?.permissions();
1163 let held = permissions.mode();
1164 // Every bit the owner's own umask chose stays theirs. This carries one
1165 // bit, so it sets one bit: the execute bits follow the read bits, so a
1166 // file readable by its group becomes runnable by its group and a
1167 // private file stays private.
1168 let mode = if executable {
1169 held | ((held & 0o444) >> 2)
1170 } else {
1171 held & !0o111
1172 };
1173 if mode == held {
1174 return Ok(());
1175 }
1176 permissions.set_mode(mode);
1177 std::fs::set_permissions(path, permissions)
1178 }
1179
1180 // On a platform with no execute bit, `set_executable`'s default — doing
1181 // nothing — is already correct, so only unix overrides it. `set_link` is
1182 // the other way round: Windows has symbolic links and does not hand them
1183 // out (creating one wants a privilege an ordinary account lacks), so the
1184 // default's refusal is the honest answer there and only unix overrides.
1185
1186 #[cfg(unix)]
1187 async fn set_link(&self, path: &Path, target: &Path) -> io::Result<()> {
1188 // Made at a temporary sibling and renamed over the target, the same
1189 // shape as `write_atomic` and for the same reason: remove-then-symlink
1190 // has a window in which `path` names nothing, and a crash in it loses
1191 // the file that was there without leaving the link that was promised.
1192 // The rename is the atomic instant; a failure before it leaves the
1193 // target exactly as it was, plus at worst one stray dotfile.
1194 let tmp = temp_sibling(path);
1195 let _ = std::fs::remove_file(&tmp);
1196 std::os::unix::fs::symlink(target, &tmp)?;
1197 match std::fs::rename(&tmp, path) {
1198 Ok(()) => Ok(()),
1199 Err(e) => {
1200 let _ = std::fs::remove_file(&tmp);
1201 Err(e)
1202 }
1203 }
1204 }
1205
1206 async fn copy_permissions(&self, from: &Path, to: &Path) -> io::Result<()> {
1207 let perms = match std::fs::metadata(from) {
1208 Ok(meta) => meta.permissions(),
1209 // Nothing to carry over: `write_atomic` is creating `from` rather
1210 // than replacing it, so the new file's default permissions are the
1211 // right ones and there is no prior state to lose.
1212 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1213 Err(e) => return Err(e),
1214 };
1215 // Deliberately best-effort. On a filesystem with no permission model —
1216 // exFAT or FAT32 on a USB stick, some FUSE mounts — `chmod` refuses
1217 // outright, but every file there already reports the same mount-wide
1218 // mode, so there was never a permission to preserve and failing the
1219 // whole document write over it would be absurd. Where modes *are* real,
1220 // this is a chmod on a file this process created moments ago and owns,
1221 // which does not fail for any reason a caller could act on.
1222 let _ = std::fs::set_permissions(to, perms);
1223 Ok(())
1224 }
1225
1226 fn capabilities(&self) -> Capabilities {
1227 // Every OS this crate targets gives an atomic same-filesystem rename and an
1228 // fsync. `std::fs::rename` replaces the destination on all of them —
1229 // POSIX by definition, Windows via `MoveFileEx(MOVEFILE_REPLACE_EXISTING)`
1230 // — so the write-temp-then-rename protocol in the default `write_atomic`
1231 // is genuinely atomic here.
1232 Capabilities::LOCAL_FS
1233 }
1234
1235 async fn sync(&self, path: &Path, need: Durability) -> io::Result<()> {
1236 // `sync_all` is the only flush in the standard library, and it is the
1237 // strong one — on Apple platforms it is `F_FULLFSYNC`, a drain of the
1238 // drive's whole write cache. By default both requests are answered
1239 // with it: stronger than `Ordered` asked for, which a backend is
1240 // always allowed to be. The `barrier-fsync` feature is the cheaper
1241 // answer where one exists — `F_BARRIERFSYNC` on Apple, a queue
1242 // barrier the device honors — and `sync_file` below is where the
1243 // request-by-request choice lives.
1244 sync_path(path, need)
1245 }
1246}
1247
1248/// Flush exactly `path` — file or directory — to the strength `need` asks for.
1249/// The one place a real OS difference lives, quarantined behind the port here
1250/// rather than leaking up into the engine.
1251fn sync_path(path: &Path, need: Durability) -> io::Result<()> {
1252 // A fresh read handle is enough: fsync acts on the inode, not the descriptor,
1253 // so it flushes writes made through any handle. A path that does not exist (a
1254 // fallback write that failed before creating it) has nothing to flush and is
1255 // not an error.
1256 //
1257 // Opening a *directory* for reading and fsyncing it — how
1258 // [`Storage::write_atomic`] makes its rename durable — is a POSIX facility.
1259 // Windows has no equivalent (`MoveFileEx`'s durability is a separate story),
1260 // and rejects the open outright, so there the directory step is skipped
1261 // rather than faked.
1262 #[cfg(not(unix))]
1263 if path.is_dir() {
1264 return Ok(());
1265 }
1266 match std::fs::File::open(path) {
1267 Ok(file) => sync_file(&file, need)?,
1268 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
1269 Err(e) => return Err(e),
1270 }
1271 Ok(())
1272}
1273
1274/// Flush one open handle to the strength `need` asks for — with the
1275/// `barrier-fsync` feature on an Apple platform, the one place `Ordered` is
1276/// answered more cheaply than `Durable`.
1277#[cfg(all(feature = "barrier-fsync", target_vendor = "apple"))]
1278fn sync_file(file: &std::fs::File, need: Durability) -> io::Result<()> {
1279 use std::os::fd::AsRawFd as _;
1280
1281 match need {
1282 // A queue barrier: everything issued before it reaches the device
1283 // before anything issued after, without waiting for the drive to
1284 // drain its cache — which is the entire request `Ordered` makes, and
1285 // on these platforms often the difference between microseconds and
1286 // milliseconds. Works on files and directories alike.
1287 Durability::Ordered => {
1288 // SAFETY: `fcntl` with `F_BARRIERFSYNC` takes no argument beyond
1289 // the descriptor, and `file` holds that descriptor open for the
1290 // whole call.
1291 if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_BARRIERFSYNC) } != -1 {
1292 return Ok(());
1293 }
1294 // A filesystem with no barrier support — a network mount, an
1295 // exotic FUSE — refuses the fcntl. Plain `fsync` still keeps the
1296 // ordering promise (the named writes reach the device before the
1297 // call returns, so nothing later can precede them); it is
1298 // `sync_all`'s `F_FULLFSYNC` that would overshoot here.
1299 //
1300 // SAFETY: as above — a plain fsync of a descriptor `file` keeps
1301 // open.
1302 if unsafe { libc::fsync(file.as_raw_fd()) } != -1 {
1303 return Ok(());
1304 }
1305 Err(io::Error::last_os_error())
1306 }
1307 Durability::Durable => file.sync_all(),
1308 }
1309}
1310
1311/// Without the feature (or off Apple), both strengths are answered with
1312/// `sync_all` — stronger than `Ordered` asked for, which a backend may always
1313/// be, never weaker.
1314#[cfg(not(all(feature = "barrier-fsync", target_vendor = "apple")))]
1315fn sync_file(file: &std::fs::File, need: Durability) -> io::Result<()> {
1316 let _ = need;
1317 file.sync_all()
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322 use crate::exec::block_on;
1323
1324 use super::*;
1325
1326 fn tmp(name: &str) -> PathBuf {
1327 let dir = std::env::temp_dir().join(format!("fstx-fs-{name}-{}", std::process::id()));
1328 let _ = std::fs::remove_dir_all(&dir);
1329 std::fs::create_dir_all(&dir).unwrap();
1330 dir
1331 }
1332
1333 // ---- capability declaration ----
1334
1335 #[test]
1336 fn stdfs_declares_the_local_filesystem_guarantees() {
1337 // The native adapter promises atomic replacement, exclusive create, and
1338 // durable fsync, but not native transactions — the journal's job, not
1339 // the filesystem's.
1340 assert_eq!(StdFs.capabilities(), Capabilities::LOCAL_FS);
1341 assert!(StdFs.capabilities().atomic_replace);
1342 assert!(StdFs.capabilities().exclusive_create);
1343 assert_eq!(StdFs.capabilities().sync_guarantee, SyncGuarantee::Durable);
1344 assert!(!StdFs.capabilities().native_transactions);
1345 }
1346
1347 #[test]
1348 fn a_guarantee_answers_only_the_requests_it_can_keep() {
1349 // The whole point of the three-valued guarantee: the middle one can serve
1350 // `write_atomic`'s staging flush without being able to serve its final
1351 // one, which a boolean had no way to say.
1352 assert!(!SyncGuarantee::None.satisfies(Durability::Ordered));
1353 assert!(!SyncGuarantee::None.satisfies(Durability::Durable));
1354 assert!(SyncGuarantee::Ordered.satisfies(Durability::Ordered));
1355 assert!(!SyncGuarantee::Ordered.satisfies(Durability::Durable));
1356 assert!(SyncGuarantee::Durable.satisfies(Durability::Ordered));
1357 assert!(SyncGuarantee::Durable.satisfies(Durability::Durable));
1358 }
1359
1360 // ---- the atomic-write protocol ----
1361
1362 #[test]
1363 fn replace_stages_and_barriers_but_never_drains() {
1364 // The atomic half alone: temp sibling, one barrier, the rename — and
1365 // no durability anywhere, because the flush is the caller's to batch.
1366 let root = tmp("replace-protocol");
1367 std::fs::write(root.join("doc.md"), "old").unwrap();
1368 let fs = crate::fs_faults::RecordingFs::local();
1369 block_on(fs.replace(&root.join("doc.md"), b"new")).unwrap();
1370
1371 use crate::fs_faults::FsEvent;
1372 let tmp_name = temp_sibling(&root.join("doc.md"));
1373 assert_eq!(
1374 fs.events(),
1375 vec![
1376 FsEvent::Write(tmp_name.clone()),
1377 FsEvent::Sync(tmp_name.clone(), Durability::Ordered),
1378 FsEvent::Rename(tmp_name, root.join("doc.md")),
1379 ]
1380 );
1381 assert_eq!(std::fs::read_to_string(root.join("doc.md")).unwrap(), "new");
1382 }
1383
1384 #[test]
1385 fn write_atomic_is_replace_plus_the_flushes_it_left_behind() {
1386 // The composed default's event list, pinned: the same staging events
1387 // `replace` alone produces, then the parent's durable flush. (That an
1388 // overridden `replace` carries into `write_atomic` is pinned from the
1389 // consumer side, by the InMemoryFs write_atomic tests — this double
1390 // overrides neither.)
1391 let root = tmp("write-atomic-composed");
1392 let fs = crate::fs_faults::RecordingFs::local();
1393 block_on(fs.write_atomic(&root.join("doc.md"), b"bytes")).unwrap();
1394
1395 use crate::fs_faults::FsEvent;
1396 let tmp_name = temp_sibling(&root.join("doc.md"));
1397 assert_eq!(
1398 fs.events(),
1399 vec![
1400 FsEvent::Write(tmp_name.clone()),
1401 FsEvent::Sync(tmp_name.clone(), Durability::Ordered),
1402 FsEvent::Rename(tmp_name, root.join("doc.md")),
1403 FsEvent::Sync(root.clone(), Durability::Durable),
1404 ]
1405 );
1406 }
1407
1408 // ---- exclusive create ----
1409
1410 #[test]
1411 fn create_new_writes_a_fresh_file() {
1412 let root = tmp("create-new");
1413 let path = root.join("once.md");
1414 block_on(StdFs.create_new(&path, b"first")).unwrap();
1415 assert_eq!(std::fs::read_to_string(&path).unwrap(), "first");
1416 }
1417
1418 #[test]
1419 fn create_new_refuses_an_occupied_path_with_already_exists() {
1420 // `AlreadyExists` is the load-bearing half of the contract: a
1421 // write-once caller branches on exactly this kind, so it must not
1422 // arrive as anything vaguer.
1423 let root = tmp("create-new-taken");
1424 let path = root.join("once.md");
1425 block_on(StdFs.create_new(&path, b"first")).unwrap();
1426 let err = block_on(StdFs.create_new(&path, b"second")).unwrap_err();
1427 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1428 // The loser changed nothing: the winner's bytes are still what's there.
1429 assert_eq!(std::fs::read_to_string(&path).unwrap(), "first");
1430 }
1431
1432 #[test]
1433 fn the_default_create_new_declines_rather_than_emulating() {
1434 // A backend that overrides neither `create_new` nor `capabilities`
1435 // must refuse, not check-then-write: the emulation's window is what a
1436 // caller reaching for this method cannot tolerate.
1437 struct Bare;
1438 impl ReadStorage for Bare {
1439 async fn read(&self, _: &Path) -> io::Result<Vec<u8>> {
1440 unreachable!()
1441 }
1442 async fn read_to_string(&self, _: &Path) -> io::Result<String> {
1443 unreachable!()
1444 }
1445 async fn read_dir(&self, _: &Path) -> io::Result<Vec<DirEntry>> {
1446 unreachable!()
1447 }
1448 async fn metadata(&self, _: &Path) -> io::Result<Metadata> {
1449 unreachable!()
1450 }
1451 }
1452 impl Storage for Bare {
1453 async fn write(&self, _: &Path, _: &[u8]) -> io::Result<()> {
1454 unreachable!()
1455 }
1456 async fn create_dir_all(&self, _: &Path) -> io::Result<()> {
1457 unreachable!()
1458 }
1459 async fn remove_file(&self, _: &Path) -> io::Result<()> {
1460 unreachable!()
1461 }
1462 async fn remove_dir_all(&self, _: &Path) -> io::Result<()> {
1463 unreachable!()
1464 }
1465 async fn rename(&self, _: &Path, _: &Path) -> io::Result<()> {
1466 unreachable!()
1467 }
1468 }
1469 assert!(!Bare.capabilities().exclusive_create);
1470 let err = block_on(Bare.create_new(Path::new("x"), b"")).unwrap_err();
1471 assert_eq!(err.kind(), io::ErrorKind::Unsupported);
1472 }
1473
1474 // ---- sync ----
1475
1476 #[test]
1477 fn sync_of_a_missing_path_is_not_an_error() {
1478 // A fallback write that failed before creating the file leaves nothing to
1479 // flush; asking to sync it is a no-op, not a failure.
1480 let root = tmp("sync-missing");
1481 block_on(StdFs.sync(&root.join("never-created.md"), Durability::Durable)).unwrap();
1482 }
1483
1484 #[test]
1485 fn sync_flushes_a_directory_as_readily_as_a_file() {
1486 // `write_atomic` makes its rename durable by syncing the directory, so a
1487 // directory has to be something `sync` accepts rather than something it
1488 // reaches only via a file's parent.
1489 let root = tmp("sync-dir");
1490 block_on(StdFs.sync(&root, Durability::Durable)).unwrap();
1491 }
1492
1493 #[test]
1494 fn sync_answers_both_strengths_on_files_and_directories() {
1495 // With `barrier-fsync` on an Apple platform this exercises the
1496 // F_BARRIERFSYNC path for `Ordered`; everywhere else it is `sync_all`
1497 // twice. Either way both strengths must simply work, on both kinds of
1498 // object, because both protocols ask for both.
1499 let root = tmp("sync-strengths");
1500 let file = root.join("doc.md");
1501 std::fs::write(&file, "bytes").unwrap();
1502 for need in [Durability::Ordered, Durability::Durable] {
1503 block_on(StdFs.sync(&file, need)).unwrap();
1504 block_on(StdFs.sync(&root, need)).unwrap();
1505 }
1506 }
1507}