#![allow(unused_assignments)]
mod clipboard;
mod config;
pub use self::clipboard::ClipboardError;
pub use self::config::ConfigError;
#[cfg(feature = "cli")]
use miette::Diagnostic;
use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
#[cfg_attr(feature = "cli", derive(Diagnostic))]
pub enum Error {
#[error("IO error: {0}")]
#[cfg_attr(
feature = "cli",
diagnostic(help("Check that the file exists and you have permission to access it"))
)]
Io(#[from] std::io::Error),
#[error("Git command failed: {command}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::git::command_failed)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Ensure git is installed and you're in a git repository"))
)]
GitCommandFailed {
command: String,
stderr: String,
source: std::io::Error,
suggestion: String,
},
#[error("Not in a git repository: {message}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::git::not_in_repo)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Run this command from within a git repository or omit the --git flag"))
)]
NotInGitRepository {
message: String,
stderr: String,
suggestion: String,
},
#[error("Invalid UTF-8 in git output")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::git::invalid_utf8)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Check that your repository paths contain valid UTF-8 characters"))
)]
GitInvalidUtf8 {
#[source]
source: std::string::FromUtf8Error,
suggestion: String,
},
#[error("Invalid path: {path}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::path::invalid)))]
#[cfg_attr(
feature = "cli",
diagnostic(help(
"Paths must be valid UTF-8, not contain null bytes, and be under 4096 characters"
))
)]
InvalidPath {
path: PathBuf,
},
#[error("File not found: {path}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_found)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Check the path and ensure the file exists"))
)]
FileNotFound {
path: PathBuf,
},
#[error("Not a file: {path}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_a_file)))]
#[cfg_attr(
feature = "cli",
diagnostic(help(
"The path exists but is not a regular file. Use directory walk mode for directories."
))
)]
NotAFile {
path: PathBuf,
},
#[error("Not a regular file (refusing to open): {path}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_regular_file)))]
#[cfg_attr(
feature = "cli",
diagnostic(help(
"Only regular files can be processed. Directories and special files are skipped."
))
)]
NotARegularFile {
path: PathBuf,
},
#[error("Configuration error: {message}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid)))]
Config {
message: String,
},
#[error("Configuration file error: {0}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::config::file_error)))]
ConfigFile(#[from] ConfigError),
#[error("Walker error: {message}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::walker::error)))]
Walker {
message: String,
},
#[error("All {count} specified file(s) were invalid")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::all_files_invalid)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Check that all file paths are valid and accessible"))
)]
AllFilesInvalid {
count: usize,
},
#[error("Printer error: {message}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::printer::error)))]
Printer {
message: String,
},
#[error("Clipboard error: {0}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::clipboard::error)))]
#[cfg_attr(
feature = "cli",
diagnostic(help(
"Check that a clipboard is available (requires display server on Linux)"
))
)]
Clipboard(#[from] ClipboardError),
#[error("File too large: {path} ({size} bytes exceeds {max_size} bytes)")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::file_too_large)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Process smaller files or increase MAX_FILE_SIZE in the configuration"))
)]
FileTooLarge {
path: PathBuf,
size: u64,
max_size: u64,
},
#[error("File is not valid UTF-8 text: {path}")]
#[cfg_attr(feature = "cli", diagnostic(code(luff::fs::invalid_utf8)))]
#[cfg_attr(
feature = "cli",
diagnostic(help("Only UTF-8 text files can be processed. Binary files are skipped."))
)]
InvalidUtf8 {
path: PathBuf,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::NotInGitRepository {
message: "test error".to_string(),
stderr: "git error output".to_string(),
suggestion: "run git init".to_string(),
};
assert!(err.to_string().contains("Not in a git repository"));
assert!(err.to_string().contains("test error"));
}
#[test]
fn test_file_not_found() {
let path = PathBuf::from("/nonexistent");
let err = Error::FileNotFound { path };
assert!(err.to_string().contains("/nonexistent"));
}
#[test]
fn test_error_from_config_error() {
let config_err = ConfigError::InvalidPattern {
pattern: "*.png".to_string(),
pattern_type: "extension".to_string(),
help: "No glob characters allowed".to_string(),
};
let err = Error::ConfigFile(config_err);
assert!(err.to_string().contains("Invalid extension pattern"));
}
#[test]
fn test_error_from_clipboard_error() {
let clip_err = ClipboardError::Timeout;
let err = Error::from(clip_err);
assert!(err.to_string().contains("timed out"));
}
#[test]
fn test_clipboard_error_preserves_structure() {
let clip_err = ClipboardError::SizeExceeded {
content_len: 2_000_000,
limit: 1_000_000,
};
let err = Error::from(clip_err);
match &err {
Error::Clipboard(inner) => {
assert!(matches!(
inner,
ClipboardError::SizeExceeded {
content_len: 2_000_000,
limit: 1_000_000,
}
));
}
_ => panic!("Expected Error::Clipboard, got {err:?}"),
}
}
#[test]
fn test_all_files_invalid_error() {
let err = Error::AllFilesInvalid { count: 5 };
assert!(
err.to_string()
.contains("All 5 specified file(s) were invalid")
);
}
#[test]
fn test_git_command_failed_fields() {
let err = Error::GitCommandFailed {
command: "git rev-parse".to_string(),
stderr: "fatal: not a git repository".to_string(),
source: std::io::Error::other("git not found"),
suggestion: "install git".to_string(),
};
match err {
Error::GitCommandFailed {
command,
stderr,
source: _,
suggestion,
} => {
assert_eq!(command, "git rev-parse");
assert!(stderr.contains("not a git repository"));
assert_eq!(suggestion, "install git");
}
_ => panic!("Unexpected error variant"),
}
}
#[test]
fn test_not_in_git_repository_fields() {
let err = Error::NotInGitRepository {
message: "no git repo".to_string(),
stderr: "fatal: not a git repository".to_string(),
suggestion: "run git init".to_string(),
};
match err {
Error::NotInGitRepository {
message,
stderr,
suggestion,
} => {
assert_eq!(message, "no git repo");
assert!(stderr.contains("not a git repository"));
assert_eq!(suggestion, "run git init");
}
_ => panic!("Unexpected error variant"),
}
}
#[test]
fn test_git_invalid_utf8_fields() {
let utf8_error = String::from_utf8(vec![0xff]).unwrap_err();
let err = Error::GitInvalidUtf8 {
source: utf8_error,
suggestion: "check git output encoding".to_string(),
};
match err {
Error::GitInvalidUtf8 {
source: _,
suggestion,
} => {
assert_eq!(suggestion, "check git output encoding");
}
_ => panic!("Unexpected error variant"),
}
}
#[test]
fn test_file_too_large_fields() {
let err = Error::FileTooLarge {
path: PathBuf::from("/tmp/large.bin"),
size: 200 * 1024 * 1024,
max_size: 100 * 1024 * 1024,
};
match err {
Error::FileTooLarge {
path,
size,
max_size,
} => {
assert_eq!(path.to_string_lossy(), "/tmp/large.bin");
assert_eq!(size, 200 * 1024 * 1024);
assert_eq!(max_size, 100 * 1024 * 1024);
}
_ => panic!("Unexpected error variant"),
}
}
}