acme_disk_use/
error.rs

1//! Error handling module with contextual error messages
2
3use std::{fmt, io, path::PathBuf};
4
5/// Custom error type that wraps IO errors with additional context
6#[derive(Debug)]
7pub enum DiskUseError {
8    /// Error occurred while scanning a directory
9    ScanError { path: PathBuf, source: io::Error },
10    /// Error occurred while reading metadata
11    MetadataError { path: PathBuf, source: io::Error },
12    /// Error occurred while reading cache file
13    CacheReadError { path: PathBuf, source: io::Error },
14    /// Error occurred while writing cache file
15    CacheWriteError { path: PathBuf, source: io::Error },
16    /// Error occurred while serializing/deserializing cache
17    CacheSerializationError { path: PathBuf, message: String },
18    /// The specified path does not exist
19    PathNotFound { path: PathBuf },
20    /// The specified path is not accessible due to permissions
21    PermissionDenied { path: PathBuf },
22}
23
24impl fmt::Display for DiskUseError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            DiskUseError::ScanError { path, source } => {
28                write!(
29                    f,
30                    "Failed to scan directory '{}': {}",
31                    path.display(),
32                    get_user_friendly_error(source)
33                )
34            }
35            DiskUseError::MetadataError { path, source } => {
36                write!(
37                    f,
38                    "Failed to read metadata for '{}': {}",
39                    path.display(),
40                    get_user_friendly_error(source)
41                )
42            }
43            DiskUseError::CacheReadError { path, source } => {
44                write!(
45                    f,
46                    "Failed to read cache file '{}': {}",
47                    path.display(),
48                    get_user_friendly_error(source)
49                )
50            }
51            DiskUseError::CacheWriteError { path, source } => {
52                write!(
53                    f,
54                    "Failed to write cache file '{}': {}",
55                    path.display(),
56                    get_user_friendly_error(source)
57                )
58            }
59            DiskUseError::CacheSerializationError { path, message } => {
60                write!(
61                    f,
62                    "Failed to serialize/deserialize cache file '{}': {}",
63                    path.display(),
64                    message
65                )
66            }
67            DiskUseError::PathNotFound { path } => {
68                write!(f, "Path '{}' does not exist", path.display())
69            }
70            DiskUseError::PermissionDenied { path } => {
71                write!(f, "Permission denied when accessing '{}'", path.display())
72            }
73        }
74    }
75}
76
77impl std::error::Error for DiskUseError {
78    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79        match self {
80            DiskUseError::ScanError { source, .. }
81            | DiskUseError::MetadataError { source, .. }
82            | DiskUseError::CacheReadError { source, .. }
83            | DiskUseError::CacheWriteError { source, .. } => Some(source),
84            _ => None,
85        }
86    }
87}
88
89/// Convert DiskUseError to io::Error
90impl From<DiskUseError> for io::Error {
91    fn from(err: DiskUseError) -> io::Error {
92        let err_string = err.to_string();
93        match err {
94            DiskUseError::PathNotFound { .. } => {
95                io::Error::new(io::ErrorKind::NotFound, err_string)
96            }
97            DiskUseError::PermissionDenied { .. } => {
98                io::Error::new(io::ErrorKind::PermissionDenied, err_string)
99            }
100            DiskUseError::ScanError { source, .. }
101            | DiskUseError::MetadataError { source, .. }
102            | DiskUseError::CacheReadError { source, .. }
103            | DiskUseError::CacheWriteError { source, .. } => {
104                io::Error::new(source.kind(), err_string)
105            }
106            DiskUseError::CacheSerializationError { .. } => {
107                io::Error::new(io::ErrorKind::InvalidData, err_string)
108            }
109        }
110    }
111}
112
113/// Provide user-friendly error messages for common IO error kinds
114fn get_user_friendly_error(err: &io::Error) -> String {
115    match err.kind() {
116        io::ErrorKind::NotFound => "The path does not exist".to_string(),
117        io::ErrorKind::PermissionDenied => {
118            "Permission denied. You may need elevated privileges to access this location."
119                .to_string()
120        }
121        io::ErrorKind::InvalidInput => "Invalid path or filename".to_string(),
122        io::ErrorKind::OutOfMemory => "Out of memory".to_string(),
123        io::ErrorKind::StorageFull => "Disk quota exceeded or insufficient disk space".to_string(),
124        _ => {
125            // Check for specific error messages in the error string
126            let err_str = err.to_string().to_lowercase();
127            if err_str.contains("quota") || err_str.contains("disk quota") {
128                "Disk quota exceeded. You have reached your storage limit.".to_string()
129            } else if err_str.contains("no space") || err_str.contains("nospc") {
130                "Insufficient disk space available".to_string()
131            } else if err_str.contains("read-only") {
132                "The filesystem is read-only".to_string()
133            } else if err_str.contains("device") {
134                "Device is not available or not ready".to_string()
135            } else if err_str.contains("stale") {
136                "Stale file handle (remote filesystem may be unavailable)".to_string()
137            } else {
138                format!("{}", err)
139            }
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_error_display() {
150        let err = DiskUseError::PathNotFound {
151            path: PathBuf::from("/nonexistent"),
152        };
153        assert_eq!(err.to_string(), "Path '/nonexistent' does not exist");
154
155        let err = DiskUseError::PermissionDenied {
156            path: PathBuf::from("/root/secret"),
157        };
158        assert_eq!(
159            err.to_string(),
160            "Permission denied when accessing '/root/secret'"
161        );
162    }
163
164    #[test]
165    fn test_user_friendly_errors() {
166        let err = io::Error::new(io::ErrorKind::NotFound, "test");
167        assert_eq!(get_user_friendly_error(&err), "The path does not exist");
168
169        let err = io::Error::new(io::ErrorKind::PermissionDenied, "test");
170        assert!(get_user_friendly_error(&err).contains("Permission denied"));
171    }
172
173    #[test]
174    fn test_disk_quota_detection() {
175        let err = io::Error::other("Disk quota exceeded");
176        let msg = get_user_friendly_error(&err);
177        assert!(
178            msg.contains("quota") || msg.contains("storage limit"),
179            "Expected quota message, got: {}",
180            msg
181        );
182
183        let err = io::Error::other("No space left on device");
184        let msg = get_user_friendly_error(&err);
185        assert!(
186            msg.contains("space") || msg.contains("disk"),
187            "Expected space message, got: {}",
188            msg
189        );
190
191        // Test StorageFull error kind
192        let err = io::Error::new(io::ErrorKind::StorageFull, "storage full");
193        let msg = get_user_friendly_error(&err);
194        assert!(
195            msg.contains("quota") || msg.contains("space"),
196            "Expected quota/space message for StorageFull, got: {}",
197            msg
198        );
199    }
200}