use std::fmt;
use std::io;
use std::path::PathBuf;
#[derive(Debug)]
pub enum Error {
Io {
source: io::Error,
path: Option<PathBuf>,
},
ConfigParse {
source: toml::de::Error,
path: PathBuf,
},
ConfigRead { source: io::Error, path: PathBuf },
RegexCompile(regex::Error),
InvalidDate {
date_str: String,
file: PathBuf,
line: usize,
},
InvalidGlob {
pattern: String,
source: globset::Error,
},
InvalidArgument(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io {
source,
path: Some(p),
} => {
write!(f, "I/O error reading '{}': {}", p.display(), source)
}
Error::Io { source, path: None } => {
write!(f, "I/O error: {}", source)
}
Error::ConfigParse { source, path } => {
write!(f, "Failed to parse config '{}': {}", path.display(), source)
}
Error::ConfigRead { source, path } => {
write!(f, "Failed to read config '{}': {}", path.display(), source)
}
Error::RegexCompile(e) => {
write!(f, "Regex compilation error: {}", e)
}
Error::InvalidDate {
date_str,
file,
line,
} => {
write!(
f,
"Invalid date '{}' at {}:{} (expected YYYY-MM-DD)",
date_str,
file.display(),
line
)
}
Error::InvalidGlob { pattern, source } => {
write!(f, "Invalid glob pattern '{}': {}", pattern, source)
}
Error::InvalidArgument(msg) => {
write!(f, "Invalid argument: {}", msg)
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
Error::ConfigParse { source, .. } => Some(source),
Error::ConfigRead { source, .. } => Some(source),
Error::RegexCompile(e) => Some(e),
Error::InvalidGlob { source, .. } => Some(source),
_ => None,
}
}
}
impl From<regex::Error> for Error {
fn from(e: regex::Error) -> Self {
Error::RegexCompile(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
const MAX_FUSE_DAYS: u32 = 3_650;
pub fn parse_duration_days(s: &str) -> Result<u32> {
let s = s.trim();
if let Some(num_str) = s.strip_suffix('d') {
let days = num_str.parse::<u32>().map_err(|_| {
Error::InvalidArgument(format!(
"'{}' is not a valid duration — expected a format like '30d'",
s
))
})?;
if days > MAX_FUSE_DAYS {
return Err(Error::InvalidArgument(format!(
"'{}' exceeds the maximum allowed fuse window of {}d (10 years)",
s, MAX_FUSE_DAYS
)));
}
Ok(days)
} else {
Err(Error::InvalidArgument(format!(
"'{}' is not a valid duration — expected a format like '30d'",
s
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration_days_valid() {
assert_eq!(parse_duration_days("30d").unwrap(), 30);
assert_eq!(parse_duration_days("0d").unwrap(), 0);
assert_eq!(parse_duration_days("365d").unwrap(), 365);
assert_eq!(parse_duration_days(" 14d ").unwrap(), 14);
}
#[test]
fn test_parse_duration_days_cap() {
assert!(parse_duration_days("3650d").is_ok());
assert!(parse_duration_days("3651d").is_err());
assert!(parse_duration_days("9999999d").is_err());
}
#[test]
fn test_parse_duration_days_invalid() {
assert!(parse_duration_days("30").is_err());
assert!(parse_duration_days("abc").is_err());
assert!(parse_duration_days("30h").is_err());
assert!(parse_duration_days("").is_err());
assert!(parse_duration_days("-5d").is_err());
}
#[test]
fn test_error_display_io_with_path() {
let err = Error::Io {
source: io::Error::new(io::ErrorKind::NotFound, "not found"),
path: Some(PathBuf::from("/some/file.rs")),
};
let msg = format!("{}", err);
assert!(msg.contains("/some/file.rs"));
assert!(msg.contains("not found"));
}
#[test]
fn test_error_display_io_without_path() {
let err = Error::Io {
source: io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
path: None,
};
let msg = format!("{}", err);
assert!(msg.contains("denied"));
}
#[test]
fn test_error_display_invalid_date() {
let err = Error::InvalidDate {
date_str: "2026-13-45".to_string(),
file: PathBuf::from("src/main.rs"),
line: 42,
};
let msg = format!("{}", err);
assert!(msg.contains("2026-13-45"));
assert!(msg.contains("src/main.rs"));
assert!(msg.contains("42"));
}
#[test]
fn test_error_display_invalid_argument() {
let err = Error::InvalidArgument("bad value".to_string());
let msg = format!("{}", err);
assert!(msg.contains("bad value"));
}
}