soar_utils/
error.rs

1//! Error types for soar-utils.
2
3// https://github.com/zkat/miette/issues/458
4// https://github.com/rust-lang/rust/issues/147648
5#![expect(unused_assignments)]
6
7use std::path::PathBuf;
8
9use miette::Diagnostic;
10use thiserror::Error;
11
12/// Error type for byte parsing operations.
13#[derive(Error, Diagnostic, Debug)]
14pub enum BytesError {
15    #[error("Failed to parse '{input}' as bytes: {reason}")]
16    #[diagnostic(
17        code(soar_utils::bytes::parse),
18        help("Use a valid byte format like '1KB', '2MB', or '3GB'")
19    )]
20    ParseFailed { input: String, reason: String },
21}
22
23/// Error type for hash operations.
24#[derive(Error, Diagnostic, Debug)]
25pub enum HashError {
26    #[error("Failed to read file '{path}'")]
27    #[diagnostic(
28        code(soar_utils::hash::read),
29        help("Check if the file exists and you have read permissions")
30    )]
31    ReadFailed {
32        path: PathBuf,
33        #[source]
34        source: std::io::Error,
35    },
36}
37
38/// Error type for path operations.
39#[derive(Error, Diagnostic, Debug)]
40pub enum PathError {
41    #[error("Failed to get current directory")]
42    #[diagnostic(
43        code(soar_utils::path::cwd),
44        help("Check if the current directory still exists")
45    )]
46    FailedToGetCurrentDir {
47        #[source]
48        source: std::io::Error,
49    },
50
51    #[error("Path is empty")]
52    #[diagnostic(code(soar_utils::path::empty), help("Provide a non-empty path"))]
53    Empty,
54
55    #[error("Environment variable '{var}' not set in '{input}'")]
56    #[diagnostic(
57        code(soar_utils::path::env_var),
58        help("Set the environment variable or use a different path")
59    )]
60    MissingEnvVar { var: String, input: String },
61
62    #[error("Unclosed variable expression starting at '{input}'")]
63    #[diagnostic(
64        code(soar_utils::path::unclosed_var),
65        help("Close the variable expression with '}}'")
66    )]
67    UnclosedVariable { input: String },
68}
69
70/// Error type for filesystem operations.
71#[derive(Error, Diagnostic, Debug)]
72pub enum FileSystemError {
73    #[error("Failed to read file '{path}'")]
74    #[diagnostic(
75        code(soar_utils::fs::read_file),
76        help("Check if the file exists and you have read permissions")
77    )]
78    ReadFile {
79        path: PathBuf,
80        #[source]
81        source: std::io::Error,
82    },
83
84    #[error("Failed to write file '{path}'")]
85    #[diagnostic(
86        code(soar_utils::fs::write_file),
87        help("Check if you have write permissions to the directory")
88    )]
89    WriteFile {
90        path: PathBuf,
91        #[source]
92        source: std::io::Error,
93    },
94
95    #[error("Failed to create file '{path}'")]
96    #[diagnostic(
97        code(soar_utils::fs::create_file),
98        help("Check if the directory exists and you have write permissions")
99    )]
100    CreateFile {
101        path: PathBuf,
102        #[source]
103        source: std::io::Error,
104    },
105
106    #[error("Failed to remove file '{path}'")]
107    #[diagnostic(
108        code(soar_utils::fs::remove_file),
109        help("Check if you have write permissions to the file")
110    )]
111    RemoveFile {
112        path: PathBuf,
113        #[source]
114        source: std::io::Error,
115    },
116
117    #[error("Failed to read directory '{path}'")]
118    #[diagnostic(
119        code(soar_utils::fs::read_dir),
120        help("Check if the directory exists and you have read permissions")
121    )]
122    ReadDirectory {
123        path: PathBuf,
124        #[source]
125        source: std::io::Error,
126    },
127
128    #[error("Failed to create directory '{path}'")]
129    #[diagnostic(
130        code(soar_utils::fs::create_dir),
131        help("Check if the parent directory exists and you have write permissions")
132    )]
133    CreateDirectory {
134        path: PathBuf,
135        #[source]
136        source: std::io::Error,
137    },
138
139    #[error("Failed to remove directory '{path}'")]
140    #[diagnostic(
141        code(soar_utils::fs::remove_dir),
142        help("Check if the directory is empty and you have write permissions")
143    )]
144    RemoveDirectory {
145        path: PathBuf,
146        #[source]
147        source: std::io::Error,
148    },
149
150    #[error("Failed to create symlink from '{from}' to '{target}'")]
151    #[diagnostic(
152        code(soar_utils::fs::create_symlink),
153        help("Check if you have write permissions and the target doesn't already exist")
154    )]
155    CreateSymlink {
156        from: PathBuf,
157        target: PathBuf,
158        #[source]
159        source: std::io::Error,
160    },
161
162    #[error("Failed to remove symlink '{path}'")]
163    #[diagnostic(
164        code(soar_utils::fs::remove_symlink),
165        help("Check if you have write permissions")
166    )]
167    RemoveSymlink {
168        path: PathBuf,
169        #[source]
170        source: std::io::Error,
171    },
172
173    #[error("Failed to read symlink '{path}'")]
174    #[diagnostic(
175        code(soar_utils::fs::read_symlink),
176        help("Check if the symlink exists")
177    )]
178    ReadSymlink {
179        path: PathBuf,
180        #[source]
181        source: std::io::Error,
182    },
183
184    #[error("Path '{path}' not found")]
185    #[diagnostic(code(soar_utils::fs::not_found), help("Check if the path exists"))]
186    NotFound { path: PathBuf },
187
188    #[error("'{path}' is not a directory")]
189    #[diagnostic(code(soar_utils::fs::not_a_dir), help("Provide a path to a directory"))]
190    NotADirectory { path: PathBuf },
191
192    #[diagnostic(code(soar_utils::fs::not_a_file), help("Provide a path to a file"))]
193    #[error("'{path}' is not a file")]
194    NotAFile { path: PathBuf },
195}
196
197/// Context for filesystem operations.
198pub struct IoContext {
199    path: PathBuf,
200    operation: IoOperation,
201}
202
203/// Type of filesystem operation.
204#[derive(Debug, Clone)]
205pub enum IoOperation {
206    ReadFile,
207    WriteFile,
208    CreateFile,
209    RemoveFile,
210    CreateDirectory,
211    RemoveDirectory,
212    ReadDirectory,
213    CreateSymlink { target: PathBuf },
214    RemoveSymlink,
215    ReadSymlink,
216}
217
218impl IoContext {
219    pub fn new(path: PathBuf, operation: IoOperation) -> Self {
220        Self {
221            path,
222            operation,
223        }
224    }
225
226    pub fn read_file<P: Into<PathBuf>>(path: P) -> Self {
227        Self::new(path.into(), IoOperation::ReadFile)
228    }
229
230    pub fn write_file<P: Into<PathBuf>>(path: P) -> Self {
231        Self::new(path.into(), IoOperation::WriteFile)
232    }
233
234    pub fn create_file<P: Into<PathBuf>>(path: P) -> Self {
235        Self::new(path.into(), IoOperation::CreateFile)
236    }
237
238    pub fn remove_file<P: Into<PathBuf>>(path: P) -> Self {
239        Self::new(path.into(), IoOperation::RemoveFile)
240    }
241
242    pub fn read_directory<P: Into<PathBuf>>(path: P) -> Self {
243        Self::new(path.into(), IoOperation::ReadDirectory)
244    }
245
246    pub fn create_directory<P: Into<PathBuf>>(path: P) -> Self {
247        Self::new(path.into(), IoOperation::CreateDirectory)
248    }
249
250    pub fn remove_directory<P: Into<PathBuf>>(path: P) -> Self {
251        Self::new(path.into(), IoOperation::RemoveDirectory)
252    }
253
254    pub fn read_symlink<P: Into<PathBuf>>(path: P) -> Self {
255        Self::new(path.into(), IoOperation::ReadSymlink)
256    }
257
258    pub fn create_symlink<P: Into<PathBuf>, T: Into<PathBuf>>(from: P, target: T) -> Self {
259        Self::new(
260            from.into(),
261            IoOperation::CreateSymlink {
262                target: target.into(),
263            },
264        )
265    }
266
267    pub fn remove_symlink<P: Into<PathBuf>>(path: P) -> Self {
268        Self::new(path.into(), IoOperation::RemoveSymlink)
269    }
270
271    pub fn operation(&self) -> &IoOperation {
272        &self.operation
273    }
274}
275
276impl From<(IoContext, std::io::Error)> for FileSystemError {
277    fn from((ctx, source): (IoContext, std::io::Error)) -> Self {
278        match ctx.operation {
279            IoOperation::ReadFile => {
280                FileSystemError::ReadFile {
281                    path: ctx.path,
282                    source,
283                }
284            }
285            IoOperation::WriteFile => {
286                FileSystemError::WriteFile {
287                    path: ctx.path,
288                    source,
289                }
290            }
291            IoOperation::CreateFile => {
292                FileSystemError::CreateFile {
293                    path: ctx.path,
294                    source,
295                }
296            }
297            IoOperation::RemoveFile => {
298                FileSystemError::RemoveFile {
299                    path: ctx.path,
300                    source,
301                }
302            }
303            IoOperation::CreateDirectory => {
304                FileSystemError::CreateDirectory {
305                    path: ctx.path,
306                    source,
307                }
308            }
309            IoOperation::RemoveDirectory => {
310                FileSystemError::RemoveDirectory {
311                    path: ctx.path,
312                    source,
313                }
314            }
315            IoOperation::ReadDirectory => {
316                FileSystemError::ReadDirectory {
317                    path: ctx.path,
318                    source,
319                }
320            }
321            IoOperation::CreateSymlink {
322                target,
323            } => {
324                FileSystemError::CreateSymlink {
325                    from: ctx.path,
326                    target,
327                    source,
328                }
329            }
330            IoOperation::RemoveSymlink => {
331                FileSystemError::RemoveSymlink {
332                    path: ctx.path,
333                    source,
334                }
335            }
336            IoOperation::ReadSymlink => {
337                FileSystemError::ReadSymlink {
338                    path: ctx.path,
339                    source,
340                }
341            }
342        }
343    }
344}
345
346/// Extension trait for adding path context to IO results.
347pub trait IoResultExt<T> {
348    fn with_path<P: Into<PathBuf>>(self, path: P, operation: IoOperation) -> FileSystemResult<T>;
349}
350
351impl<T> IoResultExt<T> for std::io::Result<T> {
352    fn with_path<P: Into<PathBuf>>(self, path: P, operation: IoOperation) -> FileSystemResult<T> {
353        self.map_err(|e| {
354            let ctx = IoContext::new(path.into(), operation);
355            (ctx, e).into()
356        })
357    }
358}
359
360/// Combined error type for all utils errors.
361#[derive(Error, Diagnostic, Debug)]
362pub enum UtilsError {
363    #[error(transparent)]
364    #[diagnostic(transparent)]
365    Bytes(#[from] BytesError),
366
367    #[error(transparent)]
368    #[diagnostic(transparent)]
369    Path(#[from] PathError),
370
371    #[error(transparent)]
372    #[diagnostic(transparent)]
373    FileSystem(#[from] FileSystemError),
374
375    #[error(transparent)]
376    #[diagnostic(transparent)]
377    Hash(#[from] HashError),
378}
379
380pub type BytesResult<T> = std::result::Result<T, BytesError>;
381pub type FileSystemResult<T> = std::result::Result<T, FileSystemError>;
382pub type HashResult<T> = std::result::Result<T, HashError>;
383pub type PathResult<T> = std::result::Result<T, PathError>;
384pub type UtilsResult<T> = std::result::Result<T, UtilsError>;
385
386#[cfg(test)]
387mod tests {
388    use std::io;
389
390    use super::*;
391
392    #[test]
393    fn test_bytes_error_display() {
394        let error = BytesError::ParseFailed {
395            input: "test".to_string(),
396            reason: "invalid".to_string(),
397        };
398        assert_eq!(
399            error.to_string(),
400            "Failed to parse 'test' as bytes: invalid"
401        );
402    }
403
404    #[test]
405    fn test_hash_error_display_and_source() {
406        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
407        let error = HashError::ReadFailed {
408            path: PathBuf::from("/test"),
409            source: io_error,
410        };
411        assert_eq!(error.to_string(), "Failed to read file '/test'");
412    }
413
414    #[test]
415    fn test_path_error_display() {
416        let empty_error = PathError::Empty;
417        assert_eq!(empty_error.to_string(), "Path is empty");
418
419        let missing_env_var_error = PathError::MissingEnvVar {
420            var: "VAR".to_string(),
421            input: "$VAR".to_string(),
422        };
423        assert_eq!(
424            missing_env_var_error.to_string(),
425            "Environment variable 'VAR' not set in '$VAR'"
426        );
427
428        let unclosed_variable_error = PathError::UnclosedVariable {
429            input: "${VAR".to_string(),
430        };
431        assert_eq!(
432            unclosed_variable_error.to_string(),
433            "Unclosed variable expression starting at '${VAR'"
434        );
435    }
436
437    #[test]
438    fn test_file_system_error_display() {
439        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "permission denied");
440        let file_error = FileSystemError::ReadFile {
441            path: PathBuf::from("/file"),
442            source: io_error,
443        };
444        assert_eq!(file_error.to_string(), "Failed to read file '/file'");
445
446        let not_a_dir_error = FileSystemError::NotADirectory {
447            path: PathBuf::from("/path"),
448        };
449        assert_eq!(not_a_dir_error.to_string(), "'/path' is not a directory");
450    }
451}