#include <hypergrep/directory_search.hpp>
#include <hypergrep/is_binary.hpp>
#include <hypergrep/trim_whitespace.hpp>
directory_search::directory_search(std::string &pattern,
const std::filesystem::path &path,
argparse::ArgumentParser &program)
: search_path(path) {
initialize_search(pattern, program, options, &database, &scratch,
&file_filter_database, &file_filter_scratch);
}
directory_search::~directory_search() {
if (file_filter_scratch) {
hs_free_scratch(file_filter_scratch);
}
if (file_filter_database) {
hs_free_database(file_filter_database);
}
if (scratch) {
hs_free_scratch(scratch);
}
if (database) {
hs_free_database(database);
}
}
void directory_search::search_thread_function() {
char buffer[FILE_CHUNK_SIZE];
std::string lines{};
hs_scratch_t *local_scratch = NULL;
hs_error_t database_error = hs_alloc_scratch(database, &local_scratch);
if (database_error != HS_SUCCESS) {
throw std::runtime_error("Error allocating scratch space\n");
}
moodycamel::ConsumerToken ctok{queue};
while (true) {
if (num_files_enqueued > 0) {
try_dequeue_and_process_path(ctok, local_scratch, buffer, lines);
}
if (!running && num_files_dequeued == num_files_enqueued) {
break;
}
}
hs_free_scratch(local_scratch);
}
void directory_search::run(std::filesystem::path path) {
if (!options.ignore_gitindex) {
git_libgit2_init();
}
std::vector<std::thread> consumer_threads(options.num_threads);
if (options.perform_search) {
for (std::size_t i = 0; i < options.num_threads; ++i) {
consumer_threads[i] = std::thread(
std::bind(&directory_search::search_thread_function, this));
}
}
// Kick off the directory traversal
moodycamel::ProducerToken ptok{queue};
visit_directory_and_enqueue(ptok, path.string(), file_filter_scratch);
// Spawn threads to handle subdirectories
{
const auto num_subdir_threads = options.num_threads;
std::vector<std::thread> traversal_threads(num_subdir_threads);
for (std::size_t i = 0; i < num_subdir_threads; ++i) {
traversal_threads[i] = std::thread([this]() {
// A local scratch for each traversal thread
hs_scratch *local_file_filter_scratch{nullptr};
if (options.filter_files) {
hs_error_t database_error = hs_alloc_scratch(
file_filter_database, &local_file_filter_scratch);
if (database_error != HS_SUCCESS) {
fprintf(stderr, "Error allocating scratch space\n");
hs_free_database(file_filter_database);
return;
}
}
while (num_dirs_enqueued > 0) {
std::string subdir{};
auto found = subdirectories.try_dequeue(subdir);
if (found) {
if (!options.ignore_gitindex) {
const auto dot_git_path = std::filesystem::path(subdir) / ".git";
if (std::filesystem::exists(dot_git_path)) {
// Enqueue git repo
git_repo_paths.enqueue(subdir);
++num_git_repos_enqueued;
} else {
// go deeper
moodycamel::ProducerToken ptok{queue};
visit_directory_and_enqueue(ptok, subdir,
local_file_filter_scratch);
}
} else {
// Ignore git index
// Just go deeper
moodycamel::ProducerToken ptok{queue};
visit_directory_and_enqueue(ptok, subdir,
local_file_filter_scratch);
}
num_dirs_enqueued -= 1;
}
}
if (local_file_filter_scratch) {
hs_free_scratch(local_file_filter_scratch);
}
});
}
for (std::size_t i = 0; i < num_subdir_threads; ++i) {
traversal_threads[i].join();
}
}
running = false;
// Done enqueuing files for search
// Help with the search now:
if (options.perform_search) {
search_thread_function();
for (std::size_t i = 0; i < options.num_threads; ++i) {
consumer_threads[i].join();
}
}
// All threads are done processing the file queue
// Search git repos one by one
if (!options.ignore_gitindex && num_git_repos_enqueued > 0) {
auto current_path = std::filesystem::current_path();
while (num_git_repos_enqueued > 0) {
std::string repo_path{};
auto found = git_repo_paths.try_dequeue(repo_path);
if (found) {
git_index_search git_index_searcher(
database, scratch, file_filter_database, file_filter_scratch,
options, repo_path);
if (chdir(repo_path.c_str()) == 0) {
git_index_searcher.run(".");
if (chdir(current_path.c_str()) != 0) {
throw std::runtime_error("Failed to restore path");
}
}
--num_git_repos_enqueued;
}
}
}
if (options.perform_search) {
// Now search large files one by one using the large_file_searcher
if (num_large_files_enqueued > 0) {
file_search large_file_searcher(database, scratch, options);
// Memory map + multi-threaded search
while (num_large_files_enqueued > 0) {
large_file lf{};
auto found = large_file_backlog.try_dequeue(lf);
if (found) {
large_file_searcher.run(lf.path, lf.size);
--num_large_files_enqueued;
}
}
}
}
}
bool is_blacklisted(const std::string &str) {
const std::vector<std::string> substrings{".o", ".so", ".png", ".jpg",
".jpeg", ".mp3", ".mp4", ".gz",
".xz", ".zip"};
for (const auto &substring : substrings) {
if (str.size() >= substring.size() &&
str.compare(str.size() - substring.size(), substring.size(),
substring) == 0) {
return true;
}
}
return false;
}
bool directory_search::process_file(std::string &&filename,
hs_scratch_t *local_scratch, char *buffer,
std::string &lines) {
if (is_blacklisted(filename)) {
return false;
}
int fd = open(filename.data(), O_RDONLY, 0);
if (fd == -1) {
std::cerr << filename << ": " << std::strerror(errno) << " (os error " << errno << ")\n";
return false;
}
bool result{false};
const auto process_fn =
(options.is_stdout || options.print_only_matching_parts ||
options.show_column_numbers || options.show_byte_offset)
? process_matches
: process_matches_nocolor_nostdout;
// Process the file in chunks
std::size_t total_bytes_read = 0;
bool max_file_size_provided = options.max_file_size.has_value();
std::size_t max_file_size =
max_file_size_provided ? options.max_file_size.value() : 0;
std::size_t bytes_read = 0;
std::atomic<std::size_t> max_line_number{0};
std::size_t current_line_number{1};
std::size_t num_matching_lines{0};
std::size_t num_matches{0};
// Read the file in chunks and perform search
bool first{true};
bool continue_even_though_large_file{false};
while (true) {
auto ret = read(fd, buffer, FILE_CHUNK_SIZE);
if (ret > 0) {
bytes_read = ret;
} else {
break;
}
total_bytes_read += bytes_read;
if (max_file_size_provided && total_bytes_read > max_file_size) {
// File size limit reached
close(fd);
lines.clear();
return false;
} else if (!continue_even_though_large_file &&
total_bytes_read > LARGE_FILE_SIZE) {
// This file is a bit large
// Add it to the backlog and process it later with a file_search object
// instead of using a single thread to read in chunks
// The file_search object will memory map and search this large file
// in multiple threads
// Perform a stat and check the file size?
// If the file size is not much larger than total_bytes_read
// just continue and finish the file
const auto file_size = std::filesystem::file_size(filename.data());
// Only bail if the file size if more than twice of
// what hypergrep has already searched
if (total_bytes_read * 2 > file_size) {
large_file lf{std::move(filename), file_size};
large_file_backlog.enqueue(lf);
++num_large_files_enqueued;
close(fd);
lines.clear();
return false;
} else {
continue_even_though_large_file = true;
}
}
if (first) {
first = false;
if (starts_with_magic_bytes(buffer, bytes_read)) {
result = false;
break;
}
if (has_null_bytes(buffer, bytes_read)) {
// NULL bytes found
// Ignore file
// Could be a .exe, .gz, .bin etc.
result = false;
break;
}
} else if (first) {
first = false;
}
// Detect end of file
// If end of file, search entire chunk
// If not end of file, search for last newline
const auto last_chunk = (bytes_read < FILE_CHUNK_SIZE);
std::size_t search_size = bytes_read;
char *last_newline = buffer + bytes_read;
if (!last_chunk) {
// Find the position of the last newline in the buffer
// In order to catch matches between chunks, need to amend the buffer
// and make sure it stops at a new line boundary
char *last_newline = (char *)memrchr(buffer, '\n', bytes_read);
if (last_newline) {
search_size = last_newline - buffer;
} else {
// Not a single newline in the entire chunk
// This could be some binary file or some minified JS file
// Skip it
result = false;
break;
}
}
std::vector<std::pair<unsigned long long, unsigned long long>> matches{};
std::atomic<size_t> number_of_matches = 0;
file_context ctx{number_of_matches, matches, options.print_only_filenames};
if (hs_scan(database, buffer, search_size, 0, local_scratch, on_match,
(void *)(&ctx)) != HS_SUCCESS) {
if (options.print_only_filenames && ctx.number_of_matches > 0) {
result = true;
} else {
result = false;
}
break;
} else {
if (ctx.number_of_matches > 0) {
result = true;
}
}
if (ctx.number_of_matches > 0) {
num_matching_lines += process_fn(
filename.data(), buffer, search_size, ctx.matches,
current_line_number, lines, options.print_filenames,
options.is_stdout, options.show_line_numbers,
options.show_column_numbers, options.show_byte_offset,
options.print_only_matching_parts, options.max_column_limit,
total_bytes_read - bytes_read, options.ltrim_each_output_line);
num_matches += ctx.number_of_matches;
}
if (!last_chunk) {
if (static_cast<std::size_t>(last_newline - buffer) == bytes_read - 1) {
// Chunk ends exactly at the newline
// Do nothing
} else {
// Backtrack "remainder" number of characters
if (bytes_read > search_size) {
if ((bytes_read - search_size) < FILE_CHUNK_SIZE) {
lseek(fd, -1 * (bytes_read - search_size), SEEK_CUR);
} else {
// Don't lseek back the entire chunk
// because that's an infinite loop
// Not a single newline in the entire chunk
// This could be some binary file or some minified JS file
// Skip it
result = false;
break;
}
}
}
}
}
close(fd);
if ((result || options.count_include_zeros) && options.count_matching_lines &&
!options.print_only_filenames) {
if (options.print_filenames) {
if (options.is_stdout) {
fmt::print("{}:{}\n",
fmt::format(fg(fmt::color::steel_blue), "{}", filename),
num_matching_lines);
} else {
fmt::print("{}:{}\n", filename, num_matching_lines);
}
} else {
fmt::print("{}\n", num_matching_lines);
}
} else if ((result || options.count_include_zeros) && options.count_matches &&
!options.print_only_filenames) {
if (options.print_filenames) {
if (options.is_stdout) {
fmt::print("{}:{}\n",
fmt::format(fg(fmt::color::steel_blue), "{}", filename),
num_matches);
} else {
fmt::print("{}:{}\n", filename, num_matches);
}
} else {
fmt::print("{}\n", num_matches);
}
} else {
if (result && options.print_only_filenames) {
if (options.is_stdout) {
fmt::print(fg(fmt::color::steel_blue), "{}\n", filename);
} else {
fmt::print("{}\n", filename);
}
} else if (result && !options.count_matching_lines &&
!options.count_matches && !options.print_only_filenames &&
!lines.empty()) {
if (options.is_stdout) {
if (options.print_filenames) {
lines = fmt::format(fg(fmt::color::steel_blue), "\n{}\n", filename) +
lines;
} else {
lines += "\n";
}
fmt::print("{}", lines);
} else {
fmt::print("{}", lines);
}
}
}
lines.clear();
return result;
}
void directory_search::visit_directory_and_enqueue(
moodycamel::ProducerToken &ptok, std::string directory,
hs_scratch *local_file_filter_scratch) {
DIR *dir = opendir(directory.c_str());
if (dir == NULL) {
return;
}
struct dirent *entry;
while (true) {
entry = readdir(dir);
if (!entry) {
break;
}
// Ignore symlinks
if (entry->d_type == DT_LNK) {
continue;
}
// Ignore dot files/directories unless requested
if (!options.search_hidden_files && entry->d_name[0] == '.')
continue;
// Check if the entry is a directory
if (entry->d_type == DT_DIR) {
if (options.search_hidden_files) {
// Even if hidden files are going to be searched
// Need to skip these two
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
}
// Enqueue subdirectory for processing
const auto path = std::filesystem::path{directory} / entry->d_name;
subdirectories.enqueue(std::move(path));
num_dirs_enqueued += 1;
} else if (entry->d_type == DT_REG) {
const auto path = std::filesystem::path{directory} / entry->d_name;
if (!options.filter_files ||
(options.filter_files &&
filter_file(path.c_str(), file_filter_database,
local_file_filter_scratch, options.negate_filter))) {
if (options.perform_search) {
queue.enqueue(ptok, std::move(path));
++num_files_enqueued;
} else {
// If --max-filesize is used, perform a stat
// and check if the file size is under the limit
static const bool max_file_size_provided =
options.max_file_size.has_value();
static const std::size_t max_file_size =
max_file_size_provided ? options.max_file_size.value() : 0;
if (max_file_size_provided) {
const auto file_size = std::filesystem::file_size(path);
if (file_size > max_file_size) {
// skip this file
continue;
}
}
if (options.is_stdout) {
fmt::print(fg(fmt::color::steel_blue), "{}\n", path.c_str());
} else {
fmt::print("{}\n", path.c_str());
}
}
}
}
}
closedir(dir);
}
bool directory_search::try_dequeue_and_process_path(
moodycamel::ConsumerToken &ctok, hs_scratch_t *local_scratch, char *buffer,
std::string &lines) {
constexpr std::size_t BULK_DEQUEUE_SIZE = 32;
std::string entries[BULK_DEQUEUE_SIZE];
auto count = queue.try_dequeue_bulk(ctok, entries, BULK_DEQUEUE_SIZE);
if (count > 0) {
for (std::size_t j = 0; j < count; ++j) {
process_file(std::move(entries[j]), local_scratch, buffer, lines);
}
num_files_dequeued += count;
return true;
}
return false;
}