Skip to main content

gravityfile_core/
error.rs

1//! Error types for scanning operations.
2
3use std::fmt;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9/// Errors that can occur during scanning.
10#[derive(Debug, Error, Serialize)]
11pub enum ScanError {
12    /// Permission denied for a path.
13    #[error("Permission denied: {path}: {}", serialize_io_error_display(source))]
14    PermissionDenied {
15        path: PathBuf,
16        #[source]
17        #[serde(serialize_with = "serialize_io_error")]
18        source: std::io::Error,
19    },
20
21    /// Path not found.
22    #[error("Path not found: {path}: {}", serialize_io_error_display(source))]
23    NotFound {
24        path: PathBuf,
25        #[source]
26        #[serde(serialize_with = "serialize_io_error")]
27        source: std::io::Error,
28    },
29
30    /// Generic I/O error.
31    #[error("I/O error at {path}: {source}")]
32    Io {
33        path: PathBuf,
34        #[source]
35        #[serde(serialize_with = "serialize_io_error")]
36        source: std::io::Error,
37    },
38
39    /// Operation was interrupted.
40    #[error("Operation interrupted")]
41    Interrupted,
42
43    /// Too many errors occurred.
44    #[error("Too many errors ({count}), aborting")]
45    TooManyErrors { count: usize },
46
47    /// Invalid configuration.
48    #[error("Invalid configuration: {message}")]
49    InvalidConfig { message: String },
50
51    /// Root path is not a directory.
52    #[error("Root path is not a directory: {path}")]
53    NotADirectory { path: PathBuf },
54
55    /// Other error.
56    #[error("{message}")]
57    Other { message: String },
58}
59
60fn serialize_io_error<S: serde::Serializer>(
61    error: &std::io::Error,
62    s: S,
63) -> Result<S::Ok, S::Error> {
64    s.serialize_str(&error.to_string())
65}
66
67fn serialize_io_error_display(error: &std::io::Error) -> String {
68    error.to_string()
69}
70
71impl ScanError {
72    /// Create an I/O error with path context. Preserves the original `io::Error` as the source.
73    pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
74        let path = path.into();
75        match source.kind() {
76            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { path, source },
77            std::io::ErrorKind::NotFound => Self::NotFound { path, source },
78            _ => Self::Io { path, source },
79        }
80    }
81}
82
83/// Kind of scan warning.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum WarningKind {
86    /// Permission was denied.
87    PermissionDenied,
88    /// Symbolic link target does not exist.
89    BrokenSymlink,
90    /// Error reading file/directory.
91    ReadError,
92    /// Error reading metadata.
93    MetadataError,
94    /// Filesystem boundary crossed (when not allowed).
95    CrossFilesystem,
96}
97
98/// Non-fatal warning encountered during scan.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ScanWarning {
101    /// Path where the warning occurred.
102    pub path: PathBuf,
103    /// Human-readable message.
104    pub message: String,
105    /// Kind of warning.
106    pub kind: WarningKind,
107}
108
109impl fmt::Display for ScanWarning {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.message)
112    }
113}
114
115impl ScanWarning {
116    /// Create a new scan warning.
117    pub fn new(path: impl Into<PathBuf>, kind: WarningKind, message: impl Into<String>) -> Self {
118        Self {
119            path: path.into(),
120            message: message.into(),
121            kind,
122        }
123    }
124
125    /// Create a permission denied warning.
126    pub fn permission_denied(path: impl Into<PathBuf>) -> Self {
127        let path = path.into();
128        Self {
129            message: format!("Permission denied: {}", path.display()),
130            path,
131            kind: WarningKind::PermissionDenied,
132        }
133    }
134
135    /// Create a broken symlink warning.
136    pub fn broken_symlink(path: impl Into<PathBuf>, target: &str) -> Self {
137        let path = path.into();
138        Self {
139            message: format!("Broken symlink: {} -> {target}", path.display()),
140            path,
141            kind: WarningKind::BrokenSymlink,
142        }
143    }
144
145    /// Create a read error warning.
146    pub fn read_error(path: impl Into<PathBuf>, error: &std::io::Error) -> Self {
147        let path = path.into();
148        Self {
149            message: format!("Read error: {error}"),
150            path,
151            kind: WarningKind::ReadError,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_scan_error_io() {
162        let err = ScanError::io(
163            "/test/path",
164            std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
165        );
166        assert!(matches!(err, ScanError::PermissionDenied { .. }));
167    }
168
169    #[test]
170    fn test_scan_error_preserves_source() {
171        let err = ScanError::io(
172            "/test/path",
173            std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
174        );
175        match err {
176            ScanError::NotFound { source, .. } => {
177                assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
178            }
179            _ => panic!("Expected NotFound variant"),
180        }
181    }
182
183    #[test]
184    fn test_scan_warning_creation() {
185        let warning = ScanWarning::permission_denied("/test/path");
186        assert_eq!(warning.kind, WarningKind::PermissionDenied);
187        assert!(warning.message.contains("Permission denied"));
188        assert_eq!(format!("{warning}"), warning.message);
189    }
190}