pub mod hardlink;
pub mod hasher;
pub mod path_utils;
pub mod walker;
use std::path::PathBuf;
use std::time::SystemTime;
pub use hardlink::HardlinkTracker;
pub use hasher::{hash_to_hex, hex_to_hash, Hash, Hasher, PREHASH_SIZE};
pub use path_utils::{
is_nfc, normalize_path_str, normalize_path_str_cow, normalize_pathbuf, path_key, paths_equal,
paths_equal_normalized,
};
pub use walker::Walker;
#[derive(Debug, Clone)]
pub struct FileEntry {
pub path: PathBuf,
pub size: u64,
pub modified: SystemTime,
pub is_symlink: bool,
pub is_hardlink: bool,
}
impl FileEntry {
#[must_use]
pub fn new(path: PathBuf, size: u64, modified: SystemTime) -> Self {
Self {
path,
size,
modified,
is_symlink: false,
is_hardlink: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WalkerConfig {
pub follow_symlinks: bool,
pub skip_hidden: bool,
pub min_size: Option<u64>,
pub max_size: Option<u64>,
pub ignore_patterns: Vec<String>,
}
impl WalkerConfig {
#[must_use]
pub fn new(
follow_symlinks: bool,
skip_hidden: bool,
min_size: Option<u64>,
max_size: Option<u64>,
ignore_patterns: Vec<String>,
) -> Self {
Self {
follow_symlinks,
skip_hidden,
min_size,
max_size,
ignore_patterns,
}
}
#[must_use]
pub fn with_follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
#[must_use]
pub fn with_skip_hidden(mut self, skip: bool) -> Self {
self.skip_hidden = skip;
self
}
#[must_use]
pub fn with_min_size(mut self, size: Option<u64>) -> Self {
self.min_size = size;
self
}
#[must_use]
pub fn with_max_size(mut self, size: Option<u64>) -> Self {
self.max_size = size;
self
}
#[must_use]
pub fn with_patterns(mut self, patterns: Vec<String>) -> Self {
self.ignore_patterns = patterns;
self
}
}
#[derive(thiserror::Error, Debug)]
pub enum ScanError {
#[error("Permission denied: {0}")]
PermissionDenied(PathBuf),
#[error("Path not found: {0}")]
NotFound(PathBuf),
#[error("Not a directory: {0}")]
NotADirectory(PathBuf),
#[error("I/O error for {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[derive(thiserror::Error, Debug)]
pub enum HashError {
#[error("File not found: {0}")]
NotFound(PathBuf),
#[error("Permission denied: {0}")]
PermissionDenied(PathBuf),
#[error("I/O error for {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_entry_new() {
let entry = FileEntry::new(PathBuf::from("/test/file.txt"), 1024, SystemTime::now());
assert_eq!(entry.path, PathBuf::from("/test/file.txt"));
assert_eq!(entry.size, 1024);
assert!(!entry.is_symlink);
assert!(!entry.is_hardlink);
}
#[test]
fn test_walker_config_default() {
let config = WalkerConfig::default();
assert!(!config.follow_symlinks);
assert!(!config.skip_hidden);
assert!(config.min_size.is_none());
assert!(config.max_size.is_none());
assert!(config.ignore_patterns.is_empty());
}
#[test]
fn test_walker_config_new() {
let config = WalkerConfig::new(
true,
true,
Some(1024),
Some(1_000_000),
vec!["*.tmp".to_string()],
);
assert!(config.follow_symlinks);
assert!(config.skip_hidden);
assert_eq!(config.min_size, Some(1024));
assert_eq!(config.max_size, Some(1_000_000));
assert_eq!(config.ignore_patterns, vec!["*.tmp".to_string()]);
}
#[test]
fn test_scan_error_display() {
let err = ScanError::PermissionDenied(PathBuf::from("/test"));
assert_eq!(err.to_string(), "Permission denied: /test");
let err = ScanError::NotFound(PathBuf::from("/missing"));
assert_eq!(err.to_string(), "Path not found: /missing");
let err = ScanError::NotADirectory(PathBuf::from("/file.txt"));
assert_eq!(err.to_string(), "Not a directory: /file.txt");
}
#[test]
fn test_hash_error_display() {
let err = HashError::NotFound(PathBuf::from("/test"));
assert_eq!(err.to_string(), "File not found: /test");
let err = HashError::PermissionDenied(PathBuf::from("/secret"));
assert_eq!(err.to_string(), "Permission denied: /secret");
}
}