use crate::config::Config; use crate::cli::Platform; use glob::glob; use std::collections::HashSet; use std::path::{Path, PathBuf}; use walkdir::WalkDir; use sha2::{Sha256, Digest}; use std::fs; use std::io::{self, Read}; use anyhow::{Context, Result}; #[derive(Debug, Clone)] pub struct DetectedDotfile { pub path: PathBuf, pub content_hash: Option, pub permissions: Option, pub size: u64, pub platform: Platform, } impl DetectedDotfile { pub fn new(path: PathBuf) -> Self { let (hash, perms, size) = Self::get_file_info(&path); Self { path, content_hash: hash, permissions: perms, size, platform: Platform::current(), } } fn get_file_info(path: &PathBuf) -> (Option, Option, u64) { let metadata = match fs::metadata(path) { Ok(m) => m, Err(_) => return (None, None, 0), }; let size = metadata.len(); let perms = if metadata.permissions().readonly() { Some(0o444) } else { Some(0o644) }; let mut file = match fs::File::open(path) { Ok(f) => f, Err(_) => return (None, perms, size), }; let mut hasher = Sha256::new(); let mut buffer = Vec::new(); match file.read_to_end(&mut buffer) { Ok(_) => { hasher.update(&buffer); let hash = format!("{:x}", hasher.finalize()); (Some(hash), perms, size) } Err(_) => (None, perms, size), } } } pub struct Detector<'a> { config: &'a Config, home_dir: PathBuf, platform: Platform, } impl<'a> Detector<'a> { pub fn new(config: &'a Config) -> Self { let home_dir = dirs::home_dir() .expect("Home directory not found"); let platform = Platform::current(); Self { config, home_dir, platform, } } pub fn detect_dotfiles(&self, include_system: bool) -> Result> { let mut detected: HashSet = HashSet::new(); if include_system { self.scan_system_directories(&mut detected) .context("Failed to scan system directories")?; } self.scan_home_directory(&mut detected) .context("Failed to scan home directory")?; self.scan_config_patterns(&mut detected) .context("Failed to scan config patterns")?; let mut result: Vec = detected .into_iter() .filter(|p| self.should_include(p)) .map(DetectedDotfile::new) .collect(); result.sort_by(|a, b| a.path.cmp(&b.path)); Ok(result) } fn scan_home_directory(&self, detected: &mut HashSet) -> Result<()> { let walker = WalkDir::new(&self.home_dir) .max_depth(self.config.detect.scan_depth) .follow_links(false); for entry in walker { let entry = entry?; let path = entry.path(); if path.is_file() { let relative = self.get_relative_path(path); if self.is_hidden_or_config(&relative) { detected.insert(path.to_path_buf()); } } } Ok(()) } fn scan_system_directories(&self, detected: &mut HashSet) -> Result<()> { let system_dirs: Vec = match self.platform { Platform::Linux | Platform::Wsl => vec![ PathBuf::from("/etc"), PathBuf::from("/usr/local"), ], Platform::Macos => vec![ PathBuf::from("/etc"), PathBuf::from("/usr/local"), PathBuf::from("/Library/Application Support"), ], }; for dir in &system_dirs { if dir.exists() { let walker = WalkDir::new(dir) .max_depth(3) .follow_links(false); for entry in walker { let entry = entry?; let path = entry.path(); if path.is_file() { let relative = path.strip_prefix("/") .unwrap_or_else(|| path) .to_path_buf(); if self.is_hidden_or_config(&relative) { detected.insert(path.to_path_buf()); } } } } } Ok(()) } fn scan_config_patterns(&self, detected: &mut HashSet) -> Result<()> { for pattern in &self.config.detect.patterns { let full_pattern = self.home_dir.join(pattern); if let Some(pattern_str) = full_pattern.to_str() { for entry in glob(pattern_str)? { match entry { Ok(path) => { if path.is_file() { detected.insert(path); } } Err(e) => { log::debug!("Glob pattern error: {}", e); } } } } } Ok(()) } fn is_hidden_or_config(&self, path: &Path) -> bool { if let Some(name) = path.file_name() { if name.to_string_lossy().starts_with('.') { return true; } } let path_str = path.to_string_lossy(); if path_str.contains(".config/") || path_str.contains(".local/") { return true; } if self.platform == Platform::Macos { if path_str.contains("Library/Application Support/") { return true; } } false } fn get_relative_path(&self, path: &Path) -> PathBuf { if let Ok(rel) = path.strip_prefix(&self.home_dir) { rel.to_path_buf() } else { path.to_path_buf() } } fn should_include(&self, path: &Path) -> bool { let path_str = path.to_string_lossy(); for exclude in &self.config.detect.exclude_patterns { if path_str.contains(exclude) { return false; } } true } } impl Platform { pub fn current() -> Self { match std::env::consts::OS { "linux" => { if std::env::var("WSL_DISTRO_NAME").is_ok() { Platform::Wsl } else { Platform::Linux } } "macos" => Platform::Macos, "windows" => Platform::Wsl, _ => Platform::Linux, } } pub fn config_dir(&self) -> PathBuf { match self { Platform::Linux => dirs::home_dir() .map(|p| p.join(".config")) .unwrap_or_else(|| PathBuf::from(".config")), Platform::Macos => dirs::home_dir() .map(|p| p.join("Library/Application Support")) .unwrap_or_else(|| PathBuf::from("Library/Application Support")), Platform::Wsl => dirs::home_dir() .map(|p| p.join(".config")) .unwrap_or_else(|| PathBuf::from(".config")), } } pub fn data_dir(&self) -> PathBuf { match self { Platform::Linux => dirs::home_dir() .map(|p| p.join(".local/share")) .unwrap_or_else(|| PathBuf::from(".local/share")), Platform::Macos => dirs::home_dir() .map(|p| p.join("Library/Application Support")) .unwrap_or_else(|| PathBuf::from("Library/Application Support")), Platform::Wsl => dirs::home_dir() .map(|p| p.join(".local/share")) .unwrap_or_else(|| PathBuf::from(".local/share")), } } }