Skip to main content

rust_expect/expect/
matcher.rs

1//! Pattern matching engine for expect operations.
2//!
3//! This module provides the core matching engine that combines
4//! patterns, buffers, and timeouts into a cohesive expect operation.
5
6use 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
14/// The pattern matching engine.
15pub struct Matcher {
16    /// The output buffer.
17    buffer: RingBuffer,
18    /// Regex cache for compiled patterns.
19    cache: Arc<RegexCache>,
20    /// Default timeout for expect operations.
21    default_timeout: Duration,
22    /// Search window size (for performance optimization).
23    search_window: Option<usize>,
24}
25
26impl Matcher {
27    /// Create a new matcher with the specified buffer size.
28    #[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    /// Create a new matcher with shared regex cache.
39    #[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    /// Set the default timeout.
50    pub const fn set_default_timeout(&mut self, timeout: Duration) {
51        self.default_timeout = timeout;
52    }
53
54    /// Set the search window size.
55    ///
56    /// When set, pattern matching will only search the last N bytes
57    /// of the buffer, improving performance for large buffers.
58    pub const fn set_search_window(&mut self, size: Option<usize>) {
59        self.search_window = size;
60    }
61
62    /// Append data to the buffer.
63    pub fn append(&mut self, data: &[u8]) {
64        self.buffer.append(data);
65    }
66
67    /// Get the current buffer.
68    #[must_use]
69    pub const fn buffer(&self) -> &RingBuffer {
70        &self.buffer
71    }
72
73    /// Get the current buffer contents as a string.
74    #[must_use]
75    pub fn buffer_str(&mut self) -> String {
76        self.buffer.as_str_lossy()
77    }
78
79    /// Clear the buffer.
80    pub fn clear(&mut self) {
81        self.buffer.clear();
82    }
83
84    /// Try to match a single pattern against the buffer.
85    #[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            // `Bytes(n)` matches once at least `n` raw bytes are buffered, and
115            // consumes the first `n` of them. It is resolved here (not in
116            // `Pattern::matches`) because it depends on the raw buffer length
117            // rather than the search text.
118            Pattern::Bytes(n) => (self.buffer.len() >= *n).then_some(MatchResult {
119                pattern_index: 0,
120                start: 0,
121                end: *n,
122                captures: Vec::new(),
123            }),
124            Pattern::Eof | Pattern::Timeout(_) => None,
125        }
126    }
127
128    /// Try to match any pattern from a set against the buffer.
129    #[must_use]
130    pub fn try_match_any(&mut self, patterns: &PatternSet) -> Option<MatchResult> {
131        let text = self.get_search_text();
132        let buffer_len = self.buffer.len();
133        let mut best: Option<MatchResult> = None;
134
135        for (idx, named) in patterns.iter().enumerate() {
136            // `Bytes(n)` depends on the raw buffer length, so it is matched
137            // directly here rather than via `Pattern::matches` (which only sees
138            // the search text and always returns `None` for `Bytes`).
139            let result = if let Pattern::Bytes(n) = &named.pattern {
140                (buffer_len >= *n).then_some(MatchResult {
141                    pattern_index: idx,
142                    start: 0,
143                    end: *n,
144                    captures: Vec::new(),
145                })
146            } else {
147                named.pattern.matches(&text).map(|pm| MatchResult {
148                    pattern_index: idx,
149                    start: self.adjust_position(pm.start),
150                    end: self.adjust_position(pm.end),
151                    captures: pm.captures,
152                })
153            };
154
155            if let Some(result) = result {
156                match &best {
157                    None => best = Some(result),
158                    Some(current) if result.start < current.start => best = Some(result),
159                    _ => {}
160                }
161            }
162        }
163
164        best
165    }
166
167    /// Consume matched content from the buffer and return a Match.
168    pub fn consume_match(&mut self, result: &MatchResult) -> Match {
169        let before = self.buffer.consume_before(result.start);
170        let matched_bytes = self.buffer.consume(result.end - result.start);
171        let matched = String::from_utf8_lossy(&matched_bytes).into_owned();
172        let after = self.buffer_str();
173
174        Match::new(result.pattern_index, matched, before, after)
175            .with_captures(result.captures.clone())
176    }
177
178    /// Get the timeout for a pattern set.
179    #[must_use]
180    pub fn get_timeout(&self, patterns: &PatternSet) -> Duration {
181        patterns.min_timeout().unwrap_or(self.default_timeout)
182    }
183
184    /// Get the regex cache.
185    #[must_use]
186    pub const fn cache(&self) -> &Arc<RegexCache> {
187        &self.cache
188    }
189
190    /// Get the text to search, applying search window if set.
191    fn get_search_text(&mut self) -> String {
192        match self.search_window {
193            Some(window) => {
194                let tail = self.buffer.tail(window);
195                String::from_utf8_lossy(&tail).into_owned()
196            }
197            None => self.buffer.as_str_lossy(),
198        }
199    }
200
201    /// Adjust position when using search window.
202    fn adjust_position(&self, pos: usize) -> usize {
203        match self.search_window {
204            Some(window) => {
205                let buffer_len = self.buffer.len();
206                let offset = buffer_len.saturating_sub(window);
207                offset + pos
208            }
209            None => pos,
210        }
211    }
212
213    /// Simple glob matching.
214    #[allow(clippy::unused_self)]
215    fn try_glob_match(&self, pattern: &str, text: &str) -> Option<(usize, usize)> {
216        // Convert glob to a simple search
217        // For now, just handle * as prefix/suffix
218        if let Some(rest) = pattern.strip_prefix('*') {
219            if let Some(inner) = rest.strip_suffix('*') {
220                // Pattern like *inner*
221                text.find(inner).map(|pos| (pos, pos + inner.len()))
222            } else {
223                // Pattern like *suffix
224                let suffix = rest;
225                if text.ends_with(suffix) {
226                    let start = text.len() - suffix.len();
227                    Some((start, text.len()))
228                } else {
229                    None
230                }
231            }
232        } else if let Some(prefix) = pattern.strip_suffix('*') {
233            // Pattern like prefix*
234            if text.starts_with(prefix) {
235                Some((0, prefix.len()))
236            } else {
237                None
238            }
239        } else {
240            text.find(pattern).map(|pos| (pos, pos + pattern.len()))
241        }
242    }
243}
244
245impl Default for Matcher {
246    fn default() -> Self {
247        Self::new(super::buffer::DEFAULT_CAPACITY)
248    }
249}
250
251/// Result of a pattern match.
252#[derive(Debug, Clone)]
253pub struct MatchResult {
254    /// Index of the pattern that matched.
255    pub pattern_index: usize,
256    /// Start position in the buffer.
257    pub start: usize,
258    /// End position in the buffer.
259    pub end: usize,
260    /// Capture groups.
261    pub captures: Vec<String>,
262}
263
264impl MatchResult {
265    /// Get the length of the match.
266    #[must_use]
267    pub const fn len(&self) -> usize {
268        self.end - self.start
269    }
270
271    /// Check if the match is empty.
272    #[must_use]
273    pub const fn is_empty(&self) -> bool {
274        self.start == self.end
275    }
276}
277
278/// State machine for async expect operations.
279pub struct ExpectState {
280    /// The patterns being matched.
281    patterns: PatternSet,
282    /// Start time of the expect operation.
283    start_time: Instant,
284    /// Timeout duration.
285    timeout: Duration,
286    /// Whether EOF has been detected.
287    eof_detected: bool,
288}
289
290impl ExpectState {
291    /// Create a new expect state.
292    #[must_use]
293    pub fn new(patterns: PatternSet, timeout: Duration) -> Self {
294        Self {
295            patterns,
296            start_time: Instant::now(),
297            timeout,
298            eof_detected: false,
299        }
300    }
301
302    /// Check if the operation has timed out.
303    #[must_use]
304    pub fn is_timed_out(&self) -> bool {
305        self.start_time.elapsed() >= self.timeout
306    }
307
308    /// Get the remaining time until timeout.
309    #[must_use]
310    pub fn remaining_time(&self) -> Duration {
311        self.timeout.saturating_sub(self.start_time.elapsed())
312    }
313
314    /// Mark EOF as detected.
315    pub const fn set_eof(&mut self) {
316        self.eof_detected = true;
317    }
318
319    /// Check if EOF was detected.
320    #[must_use]
321    pub const fn is_eof(&self) -> bool {
322        self.eof_detected
323    }
324
325    /// Get the patterns.
326    #[must_use]
327    pub const fn patterns(&self) -> &PatternSet {
328        &self.patterns
329    }
330
331    /// Check if the patterns include an EOF pattern.
332    #[must_use]
333    pub fn expects_eof(&self) -> bool {
334        self.patterns.has_eof()
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn matcher_literal() {
344        let mut matcher = Matcher::new(1024);
345        matcher.append(b"hello world");
346
347        let pattern = Pattern::literal("world");
348        let result = matcher.try_match(&pattern);
349        assert!(result.is_some());
350
351        let m = result.unwrap();
352        assert_eq!(m.start, 6);
353        assert_eq!(m.end, 11);
354    }
355
356    #[test]
357    fn matcher_regex() {
358        let mut matcher = Matcher::new(1024);
359        matcher.append(b"value: 42");
360
361        let pattern = Pattern::regex(r"\d+").unwrap();
362        let result = matcher.try_match(&pattern);
363        assert!(result.is_some());
364
365        let m = result.unwrap();
366        assert_eq!(m.start, 7);
367        assert_eq!(m.end, 9);
368    }
369
370    #[test]
371    fn matcher_consume() {
372        let mut matcher = Matcher::new(1024);
373        matcher.append(b"prefix|match|suffix");
374
375        let pattern = Pattern::literal("match");
376        let result = matcher.try_match(&pattern).unwrap();
377        let m = matcher.consume_match(&result);
378
379        assert_eq!(m.before, "prefix|");
380        assert_eq!(m.matched, "match");
381        assert_eq!(m.after, "|suffix");
382    }
383
384    #[test]
385    fn matcher_pattern_set() {
386        let mut matcher = Matcher::new(1024);
387        matcher.append(b"error: something went wrong");
388
389        let mut patterns = PatternSet::new();
390        patterns
391            .add(Pattern::literal("success"))
392            .add(Pattern::literal("error"));
393
394        let result = matcher.try_match_any(&patterns);
395        assert!(result.is_some());
396        assert_eq!(result.unwrap().pattern_index, 1);
397    }
398
399    #[test]
400    fn matcher_bytes_waits_then_matches() {
401        let mut matcher = Matcher::new(1024);
402        let pattern = Pattern::bytes(5);
403
404        // Fewer than 5 bytes: no match.
405        matcher.append(b"abc");
406        assert!(
407            matcher.try_match(&pattern).is_none(),
408            "Bytes(5) must not match with only 3 bytes buffered"
409        );
410
411        // Reaching 5 bytes: matches, consuming exactly the first 5.
412        matcher.append(b"defgh");
413        let result = matcher.try_match(&pattern).expect("Bytes(5) should match");
414        assert_eq!(result.start, 0);
415        assert_eq!(result.end, 5);
416
417        let m = matcher.consume_match(&result);
418        assert_eq!(m.matched, "abcde");
419    }
420
421    #[test]
422    fn matcher_bytes_in_pattern_set() {
423        let mut matcher = Matcher::new(1024);
424        matcher.append(b"abcdef");
425
426        let mut patterns = PatternSet::new();
427        patterns.add(Pattern::literal("zzz")).add(Pattern::bytes(4));
428
429        let result = matcher
430            .try_match_any(&patterns)
431            .expect("Bytes(4) should match in the set");
432        assert_eq!(result.pattern_index, 1);
433        assert_eq!(result.end - result.start, 4);
434    }
435
436    #[test]
437    fn expect_state_timeout() {
438        let patterns = PatternSet::from_patterns(vec![Pattern::literal("test")]);
439        let state = ExpectState::new(patterns, Duration::from_millis(10));
440
441        assert!(!state.is_timed_out());
442        std::thread::sleep(Duration::from_millis(20));
443        assert!(state.is_timed_out());
444    }
445}