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            Pattern::Eof | Pattern::Timeout(_) | Pattern::Bytes(_) => None,
115        }
116    }
117
118    /// Try to match any pattern from a set against the buffer.
119    #[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    /// Consume matched content from the buffer and return a Match.
145    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    /// Get the timeout for a pattern set.
156    #[must_use]
157    pub fn get_timeout(&self, patterns: &PatternSet) -> Duration {
158        patterns.min_timeout().unwrap_or(self.default_timeout)
159    }
160
161    /// Get the regex cache.
162    #[must_use]
163    pub const fn cache(&self) -> &Arc<RegexCache> {
164        &self.cache
165    }
166
167    /// Get the text to search, applying search window if set.
168    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    /// Adjust position when using search window.
179    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    /// Simple glob matching.
191    #[allow(clippy::unused_self)]
192    fn try_glob_match(&self, pattern: &str, text: &str) -> Option<(usize, usize)> {
193        // Convert glob to a simple search
194        // For now, just handle * as prefix/suffix
195        if let Some(rest) = pattern.strip_prefix('*') {
196            if let Some(inner) = rest.strip_suffix('*') {
197                // Pattern like *inner*
198                text.find(inner).map(|pos| (pos, pos + inner.len()))
199            } else {
200                // Pattern like *suffix
201                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            // Pattern like prefix*
211            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/// Result of a pattern match.
229#[derive(Debug, Clone)]
230pub struct MatchResult {
231    /// Index of the pattern that matched.
232    pub pattern_index: usize,
233    /// Start position in the buffer.
234    pub start: usize,
235    /// End position in the buffer.
236    pub end: usize,
237    /// Capture groups.
238    pub captures: Vec<String>,
239}
240
241impl MatchResult {
242    /// Get the length of the match.
243    #[must_use]
244    pub const fn len(&self) -> usize {
245        self.end - self.start
246    }
247
248    /// Check if the match is empty.
249    #[must_use]
250    pub const fn is_empty(&self) -> bool {
251        self.start == self.end
252    }
253}
254
255/// State machine for async expect operations.
256pub struct ExpectState {
257    /// The patterns being matched.
258    patterns: PatternSet,
259    /// Start time of the expect operation.
260    start_time: Instant,
261    /// Timeout duration.
262    timeout: Duration,
263    /// Whether EOF has been detected.
264    eof_detected: bool,
265}
266
267impl ExpectState {
268    /// Create a new expect state.
269    #[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    /// Check if the operation has timed out.
280    #[must_use]
281    pub fn is_timed_out(&self) -> bool {
282        self.start_time.elapsed() >= self.timeout
283    }
284
285    /// Get the remaining time until timeout.
286    #[must_use]
287    pub fn remaining_time(&self) -> Duration {
288        self.timeout.saturating_sub(self.start_time.elapsed())
289    }
290
291    /// Mark EOF as detected.
292    pub const fn set_eof(&mut self) {
293        self.eof_detected = true;
294    }
295
296    /// Check if EOF was detected.
297    #[must_use]
298    pub const fn is_eof(&self) -> bool {
299        self.eof_detected
300    }
301
302    /// Get the patterns.
303    #[must_use]
304    pub const fn patterns(&self) -> &PatternSet {
305        &self.patterns
306    }
307
308    /// Check if the patterns include an EOF pattern.
309    #[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}