sift_core/search/query/
matcher.rs1use 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 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}