1use std::sync::Mutex;
2use std::time::Instant;
3#[cfg(test)]
4use std::{
5 io,
6 path::{Path, PathBuf},
7};
8
9#[cfg(test)]
10use grep_matcher::Matcher;
11use grep_regex::RegexMatcher;
12use grep_searcher::Searcher;
13use once_cell::sync::OnceCell;
14
15#[cfg(test)]
16use crate::candidate::Candidate;
17use crate::query::QueryFlags;
18use crate::query::QuerySpec;
19use crate::search::SearchError;
20use crate::search::SearchOutcome;
21use crate::search::emit::format::sum_candidate_file_bytes;
22use crate::search::emit::stats::{SearchStats, TextStatsCounters};
23#[cfg(test)]
24use crate::search::filter::CandidateFilter;
25#[cfg(test)]
26use crate::search::filter::config::{
27 CandidateFilterConfig, HiddenMode, IgnoreConfig, VisibilityConfig,
28};
29use crate::search::options::SearchOptions;
30use crate::search::output::SearchOutputFormat;
31#[cfg(test)]
32use crate::search::output::mode::MatchEmissionMode;
33use crate::search::output::mode::SearchMode;
34use crate::search::request::SearchExecution;
35
36pub mod matcher;
37
38type SearcherCacheEntry = ((bool, Option<usize>, usize, usize), Searcher);
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Match {
42 pub file: std::path::PathBuf,
43 pub line: usize,
44 pub text: String,
45}
46
47#[derive(Debug)]
48pub struct SearchQuery {
49 pub patterns: Vec<String>,
50 pub opts: SearchOptions,
51 pub matcher: OnceCell<RegexMatcher>,
52 pub searcher_cache: Mutex<Option<SearcherCacheEntry>>,
53}
54
55impl SearchQuery {
56 pub fn new(patterns: &[String], opts: SearchOptions) -> Result<Self, SearchError> {
62 if patterns.is_empty() {
63 return Err(SearchError::EmptyPatterns);
64 }
65 Ok(Self {
66 patterns: patterns.to_vec(),
67 opts,
68 matcher: OnceCell::new(),
69 searcher_cache: Mutex::new(None),
70 })
71 }
72
73 #[must_use]
74 pub fn patterns(&self) -> &[String] {
75 &self.patterns
76 }
77
78 pub(crate) fn spec(&self) -> QuerySpec<'_> {
79 self.build_query_spec()
80 }
81
82 pub(crate) fn search(&self, execution: &SearchExecution<'_>) -> crate::Result<SearchOutcome> {
83 if self.opts.max_results == Some(0) {
84 return Err(SearchError::InvalidMaxCount.into());
85 }
86
87 let output = execution.output;
88 let candidates = &execution.candidates;
89
90 if candidates.is_empty() {
91 return Ok(SearchOutcome {
92 matched: false,
93 stats: execution.collect_stats.then_some(SearchStats::default()),
94 });
95 }
96
97 let search_start = Instant::now();
98 let matcher = self.matcher.get_or_try_init(|| self.build_matcher())?;
99
100 let (did_match, stats) = match output.format {
101 SearchOutputFormat::Json => match output.mode {
102 SearchMode::Standard | SearchMode::OnlyMatching => {
103 let mut stats = execution.collect_stats.then_some(SearchStats::default());
104 let scan = crate::search::scan::json::JsonScan::new(
105 self,
106 matcher,
107 output,
108 search_start,
109 );
110 let json_matched = scan.run(candidates, stats.as_mut())?;
111 (json_matched, stats)
112 }
113 _ => return Err(SearchError::JsonOutputIncompatibleMode.into()),
114 },
115 SearchOutputFormat::Text => self.run_text_output(
116 candidates,
117 matcher,
118 output,
119 execution.separators,
120 search_start,
121 execution.collect_stats,
122 )?,
123 };
124
125 Ok(SearchOutcome {
126 matched: did_match,
127 stats,
128 })
129 }
130
131 fn build_query_spec(&self) -> QuerySpec<'_> {
132 let mut flags = QueryFlags::empty();
133 if self.opts.fixed_strings() {
134 flags |= QueryFlags::FIXED_STRINGS;
135 }
136 if self.opts.case_insensitive() {
137 flags |= QueryFlags::CASE_INSENSITIVE;
138 }
139 if self.opts.word_regexp() {
140 flags |= QueryFlags::WORD_REGEXP;
141 }
142 if self.opts.line_regexp() {
143 flags |= QueryFlags::LINE_REGEXP;
144 }
145 if self.opts.invert_match() {
146 flags |= QueryFlags::INVERT_MATCH;
147 }
148 QuerySpec {
149 patterns: &self.patterns,
150 flags,
151 }
152 }
153
154 fn run_text_output(
155 &self,
156 candidates: &[crate::Candidate],
157 matcher: &RegexMatcher,
158 output: crate::search::output::SearchOutput,
159 separators: &crate::search::output::style::SearchSeparators,
160 search_start: Instant,
161 collect_stats: bool,
162 ) -> crate::Result<(bool, Option<SearchStats>)> {
163 let counters = TextStatsCounters::new(collect_stats);
164
165 let did_match = match output.mode {
166 SearchMode::Standard | SearchMode::OnlyMatching => {
167 let scan = crate::search::scan::standard::StandardScan::new(
168 self, matcher, output, separators, &counters,
169 );
170 scan.run(candidates)?
171 }
172 SearchMode::Count
173 | SearchMode::CountMatches
174 | SearchMode::FilesWithMatches
175 | SearchMode::FilesWithoutMatch => {
176 let scan = crate::search::scan::summary::SummaryScan::new(
177 self, matcher, output, &counters,
178 );
179 scan.run(candidates)?
180 }
181 };
182
183 let stats = counters.finish(
184 candidates.len(),
185 sum_candidate_file_bytes(candidates),
186 search_start.elapsed(),
187 );
188
189 Ok((did_match, stats))
190 }
191
192 #[cfg(test)]
193 pub(crate) fn collect_index_matches(
194 &self,
195 index: &crate::index::Index,
196 ) -> crate::Result<Vec<crate::search::Match>> {
197 let config = CandidateFilterConfig {
198 visibility: VisibilityConfig {
199 hidden: HiddenMode::Include,
200 ignore: IgnoreConfig::default(),
201 },
202 ..CandidateFilterConfig::default()
203 };
204 let filter = CandidateFilter::new(&config, index.root())?;
205 let spec = self.build_query_spec();
206 let candidates = index.candidates(&spec).unwrap_or_default();
207 self.collect_index_candidate_paths(&filter, &candidates)
208 }
209
210 #[cfg(test)]
211 pub(crate) fn collect_walk_matches(
212 &self,
213 root: &Path,
214 ) -> crate::Result<Vec<crate::search::Match>> {
215 let root = root.canonicalize()?;
216 let mut candidates = Vec::new();
217 let walker = ignore::WalkBuilder::new(&root)
218 .follow_links(false)
219 .hidden(false)
220 .parents(false)
221 .ignore(false)
222 .git_global(false)
223 .git_ignore(false)
224 .git_exclude(false)
225 .require_git(false)
226 .build();
227 for entry in walker {
228 let entry = entry.map_err(crate::Error::Ignore)?;
229 if entry.file_type().is_some_and(|ft| ft.is_file()) {
230 let path = entry.path();
231 if path.components().any(|c| c.as_os_str() == ".sift") {
232 continue;
233 }
234 candidates.push(path.to_path_buf());
235 }
236 }
237 self.collect_walk_candidates(&candidates)
238 }
239
240 #[cfg(test)]
241 fn collect_index_candidate_paths(
242 &self,
243 filter: &CandidateFilter,
244 candidates: &[Candidate],
245 ) -> crate::Result<Vec<crate::search::Match>> {
246 let matcher = self.matcher.get_or_try_init(|| self.build_matcher())?;
247 let mut out = Vec::new();
248 let mut searcher = self.build_searcher(true, None, true);
249 for candidate in candidates {
250 if !candidate.matches(filter) {
251 continue;
252 }
253 let mut sink = CollectSink::new(
254 candidate.abs_path().to_path_buf(),
255 if self.opts.only_matching() {
256 MatchEmissionMode::OnlyMatching
257 } else {
258 MatchEmissionMode::Lines
259 },
260 matcher.clone(),
261 );
262 let _ = searcher.search_path(matcher, candidate.abs_path(), &mut sink);
263 out.extend(sink.into_matches());
264 }
265 Ok(out)
266 }
267
268 #[cfg(test)]
269 fn collect_walk_candidates(
270 &self,
271 candidates: &[PathBuf],
272 ) -> crate::Result<Vec<crate::search::Match>> {
273 let matcher = self.matcher.get_or_try_init(|| self.build_matcher())?;
274 let mut out = Vec::new();
275 let mut searcher = self.build_searcher(true, None, true);
276 for candidate in candidates {
277 let mut sink = CollectSink::new(
278 candidate.clone(),
279 if self.opts.only_matching() {
280 MatchEmissionMode::OnlyMatching
281 } else {
282 MatchEmissionMode::Lines
283 },
284 matcher.clone(),
285 );
286 let _ = searcher.search_path(matcher, candidate, &mut sink);
287 out.extend(sink.into_matches());
288 }
289 Ok(out)
290 }
291}
292
293#[cfg(test)]
294struct CollectSink {
295 path: PathBuf,
296 emission: MatchEmissionMode,
297 matcher: RegexMatcher,
298 matches: Vec<crate::search::Match>,
299}
300
301#[cfg(test)]
302impl CollectSink {
303 fn new(path: PathBuf, emission: MatchEmissionMode, matcher: RegexMatcher) -> Self {
304 Self {
305 path,
306 emission,
307 matcher,
308 matches: Vec::new(),
309 }
310 }
311
312 fn into_matches(self) -> Vec<crate::search::Match> {
313 self.matches
314 }
315}
316
317#[cfg(test)]
318impl grep_searcher::Sink for CollectSink {
319 type Error = io::Error;
320
321 fn matched(
322 &mut self,
323 _: &grep_searcher::Searcher,
324 mat: &grep_searcher::SinkMatch<'_>,
325 ) -> Result<bool, Self::Error> {
326 let line = usize::try_from(mat.line_number().unwrap_or(0)).unwrap_or(0);
327 let line_bytes = mat.bytes();
328 if matches!(self.emission, MatchEmissionMode::OnlyMatching) {
329 let _ = self
330 .matcher
331 .find_iter(line_bytes, |m: grep_matcher::Match| {
332 self.matches.push(crate::search::Match {
333 file: self.path.clone(),
334 line,
335 text: String::from_utf8_lossy(&line_bytes[m.start()..m.end()]).into_owned(),
336 });
337 true
338 });
339 } else {
340 self.matches.push(crate::search::Match {
341 file: self.path.clone(),
342 line,
343 text: String::from_utf8_lossy(line_bytes).into_owned(),
344 });
345 }
346 Ok(true)
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::search::options::SearchMatchFlags;
354
355 #[test]
356 fn case_mode_insensitive_returns_true() {
357 assert!(crate::search::options::CaseMode::Insensitive.is_case_insensitive());
358 }
359
360 #[test]
361 fn case_mode_sensitive_returns_false() {
362 assert!(!crate::search::options::CaseMode::Sensitive.is_case_insensitive());
363 }
364
365 #[test]
366 fn case_mode_smart_returns_false() {
367 assert!(!crate::search::options::CaseMode::Smart.is_case_insensitive());
368 }
369
370 #[test]
371 fn search_options_defaults() {
372 let opts = SearchOptions::default();
373 assert!(!opts.case_insensitive());
374 assert!(!opts.invert_match());
375 assert!(!opts.fixed_strings());
376 assert!(!opts.word_regexp());
377 assert!(!opts.line_regexp());
378 assert!(!opts.only_matching());
379 assert!(!opts.multiline());
380 assert!(!opts.multiline_dotall());
381 assert!(!opts.crlf());
382 assert!(!opts.precludes_trigram_index());
383 assert_eq!(opts.max_results, None);
384 assert_eq!(opts.before_context, 0);
385 assert_eq!(opts.after_context, 0);
386 assert_eq!(opts.binary_mode, crate::search::options::BinaryMode::Quit);
387 assert!(opts.unicode);
388 }
389
390 #[test]
391 fn search_options_precludes_trigram_index_only_for_invert_match() {
392 let mut opts = SearchOptions::default();
393 assert!(!opts.precludes_trigram_index());
394
395 opts.flags |= SearchMatchFlags::INVERT_MATCH;
396 assert!(opts.precludes_trigram_index());
397 }
398
399 #[test]
400 fn search_query_new_rejects_empty_patterns() {
401 let result = SearchQuery::new(&[], SearchOptions::default());
402 assert!(result.is_err());
403 }
404
405 #[test]
406 fn search_query_new_stores_patterns_and_options() {
407 let patterns = vec!["foo".to_string(), "bar".to_string()];
408 let opts = SearchOptions {
409 case_mode: crate::search::options::CaseMode::Insensitive,
410 ..SearchOptions::default()
411 };
412 let search = SearchQuery::new(&patterns, opts).expect("create search");
413 assert_eq!(search.patterns(), &patterns);
414 assert!(search.opts.case_insensitive());
415 }
416}