Skip to main content

eggup_core/
error.rs

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