use super::reader::FileReader;
use crate::output::result::{FileMatchResult, ResultMessage};
use crate::output::{colors::Color, highlighter::TextHighlighter};
use memmap2::MmapOptions;
use rayon::scope;
use std::fs::File;
use std::io::{BufRead, BufReader, Result};
use std::path::PathBuf;
use std::sync::mpsc;
fn _process_content_lines(
content: &str,
highlighter: &TextHighlighter,
messages: &mut Vec<ResultMessage>,
) -> (usize, usize) {
let mut total_lines = 0;
let mut matched_count = 0;
for (index, line) in content.lines().enumerate() {
total_lines += 1;
if highlighter.regex.is_match(line) {
let line_msg = ResultMessage::Line {
index,
content: highlighter.highlight(line),
};
messages.push(line_msg);
let matches_in_line = highlighter.regex.find_iter(line).count();
matched_count += matches_in_line;
}
}
(total_lines, matched_count)
}
fn _process_file_streaming(
filepath: &PathBuf,
highlighter: &TextHighlighter,
messages: &mut Vec<ResultMessage>,
) -> Result<(usize, usize, usize)> {
let file = File::open(filepath)?;
let reader = BufReader::new(file);
let mut total_lines = 0;
let mut matched_count = 0;
let mut skipped_count = 0;
for (index, line) in reader.lines().enumerate() {
let line = match line {
Ok(l) => l,
Err(_e) => {
skipped_count += 1;
continue;
}
};
total_lines += 1;
if highlighter.regex.is_match(&line) {
let line_msg = ResultMessage::Line {
index,
content: highlighter.highlight(&line),
};
messages.push(line_msg);
let matches_in_line = highlighter.regex.find_iter(&line).count();
matched_count += matches_in_line;
}
}
Ok((total_lines, matched_count, skipped_count))
}
fn _process_file_bulk_read(
filepath: &PathBuf,
highlighter: &TextHighlighter,
messages: &mut Vec<ResultMessage>,
) -> Result<(usize, usize, usize)> {
let content = std::fs::read_to_string(filepath)?;
let (total_lines, matched_count) = _process_content_lines(&content, highlighter, messages);
Ok((total_lines, matched_count, 0)) }
fn _process_file_memory_map(
filepath: &PathBuf,
highlighter: &TextHighlighter,
messages: &mut Vec<ResultMessage>,
) -> Result<(usize, usize, usize)> {
let file = File::open(filepath)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let content = std::str::from_utf8(&mmap)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let (total_lines, matched_count) = _process_content_lines(content, highlighter, messages);
Ok((total_lines, matched_count, 0)) }
fn _process_file(
filepath: &PathBuf,
_pattern: &str,
highlighter: &TextHighlighter,
show_stats: bool,
reader: FileReader,
) -> Result<FileMatchResult> {
let mut messages = Vec::new();
messages.push(ResultMessage::Header(filepath.to_path_buf()));
let (total_lines, matched_count, skipped_count) = match reader {
FileReader::Streaming => {
match _process_file_streaming(filepath, highlighter, &mut messages) {
Ok(stats) => stats,
Err(e) => {
let err_msg = format!("Failed to process file {}: {}", filepath.display(), e);
messages.push(ResultMessage::Error(err_msg));
return Ok(messages);
}
}
}
FileReader::BulkRead => {
match _process_file_bulk_read(filepath, highlighter, &mut messages) {
Ok(stats) => stats,
Err(e) => {
let err_msg = format!("Failed to read file {}: {}", filepath.display(), e);
messages.push(ResultMessage::Error(err_msg));
return Ok(messages);
}
}
}
FileReader::MemoryMap => {
match _process_file_memory_map(filepath, highlighter, &mut messages) {
Ok(stats) => stats,
Err(e) => {
let err_msg =
format!("Failed to memory map file {}: {}", filepath.display(), e);
messages.push(ResultMessage::Error(err_msg));
return Ok(messages);
}
}
}
};
if show_stats {
messages.push(ResultMessage::SearchStats {
lines: total_lines,
matched: matched_count,
skipped: skipped_count,
});
}
messages.push(ResultMessage::Done);
Ok(messages)
}
pub fn search_files(
files: &[PathBuf],
pattern: &str,
color: &Color,
show_stats: bool,
) -> mpsc::Receiver<FileMatchResult> {
let (tx, rx) = mpsc::channel();
let highlighter = TextHighlighter::new(pattern, color);
let is_single_file = files.len() == 1;
if is_single_file {
let file = &files[0];
let reader = FileReader::select(file, true);
let messages = match _process_file(file, pattern, &highlighter, show_stats, reader) {
Ok(msg) => msg,
Err(e) => {
let err_msg = format!("Error processing file {}: {}", file.display(), e);
vec![ResultMessage::Error(err_msg)]
}
};
tx.send(messages).ok();
return rx;
}
scope(|s| {
for file in files {
let _tx = tx.clone();
let _highlighter = &highlighter;
let _pattern = pattern;
let _file = file.clone();
s.spawn(move |_| {
let reader = FileReader::select(&_file, false);
let messages =
match _process_file(&_file, _pattern, _highlighter, show_stats, reader) {
Ok(msg) => msg,
Err(e) => {
let err_msg =
format!("Error processing file {}: {}", _file.display(), e);
vec![ResultMessage::Error(err_msg)]
}
};
_tx.send(messages).ok();
});
}
});
rx
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempdir::TempDir;
#[test]
fn test_search_files_finds_pattern() {
let temp_dir = TempDir::new("search_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Hello world").unwrap();
writeln!(file, "This is a test").unwrap();
writeln!(file, "Hello again").unwrap();
let files = vec![test_file];
let pattern = "Hello";
let color = Color::Red;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_multiple_files() {
let temp_dir = TempDir::new("search_multi_test").unwrap();
let file1 = temp_dir.path().join("file1.txt");
let mut f1 = File::create(&file1).unwrap();
writeln!(f1, "Pattern in file 1").unwrap();
writeln!(f1, "Some other text").unwrap();
let file2 = temp_dir.path().join("file2.txt");
let mut f2 = File::create(&file2).unwrap();
writeln!(f2, "Different content").unwrap();
writeln!(f2, "Pattern in file 2").unwrap();
let files = vec![file1, file2];
let pattern = "Pattern";
let color = Color::Blue;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_no_matches() {
let temp_dir = TempDir::new("search_no_match_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Hello world").unwrap();
writeln!(file, "This is a test").unwrap();
let files = vec![test_file];
let pattern = "NotFound";
let color = Color::Green;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_empty_file() {
let temp_dir = TempDir::new("search_empty_test").unwrap();
let test_file = temp_dir.path().join("empty.txt");
File::create(&test_file).unwrap();
let files = vec![test_file];
let pattern = "anything";
let color = Color::Red;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_nonexistent_file() {
let temp_dir = TempDir::new("search_nonexistent_test").unwrap();
let nonexistent_file = temp_dir.path().join("does_not_exist.txt");
let files = vec![nonexistent_file];
let pattern = "anything";
let color = Color::Red;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_different_colors() {
let temp_dir = TempDir::new("search_colors_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Test pattern here").unwrap();
let files = vec![
test_file.clone(),
test_file.clone(),
test_file.clone(),
test_file,
];
let pattern = "pattern";
search_files(&vec![files[0].clone()], pattern, &Color::Red, false);
search_files(&vec![files[1].clone()], pattern, &Color::Green, false);
search_files(&vec![files[2].clone()], pattern, &Color::Blue, false);
search_files(&vec![files[3].clone()], pattern, &Color::Bold, false);
}
#[test]
fn test_search_files_regex_patterns() {
let temp_dir = TempDir::new("search_regex_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "email@example.com").unwrap();
writeln!(file, "test@domain.org").unwrap();
writeln!(file, "not an email").unwrap();
let files = vec![test_file];
let pattern = r"\w+@\w+\.\w+"; let color = Color::Blue;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_special_characters() {
let temp_dir = TempDir::new("search_special_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Special chars: àáâã").unwrap();
writeln!(file, "Symbols: @#$%^&*()").unwrap();
writeln!(file, "Unicode: 🦀 Rust crab").unwrap();
let files = vec![test_file];
let pattern = "🦀";
let color = Color::Green;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_case_sensitive() {
let temp_dir = TempDir::new("search_case_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Hello World").unwrap();
writeln!(file, "hello world").unwrap();
writeln!(file, "HELLO WORLD").unwrap();
let files = vec![test_file];
let pattern = "Hello"; let color = Color::Red;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_long_lines() {
let temp_dir = TempDir::new("search_long_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
let long_line = "x".repeat(10000) + "PATTERN" + &"y".repeat(10000);
writeln!(file, "{}", long_line).unwrap();
writeln!(file, "Short line").unwrap();
let files = vec![test_file];
let pattern = "PATTERN";
let color = Color::Blue;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_empty_pattern() {
let temp_dir = TempDir::new("search_empty_pattern_test").unwrap();
let test_file = temp_dir.path().join("test.txt");
let mut file = File::create(&test_file).unwrap();
writeln!(file, "Some content").unwrap();
let files = vec![test_file];
let pattern = ""; let color = Color::Red;
search_files(&files, pattern, &color, false);
}
#[test]
fn test_search_files_mixed_scenarios() {
let temp_dir = TempDir::new("search_mixed_test").unwrap();
let valid_file = temp_dir.path().join("valid.txt");
let mut f = File::create(&valid_file).unwrap();
writeln!(f, "Valid content with pattern").unwrap();
let empty_file = temp_dir.path().join("empty.txt");
File::create(&empty_file).unwrap();
let nonexistent = temp_dir.path().join("missing.txt");
let files = vec![valid_file, empty_file, nonexistent];
let pattern = "pattern";
let color = Color::Green;
search_files(&files, pattern, &color, false);
}
}