Skip to main content

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}