anyfs_backend/error.rs
1//! # Error Types
2//!
3//! Comprehensive error handling for AnyFS filesystem operations.
4//!
5//! ## Overview
6//!
7//! All AnyFS operations return `Result<T, FsError>`. The [`FsError`] enum provides
8//! detailed, contextual error variants that include:
9//!
10//! - **Path information** — Which file/directory caused the error
11//! - **Operation context** — What operation was attempted
12//! - **Specific details** — Quota limits, invalid data descriptions, etc.
13//!
14//! ## Error Categories
15//!
16//! | Category | Variants | Description |
17//! |----------|----------|-------------|
18//! | Path/File | `NotFound`, `AlreadyExists`, `NotAFile`, `NotADirectory` | Path existence and type errors |
19//! | Permission | `PermissionDenied`, `AccessDenied`, `ReadOnly` | Access control errors |
20//! | Resource | `QuotaExceeded`, `FileSizeExceeded`, `RateLimitExceeded` | Limit violations |
21//! | Data | `InvalidData`, `CorruptedData`, `IntegrityError` | Content problems |
22//! | Operation | `NotSupported`, `Conflict`, `Backend` | Backend/operation failures |
23//!
24//! ## Quick Example
25//!
26//! ```rust
27//! use anyfs_backend::FsError;
28//! use std::path::PathBuf;
29//!
30//! // Errors include the path that caused the problem
31//! let err = FsError::NotFound { path: PathBuf::from("/missing.txt") };
32//! assert!(err.to_string().contains("/missing.txt"));
33//!
34//! // Permission errors include the operation
35//! let err = FsError::PermissionDenied {
36//! path: PathBuf::from("/secret"),
37//! operation: "read",
38//! };
39//! assert!(err.to_string().contains("read"));
40//! ```
41//!
42//! ## Conversion from std::io::Error
43//!
44//! [`FsError`] implements `From<std::io::Error>` for easy interoperability:
45//!
46//! ```rust
47//! use anyfs_backend::FsError;
48//! use std::io::{Error, ErrorKind};
49//!
50//! let io_err = Error::new(ErrorKind::NotFound, "file not found");
51//! let fs_err: FsError = io_err.into();
52//! assert!(matches!(fs_err, FsError::NotFound { .. }));
53//! ```
54
55use std::path::PathBuf;
56
57/// Comprehensive filesystem error type.
58///
59/// All AnyFS operations return `Result<T, FsError>`. Each variant includes
60/// relevant context (paths, operations, limits) to make debugging easier.
61///
62/// # Non-Exhaustive
63///
64/// This enum is marked `#[non_exhaustive]`, meaning new variants may be added
65/// in future versions without breaking changes. Always include a wildcard arm
66/// when pattern matching:
67///
68/// ```rust
69/// use anyfs_backend::FsError;
70/// use std::path::PathBuf;
71///
72/// fn handle_error(err: FsError) {
73/// match err {
74/// FsError::NotFound { path } => println!("Not found: {}", path.display()),
75/// FsError::PermissionDenied { path, operation } => {
76/// println!("Permission denied for {} on {}", operation, path.display())
77/// }
78/// other => println!("Other error: {}", other),
79/// }
80/// }
81/// ```
82///
83/// # Display Format
84///
85/// All variants implement `Display` with human-readable messages:
86///
87/// ```rust
88/// use anyfs_backend::FsError;
89/// use std::path::PathBuf;
90///
91/// let err = FsError::QuotaExceeded { limit: 100, requested: 50, usage: 80 };
92/// let msg = err.to_string();
93/// assert!(msg.contains("100") && msg.contains("50") && msg.contains("80"));
94/// ```
95///
96/// # Error Source Chain
97///
98/// The [`Io`](FsError::Io) variant wraps `std::io::Error` with the `#[source]`
99/// attribute, enabling error chain traversal via `std::error::Error::source()`.
100#[non_exhaustive]
101#[derive(Debug, thiserror::Error)]
102pub enum FsError {
103 // Path/File Errors
104 /// Path does not exist.
105 #[error("not found: {path}")]
106 NotFound {
107 /// The path that was not found.
108 path: PathBuf,
109 },
110
111 /// A threat was detected (e.g., path traversal, malicious content).
112 #[error("threat detected: {reason} in {path}")]
113 ThreatDetected {
114 /// The path where the threat was detected.
115 path: PathBuf,
116 /// Description of the threat.
117 reason: String,
118 },
119
120 /// Path already exists when it shouldn't.
121 #[error("{operation}: already exists: {path}")]
122 AlreadyExists {
123 /// The path that already exists.
124 path: PathBuf,
125 /// The operation that failed.
126 operation: &'static str,
127 },
128
129 /// Expected a file but found something else.
130 #[error("not a file: {path}")]
131 NotAFile {
132 /// The path that is not a file.
133 path: PathBuf,
134 },
135
136 /// Expected a directory but found something else.
137 #[error("not a directory: {path}")]
138 NotADirectory {
139 /// The path that is not a directory.
140 path: PathBuf,
141 },
142
143 /// Directory is not empty when it should be.
144 #[error("directory not empty: {path}")]
145 DirectoryNotEmpty {
146 /// The path to the non-empty directory.
147 path: PathBuf,
148 },
149
150 /// Inode does not exist.
151 #[error("inode not found: {inode}")]
152 InodeNotFound {
153 /// The inode number that was not found.
154 inode: u64,
155 },
156
157 /// File handle is invalid or closed.
158 #[error("invalid handle: {}", handle.0)]
159 InvalidHandle {
160 /// The invalid handle.
161 handle: crate::Handle,
162 },
163
164 /// Extended attribute not found.
165 #[error("xattr not found: {name} on {path}")]
166 XattrNotFound {
167 /// The path where the xattr was not found.
168 path: PathBuf,
169 /// The attribute name that was not found.
170 name: String,
171 },
172
173 // Permission/Access Errors
174 /// Permission denied for operation.
175 #[error("{operation}: permission denied: {path}")]
176 PermissionDenied {
177 /// The path where permission was denied.
178 path: PathBuf,
179 /// The operation that was denied.
180 operation: &'static str,
181 },
182
183 /// Access denied with reason.
184 #[error("access denied: {path} ({reason})")]
185 AccessDenied {
186 /// The path where access was denied.
187 path: PathBuf,
188 /// The reason for denial.
189 reason: String,
190 },
191
192 /// Filesystem is read-only.
193 #[error("read-only filesystem: {operation}")]
194 ReadOnly {
195 /// The operation that was attempted.
196 operation: &'static str,
197 },
198
199 /// Feature is not enabled.
200 #[error("{operation}: feature not enabled: {feature}")]
201 FeatureNotEnabled {
202 /// The feature that is not enabled.
203 feature: &'static str,
204 /// The operation that requires the feature.
205 operation: &'static str,
206 },
207
208 // Resource Limit Errors
209 /// Quota exceeded.
210 #[error("quota exceeded: limit {limit}, requested {requested}, usage {usage}")]
211 QuotaExceeded {
212 /// The quota limit.
213 limit: u64,
214 /// The amount requested.
215 requested: u64,
216 /// The current usage.
217 usage: u64,
218 },
219
220 /// File size limit exceeded.
221 #[error("file size exceeded: {path} ({size} > {limit})")]
222 FileSizeExceeded {
223 /// The path to the file.
224 path: PathBuf,
225 /// The actual size.
226 size: u64,
227 /// The size limit.
228 limit: u64,
229 },
230
231 /// Rate limit exceeded.
232 #[error("rate limit exceeded: {limit}/s (window: {window_secs}s)")]
233 RateLimitExceeded {
234 /// The rate limit.
235 limit: u32,
236 /// The time window in seconds.
237 window_secs: u64,
238 },
239
240 // Data Errors
241 /// Invalid data encountered.
242 #[error("invalid data: {path} ({details})")]
243 InvalidData {
244 /// The path with invalid data.
245 path: PathBuf,
246 /// Details about the invalid data.
247 details: String,
248 },
249
250 /// Corrupted data detected.
251 #[error("corrupted data: {path} ({details})")]
252 CorruptedData {
253 /// The path with corrupted data.
254 path: PathBuf,
255 /// Details about the corruption.
256 details: String,
257 },
258
259 /// Data integrity check failed.
260 #[error("integrity error: {path}")]
261 IntegrityError {
262 /// The path that failed integrity check.
263 path: PathBuf,
264 },
265
266 /// Serialization error.
267 #[error("serialization error: {0}")]
268 Serialization(String),
269
270 /// Deserialization error.
271 #[error("deserialization error: {0}")]
272 Deserialization(String),
273
274 // Backend/Operation Errors
275 /// Operation is not supported.
276 #[error("operation not supported: {operation}")]
277 NotSupported {
278 /// The unsupported operation.
279 operation: &'static str,
280 },
281
282 /// Invalid password provided.
283 #[error("invalid password")]
284 InvalidPassword,
285
286 /// Conflict detected (e.g., concurrent modification).
287 #[error("conflict: {path}")]
288 Conflict {
289 /// The path with a conflict.
290 path: PathBuf,
291 },
292
293 /// Generic backend error.
294 #[error("backend error: {0}")]
295 Backend(String),
296
297 /// I/O error with context.
298 #[error("{operation} failed for {path}: {source}")]
299 Io {
300 /// The operation that failed.
301 operation: &'static str,
302 /// The path involved in the operation.
303 path: PathBuf,
304 /// The underlying I/O error.
305 #[source]
306 source: std::io::Error,
307 },
308}
309
310impl From<std::io::Error> for FsError {
311 fn from(error: std::io::Error) -> Self {
312 // Convert common io::ErrorKind to more specific FsError variants when possible
313 match error.kind() {
314 std::io::ErrorKind::NotFound => FsError::NotFound {
315 path: PathBuf::new(),
316 },
317 std::io::ErrorKind::PermissionDenied => FsError::PermissionDenied {
318 path: PathBuf::new(),
319 operation: "io",
320 },
321 std::io::ErrorKind::AlreadyExists => FsError::AlreadyExists {
322 path: PathBuf::new(),
323 operation: "io",
324 },
325 _ => FsError::Io {
326 operation: "io",
327 path: PathBuf::new(),
328 source: error,
329 },
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn fs_error_not_found_display() {
340 let err = FsError::NotFound {
341 path: PathBuf::from("/missing"),
342 };
343 assert_eq!(err.to_string(), "not found: /missing");
344 }
345
346 #[test]
347 fn fs_error_already_exists_display() {
348 let err = FsError::AlreadyExists {
349 path: PathBuf::from("/exists"),
350 operation: "create",
351 };
352 assert_eq!(err.to_string(), "create: already exists: /exists");
353 }
354
355 #[test]
356 fn fs_error_quota_exceeded_display() {
357 let err = FsError::QuotaExceeded {
358 limit: 100,
359 requested: 50,
360 usage: 80,
361 };
362 assert!(err.to_string().contains("100"));
363 assert!(err.to_string().contains("50"));
364 assert!(err.to_string().contains("80"));
365 }
366
367 #[test]
368 fn fs_error_from_io_not_found() {
369 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
370 let fs_err = FsError::from(io_err);
371 assert!(matches!(fs_err, FsError::NotFound { .. }));
372 }
373
374 #[test]
375 fn fs_error_from_io_permission_denied() {
376 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test");
377 let fs_err = FsError::from(io_err);
378 assert!(matches!(fs_err, FsError::PermissionDenied { .. }));
379 }
380
381 #[test]
382 fn fs_error_from_io_already_exists() {
383 let io_err = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "test");
384 let fs_err = FsError::from(io_err);
385 assert!(matches!(fs_err, FsError::AlreadyExists { .. }));
386 }
387
388 #[test]
389 fn fs_error_from_io_other() {
390 let io_err = std::io::Error::new(std::io::ErrorKind::Other, "test");
391 let fs_err = FsError::from(io_err);
392 assert!(matches!(fs_err, FsError::Io { .. }));
393 }
394}