1use std::fmt;
2use std::io;
3use std::path::PathBuf;
4
5#[derive(Debug)]
7pub enum Error {
8 Io {
10 source: io::Error,
11 path: Option<PathBuf>,
12 },
13
14 ConfigParse {
16 source: toml::de::Error,
17 path: PathBuf,
18 },
19
20 ConfigRead { source: io::Error, path: PathBuf },
22
23 RegexCompile(regex::Error),
25
26 InvalidDate {
28 date_str: String,
29 file: PathBuf,
30 line: usize,
31 },
32
33 InvalidGlob {
35 pattern: String,
36 source: globset::Error,
37 },
38
39 InvalidArgument(String),
41}
42
43impl fmt::Display for Error {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Error::Io {
47 source,
48 path: Some(p),
49 } => {
50 write!(f, "I/O error reading '{}': {}", p.display(), source)
51 }
52 Error::Io { source, path: None } => {
53 write!(f, "I/O error: {}", source)
54 }
55 Error::ConfigParse { source, path } => {
56 write!(f, "Failed to parse config '{}': {}", path.display(), source)
57 }
58 Error::ConfigRead { source, path } => {
59 write!(f, "Failed to read config '{}': {}", path.display(), source)
60 }
61 Error::RegexCompile(e) => {
62 write!(f, "Regex compilation error: {}", e)
63 }
64 Error::InvalidDate {
65 date_str,
66 file,
67 line,
68 } => {
69 write!(
70 f,
71 "Invalid date '{}' at {}:{} (expected YYYY-MM-DD)",
72 date_str,
73 file.display(),
74 line
75 )
76 }
77 Error::InvalidGlob { pattern, source } => {
78 write!(f, "Invalid glob pattern '{}': {}", pattern, source)
79 }
80 Error::InvalidArgument(msg) => {
81 write!(f, "Invalid argument: {}", msg)
82 }
83 }
84 }
85}
86
87impl std::error::Error for Error {
88 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89 match self {
90 Error::Io { source, .. } => Some(source),
91 Error::ConfigParse { source, .. } => Some(source),
92 Error::ConfigRead { source, .. } => Some(source),
93 Error::RegexCompile(e) => Some(e),
94 Error::InvalidGlob { source, .. } => Some(source),
95 _ => None,
96 }
97 }
98}
99
100impl From<regex::Error> for Error {
101 fn from(e: regex::Error) -> Self {
102 Error::RegexCompile(e)
103 }
104}
105
106pub type Result<T> = std::result::Result<T, Error>;
108
109const MAX_FUSE_DAYS: u32 = 3_650;
114
115pub fn parse_duration_days(s: &str) -> Result<u32> {
118 let s = s.trim();
119 if let Some(num_str) = s.strip_suffix('d') {
120 let days = num_str.parse::<u32>().map_err(|_| {
121 Error::InvalidArgument(format!(
122 "'{}' is not a valid duration — expected a format like '30d'",
123 s
124 ))
125 })?;
126 if days > MAX_FUSE_DAYS {
127 return Err(Error::InvalidArgument(format!(
128 "'{}' exceeds the maximum allowed fuse window of {}d (10 years)",
129 s, MAX_FUSE_DAYS
130 )));
131 }
132 Ok(days)
133 } else {
134 Err(Error::InvalidArgument(format!(
135 "'{}' is not a valid duration — expected a format like '30d'",
136 s
137 )))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_parse_duration_days_valid() {
147 assert_eq!(parse_duration_days("30d").unwrap(), 30);
148 assert_eq!(parse_duration_days("0d").unwrap(), 0);
149 assert_eq!(parse_duration_days("365d").unwrap(), 365);
150 assert_eq!(parse_duration_days(" 14d ").unwrap(), 14);
151 }
152
153 #[test]
154 fn test_parse_duration_days_cap() {
155 assert!(parse_duration_days("3650d").is_ok());
156 assert!(parse_duration_days("3651d").is_err());
157 assert!(parse_duration_days("9999999d").is_err());
158 }
159
160 #[test]
161 fn test_parse_duration_days_invalid() {
162 assert!(parse_duration_days("30").is_err());
163 assert!(parse_duration_days("abc").is_err());
164 assert!(parse_duration_days("30h").is_err());
165 assert!(parse_duration_days("").is_err());
166 assert!(parse_duration_days("-5d").is_err());
167 }
168
169 #[test]
170 fn test_error_display_io_with_path() {
171 let err = Error::Io {
172 source: io::Error::new(io::ErrorKind::NotFound, "not found"),
173 path: Some(PathBuf::from("/some/file.rs")),
174 };
175 let msg = format!("{}", err);
176 assert!(msg.contains("/some/file.rs"));
177 assert!(msg.contains("not found"));
178 }
179
180 #[test]
181 fn test_error_display_io_without_path() {
182 let err = Error::Io {
183 source: io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
184 path: None,
185 };
186 let msg = format!("{}", err);
187 assert!(msg.contains("denied"));
188 }
189
190 #[test]
191 fn test_error_display_invalid_date() {
192 let err = Error::InvalidDate {
193 date_str: "2026-13-45".to_string(),
194 file: PathBuf::from("src/main.rs"),
195 line: 42,
196 };
197 let msg = format!("{}", err);
198 assert!(msg.contains("2026-13-45"));
199 assert!(msg.contains("src/main.rs"));
200 assert!(msg.contains("42"));
201 }
202
203 #[test]
204 fn test_error_display_invalid_argument() {
205 let err = Error::InvalidArgument("bad value".to_string());
206 let msg = format!("{}", err);
207 assert!(msg.contains("bad value"));
208 }
209}