rust_expect/expect/
matcher.rs1use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use super::buffer::RingBuffer;
10use super::cache::RegexCache;
11use super::pattern::{Pattern, PatternSet};
12use crate::types::Match;
13
14pub struct Matcher {
16 buffer: RingBuffer,
18 cache: Arc<RegexCache>,
20 default_timeout: Duration,
22 search_window: Option<usize>,
24}
25
26impl Matcher {
27 #[must_use]
29 pub fn new(buffer_size: usize) -> Self {
30 Self {
31 buffer: RingBuffer::new(buffer_size),
32 cache: Arc::new(RegexCache::with_default_size()),
33 default_timeout: Duration::from_secs(30),
34 search_window: None,
35 }
36 }
37
38 #[must_use]
40 pub fn with_cache(buffer_size: usize, cache: Arc<RegexCache>) -> Self {
41 Self {
42 buffer: RingBuffer::new(buffer_size),
43 cache,
44 default_timeout: Duration::from_secs(30),
45 search_window: None,
46 }
47 }
48
49 pub const fn set_default_timeout(&mut self, timeout: Duration) {
51 self.default_timeout = timeout;
52 }
53
54 pub const fn set_search_window(&mut self, size: Option<usize>) {
59 self.search_window = size;
60 }
61
62 pub fn append(&mut self, data: &[u8]) {
64 self.buffer.append(data);
65 }
66
67 #[must_use]
69 pub const fn buffer(&self) -> &RingBuffer {
70 &self.buffer
71 }
72
73 #[must_use]
75 pub fn buffer_str(&mut self) -> String {
76 self.buffer.as_str_lossy()
77 }
78
79 pub fn clear(&mut self) {
81 self.buffer.clear();
82 }
83
84 #[must_use]
86 pub fn try_match(&mut self, pattern: &Pattern) -> Option<MatchResult> {
87 let text = self.get_search_text();
88
89 match pattern {
90 Pattern::Literal(s) => text.find(s).map(|pos| MatchResult {
91 pattern_index: 0,
92 start: self.adjust_position(pos),
93 end: self.adjust_position(pos + s.len()),
94 captures: Vec::new(),
95 }),
96 Pattern::Regex(compiled) => compiled.find(&text).map(|m| {
97 let captures = compiled.captures(&text);
98 MatchResult {
99 pattern_index: 0,
100 start: self.adjust_position(m.start()),
101 end: self.adjust_position(m.end()),
102 captures,
103 }
104 }),
105 Pattern::Glob(glob) => {
106 self.try_glob_match(glob, &text)
107 .map(|(start, end)| MatchResult {
108 pattern_index: 0,
109 start: self.adjust_position(start),
110 end: self.adjust_position(end),
111 captures: Vec::new(),
112 })
113 }
114 Pattern::Eof | Pattern::Timeout(_) | Pattern::Bytes(_) => None,
115 }
116 }
117
118 #[must_use]
120 pub fn try_match_any(&mut self, patterns: &PatternSet) -> Option<MatchResult> {
121 let text = self.get_search_text();
122 let mut best: Option<MatchResult> = None;
123
124 for (idx, named) in patterns.iter().enumerate() {
125 if let Some(pm) = named.pattern.matches(&text) {
126 let result = MatchResult {
127 pattern_index: idx,
128 start: self.adjust_position(pm.start),
129 end: self.adjust_position(pm.end),
130 captures: pm.captures,
131 };
132
133 match &best {
134 None => best = Some(result),
135 Some(current) if result.start < current.start => best = Some(result),
136 _ => {}
137 }
138 }
139 }
140
141 best
142 }
143
144 pub fn consume_match(&mut self, result: &MatchResult) -> Match {
146 let before = self.buffer.consume_before(result.start);
147 let matched_bytes = self.buffer.consume(result.end - result.start);
148 let matched = String::from_utf8_lossy(&matched_bytes).into_owned();
149 let after = self.buffer_str();
150
151 Match::new(result.pattern_index, matched, before, after)
152 .with_captures(result.captures.clone())
153 }
154
155 #[must_use]
157 pub fn get_timeout(&self, patterns: &PatternSet) -> Duration {
158 patterns.min_timeout().unwrap_or(self.default_timeout)
159 }
160
161 #[must_use]
163 pub const fn cache(&self) -> &Arc<RegexCache> {
164 &self.cache
165 }
166
167 fn get_search_text(&mut self) -> String {
169 match self.search_window {
170 Some(window) => {
171 let tail = self.buffer.tail(window);
172 String::from_utf8_lossy(&tail).into_owned()
173 }
174 None => self.buffer.as_str_lossy(),
175 }
176 }
177
178 fn adjust_position(&self, pos: usize) -> usize {
180 match self.search_window {
181 Some(window) => {
182 let buffer_len = self.buffer.len();
183 let offset = buffer_len.saturating_sub(window);
184 offset + pos
185 }
186 None => pos,
187 }
188 }
189
190 #[allow(clippy::unused_self)]
192 fn try_glob_match(&self, pattern: &str, text: &str) -> Option<(usize, usize)> {
193 if let Some(rest) = pattern.strip_prefix('*') {
196 if let Some(inner) = rest.strip_suffix('*') {
197 text.find(inner).map(|pos| (pos, pos + inner.len()))
199 } else {
200 let suffix = rest;
202 if text.ends_with(suffix) {
203 let start = text.len() - suffix.len();
204 Some((start, text.len()))
205 } else {
206 None
207 }
208 }
209 } else if let Some(prefix) = pattern.strip_suffix('*') {
210 if text.starts_with(prefix) {
212 Some((0, prefix.len()))
213 } else {
214 None
215 }
216 } else {
217 text.find(pattern).map(|pos| (pos, pos + pattern.len()))
218 }
219 }
220}
221
222impl Default for Matcher {
223 fn default() -> Self {
224 Self::new(super::buffer::DEFAULT_CAPACITY)
225 }
226}
227
228#[derive(Debug, Clone)]
230pub struct MatchResult {
231 pub pattern_index: usize,
233 pub start: usize,
235 pub end: usize,
237 pub captures: Vec<String>,
239}
240
241impl MatchResult {
242 #[must_use]
244 pub const fn len(&self) -> usize {
245 self.end - self.start
246 }
247
248 #[must_use]
250 pub const fn is_empty(&self) -> bool {
251 self.start == self.end
252 }
253}
254
255pub struct ExpectState {
257 patterns: PatternSet,
259 start_time: Instant,
261 timeout: Duration,
263 eof_detected: bool,
265}
266
267impl ExpectState {
268 #[must_use]
270 pub fn new(patterns: PatternSet, timeout: Duration) -> Self {
271 Self {
272 patterns,
273 start_time: Instant::now(),
274 timeout,
275 eof_detected: false,
276 }
277 }
278
279 #[must_use]
281 pub fn is_timed_out(&self) -> bool {
282 self.start_time.elapsed() >= self.timeout
283 }
284
285 #[must_use]
287 pub fn remaining_time(&self) -> Duration {
288 self.timeout.saturating_sub(self.start_time.elapsed())
289 }
290
291 pub const fn set_eof(&mut self) {
293 self.eof_detected = true;
294 }
295
296 #[must_use]
298 pub const fn is_eof(&self) -> bool {
299 self.eof_detected
300 }
301
302 #[must_use]
304 pub const fn patterns(&self) -> &PatternSet {
305 &self.patterns
306 }
307
308 #[must_use]
310 pub fn expects_eof(&self) -> bool {
311 self.patterns.has_eof()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn matcher_literal() {
321 let mut matcher = Matcher::new(1024);
322 matcher.append(b"hello world");
323
324 let pattern = Pattern::literal("world");
325 let result = matcher.try_match(&pattern);
326 assert!(result.is_some());
327
328 let m = result.unwrap();
329 assert_eq!(m.start, 6);
330 assert_eq!(m.end, 11);
331 }
332
333 #[test]
334 fn matcher_regex() {
335 let mut matcher = Matcher::new(1024);
336 matcher.append(b"value: 42");
337
338 let pattern = Pattern::regex(r"\d+").unwrap();
339 let result = matcher.try_match(&pattern);
340 assert!(result.is_some());
341
342 let m = result.unwrap();
343 assert_eq!(m.start, 7);
344 assert_eq!(m.end, 9);
345 }
346
347 #[test]
348 fn matcher_consume() {
349 let mut matcher = Matcher::new(1024);
350 matcher.append(b"prefix|match|suffix");
351
352 let pattern = Pattern::literal("match");
353 let result = matcher.try_match(&pattern).unwrap();
354 let m = matcher.consume_match(&result);
355
356 assert_eq!(m.before, "prefix|");
357 assert_eq!(m.matched, "match");
358 assert_eq!(m.after, "|suffix");
359 }
360
361 #[test]
362 fn matcher_pattern_set() {
363 let mut matcher = Matcher::new(1024);
364 matcher.append(b"error: something went wrong");
365
366 let mut patterns = PatternSet::new();
367 patterns
368 .add(Pattern::literal("success"))
369 .add(Pattern::literal("error"));
370
371 let result = matcher.try_match_any(&patterns);
372 assert!(result.is_some());
373 assert_eq!(result.unwrap().pattern_index, 1);
374 }
375
376 #[test]
377 fn expect_state_timeout() {
378 let patterns = PatternSet::from_patterns(vec![Pattern::literal("test")]);
379 let state = ExpectState::new(patterns, Duration::from_millis(10));
380
381 assert!(!state.is_timed_out());
382 std::thread::sleep(Duration::from_millis(20));
383 assert!(state.is_timed_out());
384 }
385}