Skip to main content

sift_core/search/query/
matcher.rs

1use grep_matcher::LineTerminator;
2use grep_regex::{RegexMatcher, RegexMatcherBuilder};
3use grep_searcher::{BinaryDetection, Searcher, SearcherBuilder};
4
5use super::SearchQuery;
6use crate::search::SearchError;
7use crate::search::options::BinaryMode;
8
9impl SearchQuery {
10    /// Builds a regex matcher from the compiled patterns and options.
11    ///
12    /// # Errors
13    ///
14    /// Returns `SearchError::RegexBuild` if pattern compilation fails.
15    pub fn build_matcher(&self) -> Result<RegexMatcher, SearchError> {
16        let mut builder = RegexMatcherBuilder::new();
17        builder.multi_line(true);
18        match self.opts.case_mode {
19            crate::search::options::CaseMode::Sensitive => {}
20            crate::search::options::CaseMode::Insensitive => {
21                builder.case_insensitive(true);
22            }
23            crate::search::options::CaseMode::Smart => {
24                builder.case_smart(true);
25            }
26        }
27        builder.unicode(self.opts.unicode);
28        builder.fixed_strings(self.opts.fixed_strings());
29        if self.opts.word_regexp() {
30            builder.word(true);
31        }
32        if self.opts.line_regexp() {
33            builder.whole_line(true);
34        }
35        if self.opts.regex_size_limit > 0 {
36            builder.size_limit(self.opts.regex_size_limit);
37        }
38        if self.opts.dfa_size_limit > 0 {
39            builder.dfa_size_limit(self.opts.dfa_size_limit);
40        }
41        if self.opts.crlf() {
42            builder.crlf(true);
43        }
44        if self.opts.multiline() {
45            if self.opts.multiline_dotall() {
46                builder.dot_matches_new_line(true);
47            }
48        } else {
49            builder.line_terminator(Some(b'\n'));
50        }
51        if self.opts.multiline() {
52            builder.ban_byte(None);
53        } else {
54            match self.opts.binary_mode {
55                BinaryMode::AsText => {
56                    builder.ban_byte(None);
57                }
58                _ => {
59                    builder.ban_byte(Some(b'\x00'));
60                }
61            }
62        }
63        builder
64            .build_many(&self.patterns)
65            .map_err(|e| SearchError::RegexBuild(e.to_string()))
66    }
67
68    pub fn build_searcher(
69        &self,
70        line_number: bool,
71        max_matches: Option<usize>,
72        include_context: bool,
73    ) -> Searcher {
74        let (before_context, after_context) = if include_context {
75            (self.opts.before_context, self.opts.after_context)
76        } else {
77            (0, 0)
78        };
79        let line_number = line_number || before_context > 0 || after_context > 0;
80        let mut builder = SearcherBuilder::new();
81        let binary_detection = match self.opts.binary_mode {
82            BinaryMode::Quit => BinaryDetection::quit(b'\x00'),
83            BinaryMode::SearchBinary => BinaryDetection::convert(b'\x00'),
84            BinaryMode::AsText => BinaryDetection::none(),
85        };
86        builder
87            .binary_detection(binary_detection)
88            .line_terminator(LineTerminator::byte(b'\n'))
89            .invert_match(self.opts.invert_match())
90            .line_number(line_number)
91            .before_context(before_context)
92            .after_context(after_context)
93            .max_matches(max_matches.map(|n| n as u64));
94        if self.opts.multiline() {
95            builder.multi_line(true);
96        }
97        builder.build()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::search::options::{SearchMatchFlags, SearchOptions};
105
106    fn make_search(patterns: &[&str], opts: SearchOptions) -> SearchQuery {
107        let patterns: Vec<String> = patterns.iter().map(ToString::to_string).collect();
108        SearchQuery::new(&patterns, opts).expect("compile search")
109    }
110
111    struct CollectStringSink {
112        hits: Vec<String>,
113    }
114
115    impl grep_searcher::Sink for CollectStringSink {
116        type Error = std::io::Error;
117
118        fn matched(
119            &mut self,
120            searcher: &grep_searcher::Searcher,
121            mat: &grep_searcher::SinkMatch<'_>,
122        ) -> Result<bool, Self::Error> {
123            std::hint::black_box(searcher);
124            self.hits
125                .push(String::from_utf8_lossy(mat.bytes()).into_owned());
126            Ok(true)
127        }
128    }
129
130    fn search_content(search: &SearchQuery, content: &[u8]) -> Vec<String> {
131        let matcher = search.build_matcher().expect("build matcher");
132        let mut sink = CollectStringSink { hits: Vec::new() };
133        let mut searcher = search.build_searcher(true, None, true);
134        let _ = searcher.search_slice(&matcher, content, &mut sink);
135        sink.hits
136    }
137
138    #[test]
139    fn sensitive_mode_matches_exact_case_only() {
140        use crate::search::options::CaseMode;
141        let opts = SearchOptions {
142            case_mode: CaseMode::Sensitive,
143            ..SearchOptions::default()
144        };
145        let search = make_search(&["Hello"], opts);
146        let hits = search_content(&search, b"Hello world\nhello world\nHELLO world\n");
147        assert_eq!(hits, vec!["Hello world\n"]);
148    }
149
150    #[test]
151    fn insensitive_mode_matches_case_variants() {
152        use crate::search::options::CaseMode;
153        let opts = SearchOptions {
154            case_mode: CaseMode::Insensitive,
155            ..SearchOptions::default()
156        };
157        let search = make_search(&["hello"], opts);
158        let hits = search_content(&search, b"Hello world\nhello world\nHELLO world\n");
159        assert_eq!(hits.len(), 3);
160    }
161
162    #[test]
163    fn fixed_strings_treat_metacharacters_literally() {
164        let mut opts = SearchOptions::default();
165        opts.flags |= SearchMatchFlags::FIXED_STRINGS;
166        let search = make_search(&["a.c"], opts);
167        let hits = search_content(&search, b"a.c\nabc\naXc\n");
168        assert_eq!(hits, vec!["a.c\n"]);
169    }
170
171    #[test]
172    fn word_regexp_rejects_embedded_matches() {
173        let mut opts = SearchOptions::default();
174        opts.flags |= SearchMatchFlags::WORD_REGEXP;
175        let search = make_search(&["cat"], opts);
176        let hits = search_content(&search, b"a cat here\nconcatenate\n");
177        assert_eq!(hits, vec!["a cat here\n"]);
178    }
179
180    #[test]
181    fn line_regexp_rejects_partial_line_matches() {
182        let mut opts = SearchOptions::default();
183        opts.flags |= SearchMatchFlags::LINE_REGEXP;
184        let search = make_search(&["yes"], opts);
185        let hits = search_content(&search, b"yes\noh yes sir\n");
186        assert_eq!(hits, vec!["yes\n"]);
187    }
188
189    #[test]
190    fn invalid_regex_returns_search_error() {
191        let search = make_search(&["("], SearchOptions::default());
192        let result = search.build_matcher();
193        assert!(result.is_err());
194        assert!(matches!(result.unwrap_err(), SearchError::RegexBuild(_)));
195    }
196
197    #[test]
198    fn binary_mode_as_text_builds_without_error() {
199        let opts = SearchOptions {
200            binary_mode: BinaryMode::AsText,
201            ..SearchOptions::default()
202        };
203        let search = make_search(&["hello"], opts);
204        let result = search.build_matcher();
205        assert!(result.is_ok());
206    }
207
208    #[test]
209    fn binary_mode_quit_builds_without_error() {
210        let opts = SearchOptions {
211            binary_mode: BinaryMode::Quit,
212            ..SearchOptions::default()
213        };
214        let search = make_search(&["hello"], opts);
215        let result = search.build_matcher();
216        assert!(result.is_ok());
217    }
218
219    #[test]
220    fn multiline_mode_builds_without_error() {
221        let mut opts = SearchOptions::default();
222        opts.flags |= SearchMatchFlags::MULTILINE;
223        let search = make_search(&["hello"], opts);
224        let result = search.build_matcher();
225        assert!(result.is_ok());
226    }
227
228    #[test]
229    fn crlf_mode_builds_without_error() {
230        let mut opts = SearchOptions::default();
231        opts.flags |= SearchMatchFlags::CRLF;
232        let search = make_search(&["hello"], opts);
233        let result = search.build_matcher();
234        assert!(result.is_ok());
235    }
236}