use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Serialize)]
pub enum ScanError {
#[error("Permission denied: {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
PermissionDenied {
#[serde(serialize_with = "crate::path_serde::serialize")]
path: PathBuf,
#[source]
#[serde(serialize_with = "serialize_io_error")]
source: std::io::Error,
},
#[error("Path not found: {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
NotFound {
#[serde(serialize_with = "crate::path_serde::serialize")]
path: PathBuf,
#[source]
#[serde(serialize_with = "serialize_io_error")]
source: std::io::Error,
},
#[error("I/O error at {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
Io {
#[serde(serialize_with = "crate::path_serde::serialize")]
path: PathBuf,
#[source]
#[serde(serialize_with = "serialize_io_error")]
source: std::io::Error,
},
#[error("Operation interrupted")]
Interrupted,
#[error("Too many errors ({count}), aborting")]
TooManyErrors { count: usize },
#[error("Scan entry limit exceeded ({limit} descendant entries)")]
EntryLimitExceeded { limit: u64 },
#[error("Invalid configuration: {}", crate::escape_terminal_text(message))]
InvalidConfig { message: String },
#[error("Root path is not a directory: {}", crate::escape_filename(.path.as_os_str()))]
NotADirectory {
#[serde(serialize_with = "crate::path_serde::serialize")]
path: PathBuf,
},
#[error("{}", crate::escape_terminal_text(message))]
Other { message: String },
}
fn serialize_io_error<S: serde::Serializer>(
error: &std::io::Error,
s: S,
) -> Result<S::Ok, S::Error> {
s.serialize_str(&error.to_string())
}
fn serialize_io_error_display(error: &std::io::Error) -> String {
crate::escape_terminal_text(&error.to_string()).into_owned()
}
impl ScanError {
pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
let path = path.into();
match source.kind() {
std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { path, source },
std::io::ErrorKind::NotFound => Self::NotFound { path, source },
_ => Self::Io { path, source },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WarningKind {
PermissionDenied,
BrokenSymlink,
ReadError,
MetadataError,
CrossFilesystem,
Omitted { count: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ScanWarning {
#[serde(with = "crate::path_serde")]
pub path: PathBuf,
pub message: String,
pub kind: WarningKind,
}
impl fmt::Display for ScanWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", crate::escape_terminal_text(&self.message))
}
}
impl ScanWarning {
pub fn represented_count(&self) -> u64 {
match self.kind {
WarningKind::Omitted { count } => count.max(1),
_ => 1,
}
}
pub fn new(path: impl Into<PathBuf>, kind: WarningKind, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
kind,
}
}
pub fn permission_denied(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
message: format!(
"Permission denied: {}",
crate::escape_filename(path.as_os_str())
),
path,
kind: WarningKind::PermissionDenied,
}
}
pub fn broken_symlink(path: impl Into<PathBuf>, target: &str) -> Self {
let path = path.into();
Self {
message: format!(
"Broken symlink: {} -> {}",
crate::escape_filename(path.as_os_str()),
crate::escape_filename(std::ffi::OsStr::new(target))
),
path,
kind: WarningKind::BrokenSymlink,
}
}
pub fn read_error(path: impl Into<PathBuf>, error: &std::io::Error) -> Self {
let path = path.into();
Self {
message: format!("Read error: {error}"),
path,
kind: WarningKind::ReadError,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn displayed_errors_escape_paths_and_messages_but_preserve_sources() {
let path = PathBuf::from("line\n\u{1b}\\n");
for kind in [
std::io::ErrorKind::PermissionDenied,
std::io::ErrorKind::NotFound,
std::io::ErrorKind::Other,
] {
let error = ScanError::io(&path, std::io::Error::new(kind, "bad\n\u{1b}source"));
let displayed = error.to_string();
let expected = if cfg!(windows) {
r"line\n\u{1b}\n"
} else {
r"line\n\u{1b}\\n"
};
assert!(displayed.contains(expected), "{displayed}");
assert!(displayed.contains(r"bad\n\u{1b}source"));
assert!(!displayed.contains(['\n', '\u{1b}']));
assert_eq!(
std::error::Error::source(&error).unwrap().to_string(),
"bad\n\u{1b}source"
);
}
for error in [
ScanError::NotADirectory { path },
ScanError::InvalidConfig {
message: "bad\nconfig".into(),
},
ScanError::Other {
message: "bad\nother".into(),
},
] {
assert!(!error.to_string().contains(['\n', '\u{1b}']));
}
let warning = ScanWarning::new("root", WarningKind::ReadError, "bad\nwarning");
assert_eq!(warning.to_string(), r"bad\nwarning");
assert_eq!(warning.message, "bad\nwarning");
}
#[test]
fn test_scan_error_io() {
let err = ScanError::io(
"/test/path",
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
);
assert!(matches!(err, ScanError::PermissionDenied { .. }));
}
#[test]
fn test_scan_error_preserves_source() {
let err = ScanError::io(
"/test/path",
std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
);
match err {
ScanError::NotFound { source, .. } => {
assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
}
_ => panic!("Expected NotFound variant"),
}
}
#[test]
fn test_scan_warning_creation() {
let warning = ScanWarning::permission_denied("/test/path");
assert_eq!(warning.kind, WarningKind::PermissionDenied);
assert!(warning.message.contains("Permission denied"));
assert_eq!(format!("{warning}"), warning.message);
}
}