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