Skip to main content

eggup_core/
error.rs

1use std::fmt;
2use std::io;
3
4/// The result type used by eggup-core.
5///
6/// `Err` means setup failed and nothing live changed. A post-lock outcome is
7/// reported through a [`crate::TransactionReceipt`], not through this type.
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// The error type of every `eggup-core` fallible entry point.
11///
12/// `Error` is a **closed** enum with exactly the seven variants below, so a
13/// downstream exhaustive `match` compiles unchanged. It covers both the
14/// pre-transaction failures (plan validation, private stage creation and copy)
15/// and the commit-time failures that are returned as `Err` rather than as a
16/// receipt: lock contention (`UpdateInProgress`), a preflight ownership
17/// conflict (`DestinationConflict`), failed integrity or candidate evidence
18/// (`VerificationFailed`, `CandidateExecution`), and live filesystem failures
19/// (`Io`).
20///
21/// Everything reached **after** the mutation lock is held is reported through a
22/// [`crate::TransactionReceipt`], not through this type: an ownership conflict
23/// or flap produces a `RolledBack` receipt, and a failure that cannot be
24/// resolved produces a `RecoveryRequired` receipt with a real
25/// `recovery_path`. See `architecture/core-transaction.md` ยง8 "Failure
26/// semantics".
27#[derive(Debug)]
28pub enum Error {
29    /// A caller supplied a value that violates a core invariant.
30    InvalidInput(String),
31    /// The requested member was not present in the artifact set.
32    UnknownMember(String),
33    /// An owned filesystem operation failed.
34    Io {
35        /// The operation being performed.
36        operation: &'static str,
37        /// The underlying operating-system error.
38        source: io::Error,
39    },
40    /// Another process or an ambiguous lock record owns the installation domain.
41    UpdateInProgress {
42        /// The lock path that prevented acquisition.
43        lock: std::path::PathBuf,
44    },
45    /// A live destination failed the immediate pre-mutation ownership check.
46    DestinationConflict {
47        /// The destination path that failed revalidation.
48        destination: std::path::PathBuf,
49    },
50    /// Declared integrity or candidate identity evidence did not pass.
51    VerificationFailed(String),
52    /// Candidate execution could not produce an accepted bounded result.
53    CandidateExecution(String),
54    /// A failure deliberately produced by the built-in test fault harness.
55    ///
56    /// This variant exists so fault injection is identified structurally,
57    /// never by matching caller-supplied message text. It is compiled only for
58    /// this crate's own tests, so the published error surface stays exactly
59    /// the seven variants `0.1.2` shipped; production paths cannot construct
60    /// it because the packaged enum does not contain it.
61    #[cfg(test)]
62    Injected(String),
63}
64
65/// The result type used by the recovery-aware entry points.
66///
67/// These APIs can fail either with an ordinary [`Error`] or by retaining real
68/// evidence that an operator must inspect. Keeping those apart means a
69/// downstream `match` on [`Error`] stays exhaustive, while a recovery failure
70/// still carries its evidence path in a typed field rather than in text.
71#[non_exhaustive]
72#[derive(Debug)]
73pub enum RecoveryError {
74    /// An ordinary core failure with no retained evidence.
75    Core(Error),
76    /// A partial recovery left Eggup-owned evidence that could not be resolved
77    /// automatically.
78    ///
79    /// `evidence` always names real retained bytes on disk. This is never
80    /// reported as a successful acquisition or a completed transaction.
81    RecoveryRequired {
82        /// Real, retained Eggup-owned evidence an operator must inspect.
83        evidence: std::path::PathBuf,
84        /// Bounded human-readable detail.
85        detail: String,
86    },
87}
88
89/// The result type returned by the recovery-aware entry points.
90///
91/// `Err` means setup failed and nothing live changed, or that real evidence was
92/// retained for an operator to inspect. A post-lock outcome that resolved
93/// cleanly is reported through a [`crate::TransactionReceipt`], not through
94/// this type.
95pub type RecoveryResult<T> = std::result::Result<T, RecoveryError>;
96
97impl RecoveryError {
98    /// Returns the real retained evidence path when this is a recovery failure.
99    pub fn evidence(&self) -> Option<&std::path::Path> {
100        match self {
101            Self::Core(_) => None,
102            Self::RecoveryRequired { evidence, .. } => Some(evidence),
103        }
104    }
105
106    /// Returns bounded human-readable detail for either variant.
107    pub fn detail(&self) -> &str {
108        match self {
109            Self::Core(error) => match error {
110                Error::InvalidInput(m)
111                | Error::UnknownMember(m)
112                | Error::VerificationFailed(m)
113                | Error::CandidateExecution(m) => m,
114                _ => "",
115            },
116            Self::RecoveryRequired { detail, .. } => detail,
117        }
118    }
119
120    /// Converts back to an ordinary [`Error`] for internal callers that cannot
121    /// represent retained evidence.
122    ///
123    /// Only used by paths that never enable stale-lock recovery, so
124    /// [`RecoveryError::RecoveryRequired`] cannot be constructed there. The
125    /// fallback arm still names the retained evidence instead of discarding it,
126    /// because silently dropping a path an operator needs is worse than an
127    /// approximate variant.
128    pub(crate) fn into_core(self) -> Error {
129        match self {
130            Self::Core(error) => error,
131            Self::RecoveryRequired { evidence, detail } => Error::io(
132                "internal recovery error outside a recovery entry point",
133                io::Error::other(format!("{detail} (retained at {})", evidence.display())),
134            ),
135        }
136    }
137}
138
139impl From<Error> for RecoveryError {
140    fn from(error: Error) -> Self {
141        Self::Core(error)
142    }
143}
144
145impl Error {
146    pub(crate) fn invalid(message: impl Into<String>) -> Self {
147        Self::InvalidInput(message.into())
148    }
149
150    /// Builds a fault-harness failure. Only the built-in fault injection
151    /// points may call this, and only in this crate's own test builds.
152    #[cfg(test)]
153    pub(crate) fn injected(message: impl Into<String>) -> Self {
154        Self::Injected(message.into())
155    }
156
157    pub(crate) fn io(operation: &'static str, source: io::Error) -> Self {
158        Self::Io { operation, source }
159    }
160}
161
162impl fmt::Display for RecoveryError {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            Self::Core(error) => error.fmt(formatter),
166            Self::RecoveryRequired { evidence, detail } => write!(
167                formatter,
168                "recovery evidence retained at {}: {detail}",
169                evidence.display()
170            ),
171        }
172    }
173}
174
175impl std::error::Error for RecoveryError {
176    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
177        match self {
178            Self::Core(error) => Some(error),
179            Self::RecoveryRequired { .. } => None,
180        }
181    }
182}
183
184impl fmt::Display for Error {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        match self {
187            Self::InvalidInput(message) => write!(formatter, "invalid update input: {message}"),
188            Self::UnknownMember(member) => write!(formatter, "unknown artifact member: {member}"),
189            Self::Io { operation, source } => write!(formatter, "{operation}: {source}"),
190            Self::UpdateInProgress { lock } => {
191                write!(
192                    formatter,
193                    "installation update already in progress: {}",
194                    lock.display()
195                )
196            }
197            Self::DestinationConflict { destination } => write!(
198                formatter,
199                "destination failed ownership revalidation: {}",
200                destination.display()
201            ),
202            Self::VerificationFailed(message) => {
203                write!(formatter, "verification failed: {message}")
204            }
205            Self::CandidateExecution(message) => {
206                write!(formatter, "candidate execution failed: {message}")
207            }
208            #[cfg(test)]
209            Self::Injected(message) => {
210                write!(formatter, "injected failure: {message}")
211            }
212        }
213    }
214}
215
216impl std::error::Error for Error {
217    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
218        match self {
219            Self::Io { source, .. } => Some(source),
220            #[cfg(test)]
221            Self::Injected(_) => None,
222            Self::InvalidInput(_)
223            | Self::UnknownMember(_)
224            | Self::UpdateInProgress { .. }
225            | Self::DestinationConflict { .. }
226            | Self::VerificationFailed(_)
227            | Self::CandidateExecution(_) => None,
228        }
229    }
230}