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}