use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::core::error::{Result, UrlsUpError};
pub fn expand_paths(
input_paths: Vec<&Path>,
recursive: bool,
file_types: Option<&HashSet<String>>,
) -> Result<Vec<PathBuf>> {
let mut result_paths = Vec::new();
for path in input_paths {
if path.is_file() {
if let Some(extensions) = file_types {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if extensions.contains(ext) {
result_paths.push(path.to_path_buf());
}
} else if extensions.contains("") {
result_paths.push(path.to_path_buf());
}
} else {
result_paths.push(path.to_path_buf());
}
} else if path.is_dir() && recursive {
let mut builder = ignore::WalkBuilder::new(path);
builder.hidden(false);
for entry in builder.build() {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_file() {
if let Some(extensions) = file_types {
if let Some(ext) = entry_path.extension().and_then(|e| e.to_str()) {
if extensions.contains(ext) {
result_paths.push(entry_path.to_path_buf());
}
} else if extensions.contains("") {
result_paths.push(entry_path.to_path_buf());
}
} else {
result_paths.push(entry_path.to_path_buf());
}
}
}
} else if path.is_dir() && !recursive {
return Err(UrlsUpError::PathExpansion(format!(
"'{}' is a directory. Use --recursive to process directories.",
path.display()
)));
}
}
Ok(result_paths)
}
#[cfg(test)]
mod tests {
#![allow(non_snake_case)]
use super::*;
use std::fs;
use tempfile::TempDir;
type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
fn create_test_structure() -> std::result::Result<TempDir, Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path();
fs::create_dir_all(base.join("subdir/nested"))?;
fs::create_dir_all(base.join("other"))?;
fs::write(base.join("README.md"), "# Test\nhttps://example.com")?;
fs::write(base.join("file.txt"), "Some text with https://test.com")?;
fs::write(
base.join("script.sh"),
"#!/bin/bash\necho https://shell.com",
)?;
fs::write(base.join("config.json"), r#"{"url": "https://json.com"}"#)?;
fs::write(base.join("no_extension"), "https://noext.com")?;
fs::write(
base.join("subdir/nested/deep.md"),
"Deep file https://deep.com",
)?;
fs::write(
base.join("other/another.txt"),
"Another https://another.com",
)?;
fs::write(base.join(".gitignore"), "*.log\ntmp/\n")?;
fs::write(base.join("debug.log"), "Should be ignored")?;
fs::create_dir_all(base.join("tmp"))?;
fs::write(base.join("tmp/temp.md"), "Should be ignored")?;
Ok(temp_dir)
}
#[test]
fn test_expand_paths__single_file() -> TestResult {
let temp_dir = create_test_structure()?;
let readme_path = temp_dir.path().join("README.md");
let result = expand_paths(vec![&readme_path], false, None)?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], readme_path);
Ok(())
}
#[test]
fn test_expand_paths__file_with_extension_filter() -> TestResult {
let temp_dir = create_test_structure()?;
let readme_path = temp_dir.path().join("README.md");
let txt_path = temp_dir.path().join("file.txt");
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
let result = expand_paths(vec![&readme_path], false, Some(&extensions))?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], readme_path);
let result = expand_paths(vec![&txt_path], false, Some(&extensions))?;
assert_eq!(result.len(), 0);
Ok(())
}
#[test]
fn test_expand_paths__directory_without_recursive_fails() -> TestResult {
let temp_dir = create_test_structure()?;
let result = expand_paths(vec![temp_dir.path()], false, None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("is a directory. Use --recursive")
);
Ok(())
}
#[test]
fn test_expand_paths__recursive_all_files() -> TestResult {
let temp_dir = create_test_structure()?;
let result = expand_paths(vec![temp_dir.path()], true, None)?;
assert!(result.len() >= 7);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"file.txt".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
assert!(file_names.contains(&"another.txt".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__recursive_with_file_type_filter() -> TestResult {
let temp_dir = create_test_structure()?;
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
let result = expand_paths(vec![temp_dir.path()], true, Some(&extensions))?;
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
assert!(!file_names.contains(&"file.txt".to_string()));
assert!(!file_names.contains(&"script.sh".to_string()));
for path in &result {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
assert_eq!(ext, "md");
}
}
Ok(())
}
#[test]
fn test_expand_paths__multiple_extensions() -> TestResult {
let temp_dir = create_test_structure()?;
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
extensions.insert("txt".to_string());
let result = expand_paths(vec![temp_dir.path()], true, Some(&extensions))?;
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"file.txt".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
assert!(file_names.contains(&"another.txt".to_string()));
assert!(!file_names.contains(&"script.sh".to_string()));
assert!(!file_names.contains(&"config.json".to_string()));
for path in &result {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
assert!(ext == "md" || ext == "txt");
}
}
Ok(())
}
#[test]
fn test_expand_paths__files_without_extension() -> TestResult {
let temp_dir = create_test_structure()?;
let mut extensions = HashSet::new();
extensions.insert("".to_string());
let result = expand_paths(vec![temp_dir.path()], true, Some(&extensions))?;
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"no_extension".to_string()));
for path in &result {
assert!(path.extension().is_none());
}
Ok(())
}
#[test]
fn test_expand_paths__mixed_files_and_directories() -> TestResult {
let temp_dir = create_test_structure()?;
let readme_path = temp_dir.path().join("README.md");
let subdir_path = temp_dir.path().join("subdir");
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
let result = expand_paths(vec![&readme_path, &subdir_path], true, Some(&extensions))?;
assert_eq!(result.len(), 2);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__nonexistent_file() -> TestResult {
let result = expand_paths(
vec![Path::new("/definitely/nonexistent/path/file.md")],
false,
None,
)?;
assert!(result.is_empty());
Ok(())
}
#[test]
fn test_expand_paths__permission_denied() -> TestResult {
let result = expand_paths(vec![Path::new("/proc/1/mem")], false, None);
let _ = result;
Ok(())
}
#[test]
fn test_expand_paths__empty_input() -> TestResult {
let result = expand_paths(vec![], false, None)?;
assert!(result.is_empty());
Ok(())
}
#[test]
fn test_expand_paths__directory_non_recursive_error() -> TestResult {
let temp_dir = tempfile::tempdir()?;
let dir_path = temp_dir.path();
let result = expand_paths(vec![dir_path], false, None);
assert!(result.is_err());
if let Err(UrlsUpError::PathExpansion(msg)) = result {
assert!(msg.contains("is a directory"));
assert!(msg.contains("Use --recursive"));
} else {
panic!("Expected PathExpansion error");
}
Ok(())
}
#[test]
fn test_expand_paths__file_extension_filtering() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let mut extensions = HashSet::new();
extensions.insert("txt".to_string());
let result = expand_paths(
vec![
base.join("file.txt").as_path(),
base.join("README.md").as_path(),
],
false,
Some(&extensions),
)?;
assert_eq!(result.len(), 1);
assert!(
result[0]
.file_name()
.unwrap()
.to_string_lossy()
.contains("file.txt")
);
Ok(())
}
#[test]
fn test_expand_paths__file_without_extension() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let mut extensions = HashSet::new();
extensions.insert("".to_string());
let result = expand_paths(
vec![base.join("no_extension").as_path()],
false,
Some(&extensions),
)?;
assert_eq!(result.len(), 1);
assert!(
result[0]
.file_name()
.unwrap()
.to_string_lossy()
.contains("no_extension")
);
Ok(())
}
#[test]
fn test_expand_paths__file_extension_case_sensitive() -> TestResult {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path();
fs::write(base.join("file.MD"), "# Test\nhttps://example.com")?;
fs::write(base.join("file.md"), "# Test\nhttps://example.com")?;
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
let result = expand_paths(
vec![
base.join("file.MD").as_path(),
base.join("file.md").as_path(),
],
false,
Some(&extensions),
)?;
assert_eq!(result.len(), 1);
assert!(
result[0]
.file_name()
.unwrap()
.to_string_lossy()
.contains("file.md")
);
Ok(())
}
#[test]
fn test_expand_paths__multiple_extensions_selective() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let mut extensions = HashSet::new();
extensions.insert("md".to_string());
extensions.insert("txt".to_string());
extensions.insert("json".to_string());
let result = expand_paths(
vec![
base.join("README.md").as_path(),
base.join("file.txt").as_path(),
base.join("config.json").as_path(),
base.join("script.sh").as_path(),
],
false,
Some(&extensions),
)?;
assert_eq!(result.len(), 3);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"file.txt".to_string()));
assert!(file_names.contains(&"config.json".to_string()));
assert!(!file_names.contains(&"script.sh".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__recursive_with_extension_filter() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let mut extensions = HashSet::new();
extensions.insert("txt".to_string());
let result = expand_paths(vec![base], true, Some(&extensions))?;
assert_eq!(result.len(), 2);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"file.txt".to_string()));
assert!(file_names.contains(&"another.txt".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__recursive_no_filter() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let result = expand_paths(vec![base], true, None)?;
assert!(result.len() >= 6);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"file.txt".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
assert!(file_names.contains(&"another.txt".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__mixed_files_and_directories_comprehensive() -> TestResult {
let temp_dir = create_test_structure()?;
let base = temp_dir.path();
let result = expand_paths(
vec![
base.join("README.md").as_path(),
base.join("subdir").as_path(),
],
true,
None,
)?;
assert!(result.len() >= 2);
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"README.md".to_string()));
assert!(file_names.contains(&"deep.md".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__ignore_gitignore_files() -> TestResult {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path();
fs::write(base.join(".gitignore"), "ignored.txt\n*.tmp")?;
fs::write(base.join("ignored.txt"), "should be ignored")?;
fs::write(base.join("test.tmp"), "should be ignored tmp")?;
fs::write(base.join("normal.txt"), "should be included")?;
let result = expand_paths(vec![base], true, None)?;
let file_names: Vec<String> = result
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(file_names.contains(&"normal.txt".to_string()));
Ok(())
}
#[test]
fn test_expand_paths__symlinks() -> TestResult {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path();
fs::write(base.join("target.txt"), "target file")?;
let symlink_path = base.join("link.txt");
let target_path = base.join("target.txt");
#[cfg(unix)]
{
if std::os::unix::fs::symlink(&target_path, &symlink_path).is_ok() {
let result = expand_paths(vec![&symlink_path], false, None)?;
assert!(result.len() <= 1); }
}
let result = expand_paths(vec![&target_path], false, None)?;
assert_eq!(result.len(), 1);
Ok(())
}
}