1#![expect(unused_assignments)]
6
7use std::path::PathBuf;
8
9use miette::Diagnostic;
10use thiserror::Error;
11
12#[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#[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#[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#[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
197pub struct IoContext {
199 path: PathBuf,
200 operation: IoOperation,
201}
202
203#[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
346pub 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#[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}