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}