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