Skip to main content

sift_core/search/scan/
summary.rs

1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
4
5use grep_matcher::Matcher;
6use grep_regex::RegexMatcher;
7use grep_searcher::{Searcher, Sink, SinkMatch};
8use rayon::prelude::*;
9
10use crate::Candidate;
11use crate::search::emit::format::{ANSI_PATH, ANSI_RESET};
12use crate::search::emit::result::{ChunkOutput, FileResult};
13use crate::search::emit::stats::TextStatsCounters;
14use crate::search::output::SearchOutput;
15use crate::search::output::mode::{OutputEmission, SearchMode, ZeroCountMode};
16use crate::search::output::style::FilenameMode;
17use crate::search::query::SearchQuery;
18use crate::search::request::SearchCollection;
19
20#[derive(Clone, Copy)]
21pub struct FileSummary {
22    pub matched: bool,
23    pub count: usize,
24}
25
26impl FileSummary {
27    #[must_use]
28    pub fn tally(&self, mode: SearchMode) -> usize {
29        match mode {
30            SearchMode::Count | SearchMode::CountMatches => self.count,
31            SearchMode::FilesWithMatches => usize::from(self.matched),
32            SearchMode::FilesWithoutMatch => usize::from(!self.matched),
33            SearchMode::Standard | SearchMode::OnlyMatching => 0,
34        }
35    }
36
37    #[must_use]
38    pub const fn is_success(&self, mode: SearchMode) -> bool {
39        match mode {
40            SearchMode::Count | SearchMode::CountMatches => self.count > 0,
41            SearchMode::FilesWithMatches | SearchMode::Standard | SearchMode::OnlyMatching => {
42                self.matched
43            }
44            SearchMode::FilesWithoutMatch => !self.matched,
45        }
46    }
47
48    #[must_use]
49    pub const fn had_positive_hit(&self, mode: SearchMode) -> bool {
50        match mode {
51            SearchMode::Count | SearchMode::CountMatches => self.count > 0,
52            SearchMode::FilesWithMatches => self.matched,
53            SearchMode::FilesWithoutMatch | SearchMode::Standard | SearchMode::OnlyMatching => {
54                false
55            }
56        }
57    }
58}
59
60pub struct SummarySink {
61    mode: SearchMode,
62    matcher: Option<RegexMatcher>,
63    matched: bool,
64    count: usize,
65}
66
67impl SummarySink {
68    #[must_use]
69    pub const fn new(mode: SearchMode, matcher: Option<RegexMatcher>) -> Self {
70        Self {
71            mode,
72            matcher,
73            matched: false,
74            count: 0,
75        }
76    }
77
78    #[must_use]
79    pub fn finish(self) -> FileSummary {
80        FileSummary {
81            matched: self.matched,
82            count: self.count,
83        }
84    }
85}
86
87impl Sink for SummarySink {
88    type Error = io::Error;
89
90    fn matched(&mut self, searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
91        std::hint::black_box(searcher);
92        self.matched = true;
93        if self.mode == SearchMode::CountMatches {
94            if let Some(ref matcher) = self.matcher {
95                let line = mat.bytes();
96                let mut n = 0;
97                let _ = matcher.find_iter(line, |_| {
98                    n += 1;
99                    true
100                });
101                self.count += n;
102            }
103        } else {
104            self.count += 1;
105        }
106        Ok(matches!(
107            self.mode,
108            SearchMode::Count | SearchMode::CountMatches
109        ))
110    }
111}
112
113fn summary_search_file(
114    searcher: &mut Searcher,
115    matcher: &RegexMatcher,
116    mode: SearchMode,
117    path: &Path,
118) -> FileSummary {
119    let sink_matcher = if mode == SearchMode::CountMatches {
120        Some(matcher.clone())
121    } else {
122        None
123    };
124    let mut sink = SummarySink::new(mode, sink_matcher);
125    let _ = searcher.search_path(matcher, path, &mut sink);
126    sink.finish()
127}
128
129fn write_summary_record(
130    out: &mut Vec<u8>,
131    output: SearchOutput,
132    display_path: &str,
133    result: FileSummary,
134) -> io::Result<()> {
135    if output.emission == OutputEmission::Quiet {
136        return Ok(());
137    }
138    let records = output.records;
139    match output.mode {
140        SearchMode::Count | SearchMode::CountMatches => {
141            if result.count == 0 && matches!(output.include_zero, ZeroCountMode::Omit) {
142                return Ok(());
143            }
144            let print_filename = output.lines.filename_mode != FilenameMode::Never;
145            if print_filename {
146                if records.should_color() {
147                    out.extend_from_slice(ANSI_PATH);
148                }
149                write!(out, "{display_path}")?;
150                if records.should_color() {
151                    out.extend_from_slice(ANSI_RESET);
152                }
153                writeln!(out, ":{}", result.count)?;
154            } else {
155                writeln!(out, "{}", result.count)?;
156            }
157            Ok(())
158        }
159        SearchMode::FilesWithMatches => {
160            if result.matched {
161                if records.should_color() {
162                    out.extend_from_slice(ANSI_PATH);
163                }
164                write!(out, "{display_path}")?;
165                if records.should_color() {
166                    out.extend_from_slice(ANSI_RESET);
167                }
168                records.terminator.write_to(out);
169            }
170            Ok(())
171        }
172        SearchMode::FilesWithoutMatch => {
173            if result.matched {
174                return Ok(());
175            }
176            if records.should_color() {
177                out.extend_from_slice(ANSI_PATH);
178            }
179            write!(out, "{display_path}")?;
180            if records.should_color() {
181                out.extend_from_slice(ANSI_RESET);
182            }
183            records.terminator.write_to(out);
184            Ok(())
185        }
186        SearchMode::Standard | SearchMode::OnlyMatching => unreachable!(),
187    }
188}
189
190struct SummaryWorker<'a> {
191    matcher: &'a RegexMatcher,
192    searcher: Searcher,
193    output: SearchOutput,
194    summary_counter: Option<&'a AtomicUsize>,
195    files_with_matches: Option<&'a AtomicUsize>,
196    collect_hits: bool,
197}
198
199impl<'a> SummaryWorker<'a> {
200    fn new(scan: &'a SummaryScan<'a>, collect: SearchCollection) -> Self {
201        Self {
202            searcher: scan
203                .search
204                .build_searcher(false, scan.search.opts().max_results, false),
205            matcher: scan.matcher,
206            output: scan.output,
207            summary_counter: scan.counters.primary(),
208            files_with_matches: scan.counters.files_with_matches(),
209            collect_hits: collect.hits,
210        }
211    }
212
213    fn search_candidate(&mut self, candidate: &Candidate, stop: &AtomicBool) -> FileResult {
214        if stop.load(Ordering::SeqCst) {
215            return FileResult {
216                output: ChunkOutput::empty(),
217                json_stats: None,
218                hit: None,
219            };
220        }
221
222        let result = summary_search_file(
223            &mut self.searcher,
224            self.matcher,
225            self.output.mode,
226            candidate.abs_path(),
227        );
228        if let Some(c) = self.summary_counter {
229            c.fetch_add(result.tally(self.output.mode), Ordering::Relaxed);
230        }
231        if let Some(c) = self.files_with_matches
232            && result.had_positive_hit(self.output.mode)
233        {
234            c.fetch_add(1, Ordering::Relaxed);
235        }
236        let matched = result.is_success(self.output.mode);
237        let mut bytes = Vec::new();
238        let display = candidate.display_path(
239            self.output.lines.path_display,
240            self.output.records.path_separator,
241        );
242        let _ = write_summary_record(&mut bytes, self.output, &display, result);
243        if self.output.emission == OutputEmission::Quiet && result.is_success(self.output.mode) {
244            stop.store(true, Ordering::SeqCst);
245        }
246
247        FileResult {
248            output: ChunkOutput {
249                bytes,
250                matched,
251                heading: false,
252            },
253            json_stats: None,
254            hit: (self.collect_hits && result.matched).then(|| candidate.rel_path().to_path_buf()),
255        }
256    }
257}
258
259pub struct SummaryScan<'a> {
260    search: &'a SearchQuery,
261    matcher: &'a RegexMatcher,
262    output: SearchOutput,
263    counters: &'a TextStatsCounters,
264}
265
266impl<'a> SummaryScan<'a> {
267    pub const fn new(
268        search: &'a SearchQuery,
269        matcher: &'a RegexMatcher,
270        output: SearchOutput,
271        counters: &'a TextStatsCounters,
272    ) -> Self {
273        Self {
274            search,
275            matcher,
276            output,
277            counters,
278        }
279    }
280
281    /// # Errors
282    ///
283    /// Returns an error if scanning or writing output fails.
284    pub fn run(
285        &self,
286        candidates: &[Candidate],
287        collect: SearchCollection,
288    ) -> crate::Result<(bool, Vec<PathBuf>)> {
289        let stop = AtomicBool::new(false);
290        let n = candidates.len();
291        let mut files = Vec::with_capacity(n);
292        candidates
293            .par_iter()
294            .map_init(
295                || SummaryWorker::new(self, collect),
296                |worker: &mut SummaryWorker<'_>, candidate: &Candidate| {
297                    worker.search_candidate(candidate, &stop)
298                },
299            )
300            .collect_into_vec(&mut files);
301        let mut hits = Vec::new();
302        let mut outputs = Vec::with_capacity(files.len());
303        let mut any_match = false;
304        for file in files {
305            if collect.hits
306                && let Some(hit) = file.hit
307            {
308                hits.push(hit);
309            }
310            any_match |= file.output.matched;
311            outputs.push(file.output);
312        }
313        ChunkOutput::flush_all(outputs, self.counters.bytes_printed())?;
314        Ok((any_match, hits))
315    }
316}