Skip to main content

prov_graph/
error.rs

1//! Error and result types.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7/// Errors produced by prov.
8#[derive(Debug, Error)]
9pub enum Error {
10    /// The embedded-metadata backend (`fig`) failed to parse or serialize.
11    #[error("metadata error: {0}")]
12    Meta(#[from] fig::Error),
13
14    /// A structural invariant was violated (e.g. malformed frontmatter fence).
15    #[error("{0}")]
16    Structure(String),
17
18    /// A document a workspace operation names is not on disk — the typed form of
19    /// the many "X does not exist" guards the mutation ops make before touching a
20    /// document (`reparent`, `rename`, `duplicate`, `register`, …). A caller can
21    /// tell a genuinely-missing target from a malformed one by matching the
22    /// variant, rather than sniffing the message text.
23    #[error("{0} does not exist")]
24    NotFound(PathBuf),
25
26    /// A workspace operation would create a document where one already exists, and
27    /// refused rather than overwrite it — the typed form of the "X already exists"
28    /// guards in `create`/`rename`/`attach`. A destination collision is a distinct
29    /// outcome from a missing source, and now distinguishable as one.
30    #[error("{0} already exists")]
31    AlreadyExists(PathBuf),
32
33    /// The storage backend failed.
34    #[error("io error: {0}")]
35    Io(#[from] std::io::Error),
36
37    /// The `twig` body parser failed — see `content.rs`.
38    #[error("content error: {0}")]
39    Content(String),
40
41    /// A record store — the id registry, the recycle-bin index, or a flat
42    /// vocabulary — was found in a **markdown** carrier (fenced frontmatter)
43    /// rather than a whole-file config document (`.yaml`/`.json`/`.figl`). prov
44    /// imposes a sorted, one-record-per-line layout on these stores (DESIGN §5),
45    /// so a prose carrier has no stable home for its records and is refused. Make
46    /// it a bare config file. See [`crate::document::require_whole_file`].
47    #[error(
48        "record store must be a whole-file config document (.yaml/.json/.figl), \
49         not markdown frontmatter: {0}"
50    )]
51    MarkdownStore(PathBuf),
52
53    /// A path handed to a workspace read or write resolved *outside* the
54    /// workspace root — an absolute path, or one that climbs above the root with
55    /// `..`. prov clamps every I/O to the tree it was pointed at (a link
56    /// target is data, and data must never be able to name `/etc/passwd` or a
57    /// sibling repo), so such a path is refused rather than followed. See
58    /// [`crate::link::escapes_root`], the guard at `prov`'s `Workspace`'s `load`
59    /// and `prov`'s `ChangeSet::apply`.
60    #[error("path escapes the workspace root: {0}")]
61    Escape(PathBuf),
62
63    /// A `prov`'s `ChangeSet` was applied while a previous change's
64    /// write-ahead journal was still on disk — an earlier mutation was
65    /// interrupted (a crash) and never recovered. Landing this set would
66    /// overwrite that journal and lose the record needed to complete the
67    /// interrupted change, so the apply refuses: run recovery
68    /// (`prov`'s `journal::recover`, which `prov check` performs) first, then
69    /// retry.
70    #[error(
71        "a previous change was interrupted and not yet recovered (found {0}); \
72         recover it first (run `prov check`), then retry"
73    )]
74    StaleJournal(PathBuf),
75
76    /// An apply could not deliver either of its durable answers — see
77    /// `prov`'s `ChangeSet::apply`. The classic case: a staged write failed
78    /// *and* the rollback that should have undone it also failed. Since
79    /// fs-transaction 0.2 the same shape also covers a rollback or a
80    /// completed apply whose certification (or journal retirement) could not
81    /// be made durable. In every case prov cannot stand behind a clean
82    /// endpoint, so it says exactly that instead of reporting the original
83    /// failure as if the workspace were untouched — and `prov check`'s
84    /// recovery is what resolves the workspace to a nameable state.
85    #[error(
86        "{cause}; and rolling back failed too: {rollback}. \
87         The workspace may be partially written — run `prov check`."
88    )]
89    Torn {
90        /// The failure that triggered the rollback.
91        cause: String,
92        /// The failure the rollback itself hit.
93        rollback: String,
94    },
95
96    /// A mutation's reading of the workspace went stale before its writes
97    /// landed: between an op computing its edits and applying them, something
98    /// else — another process, a sync daemon — wrote to this path. The op
99    /// stages what it read as an expectation on its `prov`'s `ChangeSet`
100    /// (`ChangeSet::expect` / `expect_absent`), and the apply refuses the whole
101    /// set rather than land edits computed from a reading that no longer holds:
102    /// nothing was written, journaled, or unwound. Unlike
103    /// [`AlreadyExists`](Self::AlreadyExists) (the same fact caught while
104    /// computing), this is retryable as-is — re-run the operation and it
105    /// recomputes over the fresh state.
106    #[error(
107        "{0} changed under this operation — something else wrote to the \
108         workspace after it was read; nothing was written, so re-run the \
109         operation to recompute over the current state"
110    )]
111    Drifted(PathBuf),
112
113    /// An operation would have registered an ID across a registration the index
114    /// already holds — see [`Collision`](crate::index::Collision). Refused rather
115    /// than resolved, because the displaced document still spells the ID in its
116    /// own frontmatter and only its author can say which one should keep it.
117    #[error("{0}; refusing to displace it")]
118    Collision(crate::index::Collision),
119}
120
121impl From<crate::index::Collision> for Error {
122    fn from(collision: crate::index::Collision) -> Self {
123        Error::Collision(collision)
124    }
125}
126
127/// Convenience alias for results in this crate.
128pub type Result<T> = std::result::Result<T, Error>;
129
130/// Carry a transaction failure into prov's own error vocabulary.
131///
132/// [`fs_transaction`] phrases its errors for a generic tree of files, since
133/// it knows nothing about workspaces. The variants map one-to-one onto prov's,
134/// which restate them in terms a prov user can act on — naming `prov check` as
135/// the recovery step, and a workspace root as the boundary that was crossed.
136impl From<fs_transaction::Error> for Error {
137    fn from(e: fs_transaction::Error) -> Self {
138        use fs_transaction::Error as Tx;
139        match e {
140            Tx::Io(e) => Error::Io(e),
141            Tx::Escape(path) => Error::Escape(path),
142            Tx::StaleJournal(path) => Error::StaleJournal(path),
143            Tx::Drifted(path) => Error::Drifted(path),
144            Tx::Torn { cause, rollback } => Error::Torn { cause, rollback },
145            // The three that have no prov-level counterpart: a journal prov
146            // cannot read, a replay it cannot finish, and a path it could not
147            // encode. All are structural failures of the on-disk state, which
148            // is what `Structure` names.
149            Tx::NonUtf8Path(path) => {
150                Error::Structure(format!("cannot journal non-UTF-8 path: {}", path.display()))
151            }
152            Tx::Corrupt(what) => Error::Structure(format!("journal is corrupt: {what}")),
153            Tx::Recovery(what) => Error::Structure(format!("journal replay: {what}")),
154            // `fs_transaction::Error` is `#[non_exhaustive]`: a variant added
155            // upstream must not silently become a compile error here, but it
156            // must not be mistaken for a prov-level failure either.
157            other => Error::Structure(other.to_string()),
158        }
159    }
160}