mod executor;
pub(crate) mod lines;
mod resolver;
pub mod verifier;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use resolver::resolve_doc;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use regex::bytes::RegexBuilder;
use crate::index::IndexSnapshot;
use crate::path::filter::{build_filter, matches_path_filter};
use crate::query::{literal_grams, route_query, GramQuery, QueryRoute};
use crate::{Config, IndexError, SearchMatch, SearchOptions};
use executor::{execute_query, should_use_index};
pub(crate) const REGEX_SIZE_LIMIT: usize = 10 * 1024 * 1024;
use verifier::{verify_empty, verify_literal, verify_regex};
#[derive(Clone)]
pub(crate) struct MatchedFile {
pub normalized: Arc<[u8]>,
pub raw_len: u64,
}
pub(crate) struct SearchOutcome {
pub matches: Vec<SearchMatch>,
pub files: HashMap<PathBuf, MatchedFile>,
}
pub fn search(
snap: Arc<IndexSnapshot>,
config: &Config,
canonical_root: &std::path::Path,
pattern: &str,
opts: &SearchOptions,
) -> Result<Vec<SearchMatch>, IndexError> {
Ok(search_with_content(snap, config, canonical_root, pattern, opts, false)?.matches)
}
pub(crate) fn search_with_content(
snap: Arc<IndexSnapshot>,
config: &Config,
canonical_root: &std::path::Path,
pattern: &str,
opts: &SearchOptions,
capture_content: bool,
) -> Result<SearchOutcome, IndexError> {
#[cfg(any(test, feature = "oracle"))]
let route = if opts.force_full_scan {
QueryRoute::FullScan
} else {
route_query(pattern, opts.case_insensitive).map_err(IndexError::InvalidPattern)?
};
#[cfg(not(any(test, feature = "oracle")))]
let route = route_query(pattern, opts.case_insensitive).map_err(IndexError::InvalidPattern)?;
let verify_pattern = opts.verify_pattern.as_deref().unwrap_or(pattern);
let compiled_re = if matches!(route, QueryRoute::Literal) && opts.verify_pattern.is_none() {
None
} else {
let re = RegexBuilder::new(verify_pattern)
.case_insensitive(opts.case_insensitive)
.multi_line(true)
.crlf(true)
.size_limit(REGEX_SIZE_LIMIT)
.dfa_size_limit(REGEX_SIZE_LIMIT)
.build()
.map_err(|e| IndexError::InvalidPattern(e.to_string()))?;
Some(re)
};
let candidates: Vec<u32> = match &route {
QueryRoute::Literal => match literal_grams(pattern) {
Some(covering) if !covering.required.is_empty() => {
if should_use_index(&covering.required, &snap)? {
execute_query(&GramQuery::Grams(covering.required), &snap)?
} else {
all_doc_ids(&snap)
}
}
_ => all_doc_ids(&snap),
},
QueryRoute::IndexedRegex(query) => {
let selective = match query {
GramQuery::Grams(grams) if !grams.is_empty() => should_use_index(grams, &snap)?,
_ => true,
};
if !selective {
all_doc_ids(&snap)
} else {
let indexed = execute_query(query, &snap)?;
if indexed.is_empty() {
all_doc_ids(&snap)
} else {
indexed
}
}
}
_ => all_doc_ids(&snap),
};
let glob_cache = snap
.glob_cache
.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
let include_types: Vec<&str> = opts
.file_type
.as_deref()
.into_iter()
.chain(opts.file_types.iter().map(String::as_str))
.collect();
let exclude_types: Vec<&str> = opts
.exclude_type
.as_deref()
.into_iter()
.chain(opts.exclude_types.iter().map(String::as_str))
.collect();
let path_filter_bitmap = build_filter(
&snap.path_index,
&include_types,
&exclude_types,
opts.path_filter.as_deref(),
Some(glob_cache),
);
let base_doc_to_file_id = path_filter_bitmap
.as_ref()
.map(|_| snap.base_doc_to_file_id());
let deterministic = opts.deterministic;
let match_count = AtomicUsize::new(0);
let root_fd = crate::index::io_util::open_root_dirfd(canonical_root);
let do_match = |&global_id: &u32| -> Option<FileResult> {
if let Some(limit) = opts.max_results {
if !deterministic && match_count.load(Ordering::Relaxed) >= limit {
return None;
}
}
let (rel_path, content, raw_len) = resolve_doc(
&snap,
global_id,
canonical_root,
root_fd.as_ref(),
config.max_file_size,
)?;
if let Some(ref pf) = path_filter_bitmap {
let doc_to_file_id = base_doc_to_file_id.as_ref().expect("built alongside pf");
let file_id_opt = if (global_id as usize) < doc_to_file_id.len() {
doc_to_file_id
.get(global_id as usize)
.copied()
.filter(|&fid| fid != u32::MAX)
} else {
snap.overlay_doc_to_file_id.get(&global_id).copied()
};
if let Some(file_id) = file_id_opt {
if !pf.file_ids.contains(file_id) {
return None;
}
} else {
if !matches_path_filter(
&rel_path,
&include_types,
&exclude_types,
opts.path_filter.as_deref(),
) {
return None;
}
}
}
let file_path = rel_path.as_path();
let file_matches = if verify_pattern.is_empty() {
verify_empty(file_path, &content, opts.skip_line_content)
} else {
match &route {
QueryRoute::Literal if opts.verify_pattern.is_none() => {
verify_literal(verify_pattern, file_path, &content, opts.skip_line_content)
}
_ => verify_regex(
compiled_re.as_ref().unwrap(),
file_path,
&content,
opts.skip_line_content,
),
}
};
if let Some(_limit) = opts.max_results {
if !file_matches.is_empty() {
match_count.fetch_add(file_matches.len(), Ordering::Relaxed);
}
}
let file = if capture_content && !file_matches.is_empty() {
Some((
rel_path,
MatchedFile {
normalized: Arc::clone(&content),
raw_len,
},
))
} else {
None
};
Some(FileResult {
file,
matches: file_matches,
})
};
#[cfg(feature = "rayon")]
let per_file: Vec<FileResult> = if let Some(ref pool) = config.thread_pool {
pool.install(|| candidates.par_iter().filter_map(do_match).collect())
} else {
candidates.par_iter().filter_map(do_match).collect()
};
#[cfg(not(feature = "rayon"))]
let per_file: Vec<FileResult> = candidates.iter().filter_map(do_match).collect();
let mut files: HashMap<PathBuf, MatchedFile> = HashMap::new();
let all_matches: Vec<SearchMatch> = if capture_content {
let mut acc = Vec::new();
for fr in per_file {
if let Some((path, mf)) = fr.file {
files.insert(path, mf);
}
acc.extend(fr.matches);
}
acc
} else {
per_file.into_iter().flat_map(|fr| fr.matches).collect()
};
let mut matches = sort_matches(all_matches);
if let Some(max) = opts.max_results {
matches.truncate(max);
if capture_content {
let live: std::collections::HashSet<&Path> =
matches.iter().map(|m| m.path.as_path()).collect();
files.retain(|p, _| live.contains(p.as_path()));
}
}
Ok(SearchOutcome { matches, files })
}
pub(crate) fn group_outcome(outcome: SearchOutcome) -> Vec<crate::FileMatches> {
let SearchOutcome { matches, mut files } = outcome;
let mut groups: Vec<crate::FileMatches> = Vec::new();
for m in matches {
if let Some(g) = groups.last_mut() {
if g.path == m.path {
g.matches.push(m);
continue;
}
}
let path = m.path.clone();
let content: Arc<[u8]> = files
.remove(&path)
.map(|mf| mf.normalized)
.unwrap_or_else(|| {
debug_assert!(
false,
"capture_content=true guarantees content for every matched path"
);
Arc::from(&[][..])
});
groups.push(crate::FileMatches {
path,
matches: vec![m],
content,
});
}
groups
}
struct FileResult {
file: Option<(PathBuf, MatchedFile)>,
matches: Vec<SearchMatch>,
}
fn sort_matches(mut matches: Vec<SearchMatch>) -> Vec<SearchMatch> {
matches.sort_unstable_by(|a, b| {
crate::path_util::cmp_path_bytes(&a.path, &b.path)
.then_with(|| a.line_number.cmp(&b.line_number))
});
matches
}
fn all_doc_ids(snap: &IndexSnapshot) -> Vec<u32> {
snap.all_doc_ids().iter().collect()
}
#[cfg(test)]
mod tests;