Skip to main content

fs_transaction/
error.rs

1//! What a transaction can fail with.
2//!
3//! Five variants, and the split between them is the crate's whole safety story:
4//! [`Io`](Error::Io) and [`Escape`](Error::Escape) are ordinary refusals that
5//! leave the target untouched, [`Corrupt`](Error::Corrupt) and
6//! [`StaleJournal`](Error::StaleJournal) are recovery refusing to guess, and
7//! [`Torn`](Error::Torn) is the one case where the state on disk cannot be
8//! named.
9//!
10//! There is no `thiserror` here on purpose — the crate has no dependencies, and
11//! five variants do not need a derive to spell out.
12
13use std::fmt;
14use std::io;
15use std::path::PathBuf;
16
17/// A transaction result.
18pub type Result<T> = std::result::Result<T, Error>;
19
20/// Everything applying or recovering a [`ChangeSet`](crate::ChangeSet) can fail
21/// with.
22#[derive(Debug)]
23#[non_exhaustive]
24pub enum Error {
25    /// The backend refused an operation. The staged set is unwound before this
26    /// surfaces, so the tree is as it was.
27    Io(io::Error),
28
29    /// A staged path resolved outside the root it was applied against — either
30    /// absolute, or climbing past the root with `..`. Refused before anything is
31    /// written or journaled, because a set assembled from untrusted data must
32    /// not be able to reach out of the tree it was pointed at.
33    Escape(PathBuf),
34
35    /// A [`Journal`](crate::journal::Journal) was asked for under a name that
36    /// is not a single path component. Refused at construction, because the
37    /// name is joined onto a caller-supplied root and one containing `..` or a
38    /// separator would write outside the very tree an apply clamps into.
39    InvalidJournalName(String),
40
41    /// A staged path could not be encoded into the journal because it is not
42    /// UTF-8. The journal stores paths as UTF-8 so that a set written on one
43    /// platform replays identically on another; a path that cannot round-trip
44    /// is refused at the commit point rather than silently mangled.
45    NonUtf8Path(PathBuf),
46
47    /// A journal was found that could not be trusted: a bad magic, a checksum
48    /// mismatch, a truncated record, an unknown op tag. Refused rather than
49    /// partially replayed — a journal exists to prevent invented states, so one
50    /// that cannot be read is never guessed at.
51    Corrupt(String),
52
53    /// A journal was read successfully but could not be replayed to completion:
54    /// a [`CopyFrom`](crate::FileOp::CopyFrom) whose source has gone, or a
55    /// rename with neither side present. Distinct from
56    /// [`Corrupt`](Error::Corrupt) — the intent was legible, the tree just
57    /// could not be brought to it. The journal is left in place so a later
58    /// recovery can finish once the missing piece is back.
59    Recovery(String),
60
61    /// A set was applied while a *previous* set's journal was still on disk: an
62    /// earlier change was interrupted and never recovered. Landing this set
63    /// would overwrite the record needed to finish that one, so the apply
64    /// refuses. Call [`recover`](crate::recover) first, then retry.
65    StaleJournal(PathBuf),
66
67    /// A staged op failed *and* the rollback that should have undone it failed
68    /// too. The one case where the crate cannot say what is on disk — so it says
69    /// exactly that, rather than reporting the original failure as if the tree
70    /// were untouched.
71    ///
72    /// The journal is deliberately left in place when this is returned, so the
73    /// next [`recover`](crate::recover) rolls the set *forward* to the applied
74    /// state. Either way the tree lands somewhere nameable.
75    Torn {
76        /// The failure that triggered the rollback.
77        cause: String,
78        /// The failure the rollback itself hit.
79        rollback: String,
80    },
81}
82
83impl fmt::Display for Error {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Error::Io(e) => write!(f, "io error: {e}"),
87            Error::Escape(p) => write!(f, "path escapes the root: {}", p.display()),
88            Error::InvalidJournalName(name) => write!(
89                f,
90                "journal name must be a single path component, got {name:?}"
91            ),
92            Error::NonUtf8Path(p) => {
93                write!(f, "journal cannot encode non-UTF-8 path: {}", p.display())
94            }
95            Error::Corrupt(what) => write!(f, "journal is corrupt: {what}"),
96            Error::Recovery(what) => write!(f, "journal replay: {what}"),
97            Error::StaleJournal(p) => write!(
98                f,
99                "a previous change was interrupted and not yet recovered (found {}); \
100                 recover it first, then retry",
101                p.display()
102            ),
103            Error::Torn { cause, rollback } => write!(
104                f,
105                "{cause}; and rolling back failed too: {rollback}. \
106                 The tree may be partially written — run recovery."
107            ),
108        }
109    }
110}
111
112impl std::error::Error for Error {
113    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114        match self {
115            Error::Io(e) => Some(e),
116            _ => None,
117        }
118    }
119}
120
121impl From<io::Error> for Error {
122    fn from(e: io::Error) -> Self {
123        Error::Io(e)
124    }
125}