mod filesystem;
mod git;
use std::path::PathBuf;
pub use filesystem::FileInfo;
pub struct Scanner {
root: PathBuf,
file_cache: Vec<FileInfo>,
}
impl Scanner {
pub fn new(root: PathBuf) -> Self {
let file_cache = filesystem::scan_directory(&root);
Self { root, file_cache }
}
pub fn repository_name(&self) -> String {
if let Some(name) = git::get_repository_name(&self.root) {
return name;
}
self.root
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
self.root.to_string_lossy().to_string()
})
}
pub fn file_exists(&self, path: &str) -> bool {
self.root.join(path).exists()
}
pub fn directory_exists(&self, path: &str) -> bool {
let full_path = self.root.join(path);
full_path.exists() && full_path.is_dir()
}
pub fn read_file(&self, path: &str) -> std::io::Result<String> {
std::fs::read_to_string(self.root.join(path))
}
pub fn files_with_extensions(&self, extensions: &[&str]) -> Vec<&FileInfo> {
self.file_cache
.iter()
.filter(|f| {
extensions
.iter()
.any(|ext| f.path.ends_with(&format!(".{}", ext)))
})
.collect()
}
pub fn files_matching_pattern(&self, pattern: &str) -> Vec<&FileInfo> {
self.file_cache
.iter()
.filter(|f| {
if pattern.contains('*') {
glob_match(pattern, &f.path)
} else {
f.path.ends_with(pattern) || f.path.contains(pattern)
}
})
.collect()
}
pub fn files_larger_than(&self, size: u64) -> Vec<&FileInfo> {
self.file_cache.iter().filter(|f| f.size > size).collect()
}
pub fn files_in_directory(&self, dir: &str) -> Vec<&FileInfo> {
let dir_path = if dir.ends_with('/') {
dir.to_string()
} else {
format!("{}/", dir)
};
self.file_cache
.iter()
.filter(|f| f.path.starts_with(&dir_path) || f.path.starts_with(dir))
.collect()
}
#[allow(dead_code)]
pub fn all_files(&self) -> &[FileInfo] {
&self.file_cache
}
pub fn root(&self) -> &std::path::Path {
&self.root
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern.starts_with("*.") {
let ext = &pattern[1..];
return text.ends_with(ext);
}
if pattern.contains("**") {
let parts: Vec<&str> = pattern.split("**").collect();
if parts.len() == 2 {
let prefix = parts[0].trim_end_matches('/');
let suffix = parts[1].trim_start_matches('/');
if !prefix.is_empty() && !text.starts_with(prefix) {
return false;
}
if !suffix.is_empty() {
return text.ends_with(suffix) || text.contains(&format!("/{}", suffix));
}
return true;
}
}
text.contains(pattern.trim_start_matches('*').trim_end_matches('*'))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_repo() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::create_dir_all(root.join("src")).unwrap();
fs::create_dir_all(root.join("tests")).unwrap();
fs::create_dir_all(root.join(".github/workflows")).unwrap();
fs::write(root.join("README.md"), "# Test Project").unwrap();
fs::write(root.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(root.join("src/lib.rs"), "pub fn lib() {}").unwrap();
fs::write(root.join("tests/test.rs"), "#[test] fn test() {}").unwrap();
fs::write(root.join(".github/workflows/ci.yml"), "name: CI\non: push").unwrap();
let large_content = "x".repeat(10000);
fs::write(root.join("large_file.bin"), large_content).unwrap();
temp_dir
}
#[test]
fn test_scanner_new() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
assert!(!scanner.all_files().is_empty());
}
#[test]
fn test_repository_name_fallback() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let name = scanner.repository_name();
assert!(!name.is_empty());
}
#[test]
fn test_file_exists() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
assert!(scanner.file_exists("README.md"));
assert!(scanner.file_exists("src/main.rs"));
assert!(!scanner.file_exists("nonexistent.txt"));
}
#[test]
fn test_directory_exists() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
assert!(scanner.directory_exists("src"));
assert!(scanner.directory_exists(".github/workflows"));
assert!(!scanner.directory_exists("nonexistent"));
}
#[test]
fn test_read_file() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let content = scanner.read_file("README.md").unwrap();
assert_eq!(content, "# Test Project");
let result = scanner.read_file("nonexistent.txt");
assert!(result.is_err());
}
#[test]
fn test_files_with_extensions() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let rs_files = scanner.files_with_extensions(&["rs"]);
assert!(rs_files.len() >= 2);
let md_files = scanner.files_with_extensions(&["md"]);
assert!(!md_files.is_empty());
let multi_ext = scanner.files_with_extensions(&["rs", "toml"]);
assert!(multi_ext.len() >= 3);
}
#[test]
fn test_files_matching_pattern() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let rs_pattern = scanner.files_matching_pattern("*.rs");
assert!(!rs_pattern.is_empty());
let src_pattern = scanner.files_matching_pattern("src/");
assert!(!src_pattern.is_empty());
}
#[test]
fn test_files_larger_than() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let large_files = scanner.files_larger_than(5000);
assert!(!large_files.is_empty());
let very_large = scanner.files_larger_than(1_000_000);
assert!(very_large.is_empty());
}
#[test]
fn test_files_in_directory() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let src_files = scanner.files_in_directory("src");
assert!(src_files.len() >= 2);
let github_files = scanner.files_in_directory(".github");
assert!(!github_files.is_empty());
}
#[test]
fn test_glob_match_star() {
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
}
#[test]
fn test_glob_match_extension() {
assert!(glob_match("*.rs", "main.rs"));
assert!(glob_match("*.rs", "src/lib.rs"));
assert!(!glob_match("*.rs", "main.txt"));
}
#[test]
fn test_glob_match_double_star() {
assert!(glob_match("src/**", "src/lib.rs"));
assert!(glob_match("src/**", "src/sub/file.rs"));
assert!(glob_match("tests/**", "tests/unit/test.rs"));
}
#[test]
fn test_glob_match_partial() {
assert!(glob_match("main", "src/main.rs"));
assert!(glob_match("*main*", "src/main.rs"));
}
#[test]
fn test_all_files() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let files = scanner.all_files();
assert!(files.len() >= 6); }
#[test]
fn test_files_matching_pattern_no_wildcard() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let pattern = scanner.files_matching_pattern("README.md");
assert!(!pattern.is_empty());
}
#[test]
fn test_glob_match_no_match() {
assert!(!glob_match("*.rs", "main.txt"));
assert!(!glob_match("src/**", "other/file.txt"));
}
#[test]
fn test_files_in_directory_consistency() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let files = scanner.files_in_directory("src");
assert!(files.len() >= 2);
}
#[test]
fn test_files_larger_than_zero() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let files = scanner.files_larger_than(0);
assert!(!files.is_empty());
}
#[test]
fn test_files_with_multiple_extensions() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let files = scanner.files_with_extensions(&["yml", "yaml"]);
assert!(!files.is_empty()); }
#[test]
fn test_glob_match_empty_prefix() {
assert!(glob_match("**/workflows", ".github/workflows"));
}
#[test]
fn test_glob_match_double_star_empty_suffix() {
assert!(glob_match("src/**", "src/main.rs"));
assert!(glob_match("src/**", "src/sub/file.rs"));
}
#[test]
fn test_glob_match_double_star_all() {
assert!(glob_match("**", "any/path/file.txt"));
}
#[test]
fn test_files_matching_pattern_with_contains() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let result = scanner.files_matching_pattern("main");
assert!(!result.is_empty());
}
#[test]
fn test_files_matching_exact_extension() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let md_files = scanner.files_matching_pattern("*.md");
assert!(!md_files.is_empty()); }
#[test]
fn test_files_in_directory_with_trailing_slash() {
let temp_dir = create_test_repo();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let files = scanner.files_in_directory("src/");
assert!(files.len() >= 2);
}
#[test]
fn test_repository_name_without_git_remote() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("test.txt"), "content").unwrap();
let scanner = Scanner::new(temp_dir.path().to_path_buf());
let name = scanner.repository_name();
assert!(!name.is_empty());
}
}