use ignore::Walk;
use std::io;
use std::path::Path;
use std::path::PathBuf;
pub fn find_files(paths: &[impl AsRef<Path>]) -> io::Result<Vec<PathBuf>> {
let mut files: Vec<PathBuf> = Vec::new();
let mut error_paths: Vec<String> = Vec::new();
for path in paths {
let path = path.as_ref();
match path.metadata() {
Ok(_) => {
for result in Walk::new(path) {
match result {
Ok(entry) => {
if entry.file_type().is_some_and(|ft| ft.is_file()) {
files.push(entry.into_path());
}
}
Err(err) => {
eprintln!("{}: {}", path.display(), err);
error_paths.push(path.display().to_string());
}
}
}
}
Err(err) => {
eprintln!("{}: {}", path.display(), err);
error_paths.push(path.display().to_string());
}
}
}
if !error_paths.is_empty() {
Err(io::Error::other(format!(
"errors occurred while processing files: {}",
error_paths.join(", ")
)))
} else {
Ok(files)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_find_single_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "content").unwrap();
let result = find_files(&[&file_path]).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], file_path);
}
#[test]
fn test_find_files_in_directory() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
fs::write(&file1, "content1").unwrap();
fs::write(&file2, "content2").unwrap();
let result = find_files(&[temp_dir.path()]).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&file1));
assert!(result.contains(&file2));
}
#[test]
fn test_find_files_in_nested_directory() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = subdir.join("file2.txt");
fs::write(&file1, "content1").unwrap();
fs::write(&file2, "content2").unwrap();
let result = find_files(&[temp_dir.path()]).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&file1));
assert!(result.contains(&file2));
}
#[test]
fn test_find_files_from_multiple_paths() {
let temp_dir1 = TempDir::new().unwrap();
let temp_dir2 = TempDir::new().unwrap();
let file1 = temp_dir1.path().join("file1.txt");
let file2 = temp_dir2.path().join("file2.txt");
fs::write(&file1, "content1").unwrap();
fs::write(&file2, "content2").unwrap();
let result = find_files(&[&file1, &file2]).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&file1));
assert!(result.contains(&file2));
}
#[test]
fn test_find_files_mixed_file_and_directory() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("file1.txt");
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let file2 = subdir.join("file2.txt");
fs::write(&file1, "content1").unwrap();
fs::write(&file2, "content2").unwrap();
let result = find_files(&[&file1, &subdir]).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&file1));
assert!(result.contains(&file2));
}
#[test]
fn test_find_files_nonexistent_path() {
let nonexistent = PathBuf::from("/nonexistent/path/file.txt");
let result = find_files(&[&nonexistent]);
assert!(result.is_err());
}
#[test]
fn test_find_files_ignores_directories_in_results() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let file = subdir.join("file.txt");
fs::write(&file, "content").unwrap();
let result = find_files(&[temp_dir.path()]).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], file);
}
#[test]
fn test_find_files_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let result = find_files(&[temp_dir.path()]).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_find_files_respects_gitignore() {
let temp_dir = TempDir::new().unwrap();
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).unwrap();
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, "ignored.txt\n").unwrap();
let ignored_file = temp_dir.path().join("ignored.txt");
let normal_file = temp_dir.path().join("normal.txt");
fs::write(&ignored_file, "ignored content").unwrap();
fs::write(&normal_file, "normal content").unwrap();
let result = find_files(&[temp_dir.path()]).unwrap();
assert_eq!(result.len(), 1, "Found files: {result:?}");
assert!(!result.contains(&ignored_file));
assert!(result.contains(&normal_file));
}
#[test]
#[cfg(unix)]
fn test_find_files_follows_symlinks_to_files() {
use std::os::unix::fs as unix_fs;
let temp_dir = TempDir::new().unwrap();
let real_file = temp_dir.path().join("real.txt");
let symlink_file = temp_dir.path().join("link.txt");
fs::write(&real_file, "content").unwrap();
unix_fs::symlink(&real_file, &symlink_file).unwrap();
let result = find_files(&[&symlink_file]).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
#[cfg(unix)]
fn test_find_files_handles_broken_symlinks() {
use std::os::unix::fs as unix_fs;
let temp_dir = TempDir::new().unwrap();
let broken_symlink = temp_dir.path().join("broken_link.txt");
unix_fs::symlink("/nonexistent/file.txt", &broken_symlink).unwrap();
let result = find_files(&[&broken_symlink]);
assert!(result.is_ok() || result.is_err());
}
#[test]
#[cfg(unix)]
fn test_find_files_follows_symlinks_to_directories() {
use std::os::unix::fs as unix_fs;
let temp_dir = TempDir::new().unwrap();
let real_dir = temp_dir.path().join("real_dir");
let symlink_dir = temp_dir.path().join("link_dir");
fs::create_dir(&real_dir).unwrap();
let file_in_dir = real_dir.join("file.txt");
fs::write(&file_in_dir, "content").unwrap();
unix_fs::symlink(&real_dir, &symlink_dir).unwrap();
let result = find_files(&[&symlink_dir]).unwrap();
assert_eq!(result.len(), 1);
}
}