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}