Skip to main content

fs_transaction/
error.rs

1//! What a transaction can fail with.
2//!
3//! The split between the variants is the crate's whole safety story:
4//! [`Io`](Error::Io), [`Escape`](Error::Escape), and
5//! [`Drifted`](Error::Drifted) are ordinary refusals that
6//! leave the target untouched, the construction-time refusals
7//! ([`InvalidJournalName`](Error::InvalidJournalName),
8//! [`InvalidJournalHome`](Error::InvalidJournalHome),
9//! [`NonUtf8Path`](Error::NonUtf8Path)) stop a mistake before anything is
10//! written, [`Corrupt`](Error::Corrupt) and
11//! [`StaleJournal`](Error::StaleJournal) are recovery refusing to guess, and
12//! [`Torn`](Error::Torn) is the one case where the state on disk cannot be
13//! named.
14//!
15//! There is no `thiserror` here on purpose — the crate has no required
16//! dependencies, and a handful of variants do not need a derive to spell out.
17
18use std::fmt;
19use std::io;
20use std::path::PathBuf;
21
22/// A transaction result.
23pub type Result<T> = std::result::Result<T, Error>;
24
25/// Everything applying or recovering a [`ChangeSet`](crate::ChangeSet) can fail
26/// with.
27#[derive(Debug)]
28#[non_exhaustive]
29pub enum Error {
30    /// The backend refused an operation. The staged set is unwound before this
31    /// surfaces, so the tree is as it was.
32    Io(io::Error),
33
34    /// A staged path resolved outside the root it was applied against — either
35    /// absolute, or climbing past the root with `..`. Refused before anything is
36    /// written or journaled, because a set assembled from untrusted data must
37    /// not be able to reach out of the tree it was pointed at.
38    Escape(PathBuf),
39
40    /// An [expectation](crate::ChangeSet::expect) the set staged did not hold
41    /// when it was applied: the tree at this path is not what the caller read
42    /// when it computed the set — something else wrote in between. Refused
43    /// before the commit point, so nothing has been written, journaled, or
44    /// unwound; the caller re-reads, restages, and retries. This is drift
45    /// *detection*, not a lock — see [`change`](crate::change) on the single
46    /// writer.
47    Drifted(PathBuf),
48
49    /// A [`Journal`](crate::journal::Journal) was asked for under a name that
50    /// is not a single path component. Refused at construction, because the
51    /// name is joined onto a caller-supplied root and one containing `..` or a
52    /// separator would write outside the very tree an apply clamps into.
53    InvalidJournalName(String),
54
55    /// A [`Journal`](crate::journal::Journal) was asked to live
56    /// ([`kept_in`](crate::journal::Journal::kept_in)) in a directory that is
57    /// not absolute. Refused at construction: a relative home resolves against
58    /// the process's current directory, which the apply that writes the
59    /// journal and the recovery that must find it have no reason to share —
60    /// and a journal sought where it was never written strands its change
61    /// half-applied.
62    InvalidJournalHome(PathBuf),
63
64    /// A staged path could not be encoded into the journal because it is not
65    /// UTF-8. The journal stores paths as UTF-8 so that a set written on one
66    /// platform replays identically on another; a path that cannot round-trip
67    /// is refused at the commit point rather than silently mangled.
68    NonUtf8Path(PathBuf),
69
70    /// A journal was found that could not be trusted: a bad magic, a checksum
71    /// mismatch, a truncated record, an unknown op tag. Refused rather than
72    /// partially replayed — a journal exists to prevent invented states, so one
73    /// that cannot be read is never guessed at.
74    Corrupt(String),
75
76    /// A journal was read successfully but could not be replayed to completion:
77    /// a [`CopyFrom`](crate::FileOp::CopyFrom) whose source has gone, or a
78    /// rename with neither side present. Distinct from
79    /// [`Corrupt`](Error::Corrupt) — the intent was legible, the tree just
80    /// could not be brought to it. The journal is left in place so a later
81    /// recovery can finish once the missing piece is back.
82    Recovery(String),
83
84    /// A set was applied while a *previous* set's journal was still on disk: an
85    /// earlier change was interrupted and never recovered. Landing this set
86    /// would overwrite the record needed to finish that one, so the apply
87    /// refuses. Call [`recover`](crate::recover) first, then retry.
88    StaleJournal(PathBuf),
89
90    /// The apply could not deliver either of its two durable answers. The
91    /// classic case: a staged op failed *and* the rollback that should have
92    /// undone it failed too. Two rarer ones share the shape — a rollback that
93    /// completed but whose certification (or the journal's retirement) could
94    /// not be made durable, and a set that applied and certified cleanly but
95    /// whose journal could not be removed. In every case the crate says
96    /// exactly what it can and cannot promise, rather than reporting a clean
97    /// endpoint it cannot stand behind.
98    ///
99    /// The journal is left in place wherever possible, so the next
100    /// [`recover`](crate::recover) resolves the tree — rolling an uncertified
101    /// set *forward* to the applied state, or no-op replaying an applied one
102    /// and clearing the journal. Either way the tree lands somewhere
103    /// nameable.
104    Torn {
105        /// The failure that triggered the rollback.
106        cause: String,
107        /// The failure the rollback itself hit.
108        rollback: String,
109    },
110}
111
112impl fmt::Display for Error {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Error::Io(e) => write!(f, "io error: {e}"),
116            Error::Escape(p) => write!(f, "path escapes the root: {}", p.display()),
117            Error::Drifted(p) => write!(
118                f,
119                "the tree drifted from what the set expected at {}; \
120                 nothing was written — re-read and restage",
121                p.display()
122            ),
123            Error::InvalidJournalName(name) => write!(
124                f,
125                "journal name must be a single path component, got {name:?}"
126            ),
127            Error::InvalidJournalHome(dir) => write!(
128                f,
129                "a journal's home must be an absolute directory, got {}",
130                dir.display()
131            ),
132            Error::NonUtf8Path(p) => {
133                write!(f, "journal cannot encode non-UTF-8 path: {}", p.display())
134            }
135            Error::Corrupt(what) => write!(f, "journal is corrupt: {what}"),
136            Error::Recovery(what) => write!(f, "journal replay: {what}"),
137            Error::StaleJournal(p) => write!(
138                f,
139                "a previous change was interrupted and not yet recovered (found {}); \
140                 recover it first, then retry",
141                p.display()
142            ),
143            Error::Torn { cause, rollback } => write!(
144                f,
145                "{cause}; and rolling back failed too: {rollback}. \
146                 The tree may be partially written — run recovery."
147            ),
148        }
149    }
150}
151
152impl std::error::Error for Error {
153    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
154        match self {
155            Error::Io(e) => Some(e),
156            _ => None,
157        }
158    }
159}
160
161impl From<io::Error> for Error {
162    fn from(e: io::Error) -> Self {
163        Error::Io(e)
164    }
165}