Skip to main content

evault_core/
error.rs

1//! Error types for `evault-core`.
2//!
3//! Each subsystem has its own [`thiserror`]-derived enum so that callers can
4//! match precisely on what went wrong. [`CoreError`] is a convenience
5//! aggregator for code that consumes several subsystems through a single
6//! result type.
7//!
8//! All error types are `#[non_exhaustive]` so that adding new variants is not
9//! a breaking change for downstream crates that pattern-match on them.
10
11use std::path::PathBuf;
12
13use thiserror::Error;
14
15use crate::model::{ProjectId, VarId, VarKind};
16
17/// Errors that can occur while interacting with a
18/// [`MetadataStore`](crate::traits::MetadataStore).
19#[non_exhaustive]
20#[derive(Error, Debug)]
21pub enum MetadataError {
22    /// A variable with the given name already exists in the registry.
23    #[error("variable named {0:?} already exists")]
24    DuplicateName(String),
25
26    /// No variable with the given identifier was found.
27    #[error("variable {0} not found")]
28    VarNotFound(VarId),
29
30    /// No project with the given identifier was found.
31    #[error("project {0} not found")]
32    ProjectNotFound(ProjectId),
33
34    /// A value supplied to the metadata layer failed a domain invariant.
35    #[error("invalid input: {0}")]
36    Invalid(String),
37
38    /// An operation that requires a specific [`VarKind`] (e.g. storing a
39    /// plain value) was invoked on a variable of the wrong kind.
40    ///
41    /// This guards against secrets accidentally flowing into the plain-value
42    /// side table — every backend must enforce this rule, never silently
43    /// accept a kind mismatch.
44    #[error("variable {id} has kind {actual:?} but {expected:?} was required")]
45    KindMismatch {
46        /// Identifier of the offending variable.
47        id: VarId,
48        /// The kind the operation expected.
49        expected: VarKind,
50        /// The kind the variable actually has.
51        actual: VarKind,
52    },
53
54    /// The underlying storage backend reported an unexpected failure.
55    ///
56    /// **Backend implementors MUST NOT** include secret material, variable
57    /// values, or absolute paths that disclose the user's home directory in
58    /// this string. Wrap raw backend errors with a category label (e.g.
59    /// `"sqlite write failed"`) rather than `format!("{e}")` that may
60    /// propagate parameter values from a failed query.
61    #[error("backend error: {0}")]
62    Backend(String),
63}
64
65/// Errors that can occur while interacting with a
66/// [`SecretStore`](crate::traits::SecretStore).
67#[non_exhaustive]
68#[derive(Error, Debug)]
69pub enum SecretError {
70    /// The underlying OS keyring or fallback backend rejected the operation.
71    ///
72    /// **Backend implementors MUST NOT** include the secret value, the
73    /// keyring service/account names containing user paths, or any data that
74    /// could disclose the secret in this string.
75    #[error("secret backend error: {0}")]
76    Backend(String),
77
78    /// The platform does not provide secure storage and no fallback was
79    /// configured by the caller.
80    #[error("no secure storage available on this platform")]
81    Unavailable,
82}
83
84/// Errors that can occur while loading or saving an `evault.toml` manifest.
85#[non_exhaustive]
86#[derive(Error, Debug)]
87pub enum ManifestError {
88    /// The manifest file does not exist at the supplied path.
89    ///
90    /// Note: the path is the exact string the caller supplied and may
91    /// disclose the user's home directory if the resulting error is shipped
92    /// off-host. Strip or redact before forwarding to remote sinks.
93    #[error("manifest path does not exist: {0}")]
94    NotFound(PathBuf),
95
96    /// An I/O error occurred while reading or writing the manifest file
97    /// (permission denied, disk failure, path is a directory, etc.).
98    ///
99    /// **Backend implementors MUST NOT** include the OS error message
100    /// verbatim (which can echo paths or quoting). Carry only the
101    /// [`std::io::ErrorKind`] discriminant or a stable category label.
102    #[error("manifest io error: {0}")]
103    Io(String),
104
105    /// The manifest contents could not be parsed.
106    ///
107    /// **Backend implementors MUST NOT** include rendered manifest content,
108    /// the inline values of `BindingSource::Inline` bindings, or absolute
109    /// paths that disclose the user's home directory in this string. Quote
110    /// only the structural error (expected token, line/column) — never the
111    /// surrounding source text.
112    #[error("manifest parse failed: {0}")]
113    Parse(String),
114
115    /// The manifest could not be written back to disk.
116    ///
117    /// **Backend implementors MUST NOT** include rendered manifest content,
118    /// the inline values being serialized, or absolute paths that disclose
119    /// the user's home directory in this string.
120    #[error("manifest write failed: {0}")]
121    Write(String),
122
123    /// The manifest parsed but violated a structural rule (e.g. duplicate var).
124    ///
125    /// **Backend implementors MUST NOT** include rendered inline values or
126    /// secret material in this string. Quote variable names by all means;
127    /// never their values.
128    #[error("invalid manifest: {0}")]
129    Invalid(String),
130}
131
132/// Errors that can occur while materializing a `.env` file from a manifest.
133#[non_exhaustive]
134#[derive(Error, Debug)]
135pub enum MaterializerError {
136    /// I/O error while reading or writing files.
137    #[error("io error: {0}")]
138    Io(#[from] std::io::Error),
139
140    /// The manifest referenced a variable that is missing from the registry.
141    #[error("variable referenced by manifest not found in registry: {0}")]
142    MissingVar(String),
143
144    /// The materializer backend reported an unexpected failure.
145    ///
146    /// **Backend implementors MUST NOT** include the rendered values, the
147    /// secret material, or absolute path components in this string.
148    #[error("materializer backend error: {0}")]
149    Backend(String),
150}
151
152/// Errors that can occur while running a child process with an injected
153/// environment.
154#[non_exhaustive]
155#[derive(Error, Debug)]
156pub enum RunnerError {
157    /// An env key or value supplied to the runner failed validation.
158    /// Specifically: keys must satisfy the same shape as variable names
159    /// (ASCII letter/underscore start, alphanumerics/underscore body);
160    /// values must not contain a NUL byte, which would terminate the
161    /// OS environment block prematurely. The error string carries the
162    /// key (which is not secret per `evault`'s design) and a category
163    /// label, but never the surrounding value text.
164    #[error("invalid env input: {0}")]
165    Invalid(String),
166
167    /// Spawning the child process failed.
168    #[error("failed to spawn process: {0}")]
169    Spawn(String),
170
171    /// The child exited with a non-zero status code.
172    #[error("process exited with non-zero status: {0}")]
173    NonZeroExit(i32),
174
175    /// I/O error while interacting with the child process.
176    #[error("io error: {0}")]
177    Io(#[from] std::io::Error),
178}
179
180/// Errors that can occur while scanning source code for environment-variable
181/// references.
182#[non_exhaustive]
183#[derive(Error, Debug)]
184pub enum ScannerError {
185    /// I/O error while walking the file tree.
186    #[error("io error: {0}")]
187    Io(#[from] std::io::Error),
188
189    /// One of the configured patterns failed to compile.
190    #[error("scanner pattern error: {0}")]
191    Pattern(String),
192}
193
194/// Convenience aggregator for code that consumes multiple subsystems through a
195/// single result type.
196#[non_exhaustive]
197#[derive(Error, Debug)]
198pub enum CoreError {
199    /// See [`MetadataError`].
200    #[error(transparent)]
201    Metadata(#[from] MetadataError),
202
203    /// See [`SecretError`].
204    #[error(transparent)]
205    Secret(#[from] SecretError),
206
207    /// See [`ManifestError`].
208    #[error(transparent)]
209    Manifest(#[from] ManifestError),
210
211    /// See [`MaterializerError`].
212    #[error(transparent)]
213    Materializer(#[from] MaterializerError),
214
215    /// See [`RunnerError`].
216    #[error(transparent)]
217    Runner(#[from] RunnerError),
218
219    /// See [`ScannerError`].
220    #[error(transparent)]
221    Scanner(#[from] ScannerError),
222
223    /// A variable's metadata advertises one [`VarKind`] but the value was
224    /// found in the opposite storage tier (or could not be found in the
225    /// expected tier while existing in the other).
226    ///
227    /// This indicates corruption that bypassed the service layer's normal
228    /// routing rules. Higher layers should surface this loudly rather than
229    /// treating it as "value missing".
230    #[error(
231        "variable {id} has kind {expected:?} in metadata but the value is in the {found:?} tier"
232    )]
233    TierMismatch {
234        /// Identifier of the offending variable.
235        id: VarId,
236        /// The kind the metadata record claims.
237        expected: VarKind,
238        /// The tier the value was actually found in.
239        found: VarKind,
240    },
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn duplicate_name_message_contains_name() {
249        let e = MetadataError::DuplicateName("API_KEY".to_owned());
250        assert!(e.to_string().contains("API_KEY"));
251    }
252
253    #[test]
254    fn core_error_from_metadata_via_question_mark() {
255        fn inner() -> Result<(), MetadataError> {
256            Err(MetadataError::Invalid("bad".into()))
257        }
258        fn outer() -> Result<(), CoreError> {
259            inner()?;
260            Ok(())
261        }
262        let err = outer().expect_err("expected error");
263        assert!(matches!(err, CoreError::Metadata(_)));
264    }
265
266    #[test]
267    fn io_error_converts_into_materializer_error() {
268        let io = std::io::Error::other("disk full");
269        let me: MaterializerError = io.into();
270        assert!(me.to_string().contains("disk full"));
271    }
272}