1use std::path::PathBuf;
2use std::time::Instant;
3#[cfg(test)]
4use std::{io, path::Path};
5
6#[cfg(test)]
7use grep_matcher::Matcher;
8use grep_regex::RegexMatcher;
9use std::sync::OnceLock;
10
11#[cfg(test)]
12use crate::Candidate;
13use crate::query::{QueryFlags, QuerySpec};
14use crate::search::SearchError;
15use crate::search::SearchOutcome;
16use crate::search::emit::stats::{SearchStats, TextStatsCounters};
17#[cfg(test)]
18use crate::search::filter::CandidateFilter;
19#[cfg(test)]
20use crate::search::filter::config::{
21 CandidateFilterConfig, HiddenMode, IgnoreConfig, VisibilityConfig,
22};
23use crate::search::options::SearchOptions;
24use crate::search::output::SearchOutputFormat;
25#[cfg(test)]
26use crate::search::output::mode::MatchEmissionMode;
27use crate::search::output::mode::SearchMode;
28use crate::search::request::SearchExecution;
29
30pub mod matcher;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Match {
34 pub file: std::path::PathBuf,
35 pub line: usize,
36 pub text: String,
37}
38
39#[derive(Debug)]
40pub struct SearchQuery {
41 patterns: Vec<String>,
42 opts: SearchOptions,
43 matcher: OnceLock<RegexMatcher>,
44}
45
46impl SearchQuery {
47 pub fn new(patterns: &[String], opts: SearchOptions) -> Result<Self, SearchError> {
53 if patterns.is_empty() {
54 return Err(SearchError::EmptyPatterns);
55 }
56 Ok(Self {
57 patterns: patterns.to_vec(),
58 opts,
59 matcher: OnceLock::new(),
60 })
61 }
62
63 #[must_use]
64 pub fn patterns(&self) -> &[String] {
65 &self.patterns
66 }
67
68 #[must_use]
69 pub const fn opts(&self) -> &SearchOptions {
70 &self.opts
71 }
72
73 pub(crate) fn build_query_spec(&self) -> QuerySpec<'_> {
74 let mut flags = QueryFlags::empty();
75 if self.opts.fixed_strings() {
76 flags |= QueryFlags::FIXED_STRINGS;
77 }
78 if self.opts.case_insensitive() {
79 flags |= QueryFlags::CASE_INSENSITIVE;
80 }
81 if self.opts.word_regexp() {
82 flags |= QueryFlags::WORD_REGEXP;
83 }
84 if self.opts.line_regexp() {
85 flags |= QueryFlags::LINE_REGEXP;
86 }
87 if self.opts.invert_match() {
88 flags |= QueryFlags::INVERT_MATCH;
89 }
90 QuerySpec {
91 patterns: &self.patterns,
92 flags,
93 }
94 }
95
96 pub(crate) fn search(
97 &self,
98 execution: &SearchExecution<'_>,
99 ) -> crate::Result<(SearchOutcome, Vec<PathBuf>)> {
100 if self.opts.max_results == Some(0) {
101 return Err(SearchError::InvalidMaxCount.into());
102 }
103
104 let output = execution.output;
105 let candidates = execution.candidates;
106
107 if candidates.is_empty() {
108 return Ok((
109 SearchOutcome {
110 matched: false,
111 stats: execution.collect.stats.then_some(SearchStats::default()),
112 },
113 Vec::new(),
114 ));
115 }
116
117 let search_start = Instant::now();
118 let matcher = self.resolve_matcher()?;
119
120 let (did_match, stats, hits) = match output.format {
121 SearchOutputFormat::Json => match output.mode {
122 SearchMode::Standard | SearchMode::OnlyMatching => {
123 let mut stats = execution.collect.stats.then_some(SearchStats::default());
124 let scan = crate::search::scan::json::JsonScan::new(
125 self,
126 matcher,
127 output,
128 search_start,
129 );
130 let json_matched = scan.run(candidates, stats.as_mut())?;
131 (json_matched, stats, Vec::new())
132 }
133 _ => return Err(SearchError::JsonOutputIncompatibleMode.into()),
134 },
135 SearchOutputFormat::Text => {
136 let (did_match, stats, hits) =
137 self.run_text_output(candidates, matcher, execution, search_start)?;
138 (did_match, stats, hits)
139 }
140 };
141
142 Ok((
143 SearchOutcome {
144 matched: did_match,
145 stats,
146 },
147 hits,
148 ))
149 }
150
151 fn resolve_matcher(&self) -> Result<&RegexMatcher, SearchError> {
152 if let Some(m) = self.matcher.get() {
153 return Ok(m);
154 }
155 let m = self.build_matcher()?;
156 let _ = self.matcher.set(m);
157 Ok(self.matcher.get().expect("just initialised"))
158 }
159
160 fn run_text_output(
161 &self,
162 candidates: &[crate::Candidate],
163 matcher: &RegexMatcher,
164 execution: &SearchExecution<'_>,
165 search_start: Instant,
166 ) -> crate::Result<(bool, Option<SearchStats>, Vec<PathBuf>)> {
167 let output = execution.output;
168 let collect = execution.collect;
169 let counters = TextStatsCounters::new(collect.stats);
170
171 let (did_match, hits) = match output.mode {
172 SearchMode::Standard | SearchMode::OnlyMatching => {
173 let scan = crate::search::scan::standard::StandardScan::new(
174 self,
175 matcher,
176 output,
177 execution.separators,
178 &counters,
179 );
180 scan.run(candidates, collect)?
181 }
182 SearchMode::Count
183 | SearchMode::CountMatches
184 | SearchMode::FilesWithMatches
185 | SearchMode::FilesWithoutMatch => {
186 let scan = crate::search::scan::summary::SummaryScan::new(
187 self, matcher, output, &counters,
188 );
189 scan.run(candidates, collect)?
190 }
191 };
192
193 let bytes_searched = if collect.stats {
194 crate::Candidate::total_file_bytes(candidates)
195 } else {
196 0
197 };
198 let stats = counters.finish(candidates.len(), bytes_searched, search_start.elapsed());
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}