1use std::fmt;
34use std::time::Duration;
35
36use regex::Regex;
37
38#[derive(Clone)]
40pub enum Pattern {
41 Literal(String),
43
44 Regex(CompiledRegex),
46
47 Glob(String),
49
50 Eof,
52
53 Timeout(Duration),
55
56 Bytes(usize),
58}
59
60impl Pattern {
61 #[must_use]
63 pub fn literal(s: impl Into<String>) -> Self {
64 Self::Literal(s.into())
65 }
66
67 pub fn regex(pattern: &str) -> Result<Self, regex::Error> {
73 let regex = Regex::new(pattern)?;
74 Ok(Self::Regex(CompiledRegex::new(pattern.to_string(), regex)))
75 }
76
77 #[must_use]
79 pub fn glob(pattern: impl Into<String>) -> Self {
80 Self::Glob(pattern.into())
81 }
82
83 #[must_use]
85 pub const fn eof() -> Self {
86 Self::Eof
87 }
88
89 #[must_use]
91 pub const fn timeout(duration: Duration) -> Self {
92 Self::Timeout(duration)
93 }
94
95 #[must_use]
97 pub const fn bytes(n: usize) -> Self {
98 Self::Bytes(n)
99 }
100
101 #[must_use]
103 pub fn as_str(&self) -> &str {
104 match self {
105 Self::Literal(s) => s,
106 Self::Regex(r) => r.pattern(),
107 Self::Glob(s) => s,
108 Self::Eof => "<EOF>",
109 Self::Timeout(_) => "<TIMEOUT>",
110 Self::Bytes(_) => "<BYTES>",
111 }
112 }
113
114 #[must_use]
118 pub fn matches(&self, text: &str) -> Option<PatternMatch> {
119 match self {
120 Self::Literal(s) => text.find(s).map(|pos| PatternMatch {
121 start: pos,
122 end: pos + s.len(),
123 captures: Vec::new(),
124 }),
125 Self::Regex(r) => r.find(text).map(|m| PatternMatch {
126 start: m.start(),
127 end: m.end(),
128 captures: r.captures(text),
129 }),
130 Self::Glob(pattern) => glob_match(pattern, text).map(|pos| PatternMatch {
131 start: pos,
132 end: text.len(),
133 captures: Vec::new(),
134 }),
135 Self::Eof | Self::Timeout(_) | Self::Bytes(_) => None,
136 }
137 }
138
139 #[must_use]
141 pub const fn is_timeout(&self) -> bool {
142 matches!(self, Self::Timeout(_))
143 }
144
145 #[must_use]
147 pub const fn is_eof(&self) -> bool {
148 matches!(self, Self::Eof)
149 }
150
151 #[must_use]
153 pub const fn timeout_duration(&self) -> Option<Duration> {
154 match self {
155 Self::Timeout(d) => Some(*d),
156 _ => None,
157 }
158 }
159
160 #[must_use]
180 pub fn shell_prompt() -> Self {
181 Self::regex(r"[$#>%]\s*$").unwrap_or_else(|_| Self::Literal("$ ".to_string()))
183 }
184
185 #[must_use]
190 pub fn any_prompt() -> Self {
191 Self::Glob("*$*".to_string())
192 }
193
194 pub fn ipv4() -> Result<Self, regex::Error> {
210 Self::regex(
211 r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b",
212 )
213 }
214
215 pub fn email() -> Result<Self, regex::Error> {
230 Self::regex(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
231 }
232
233 pub fn timestamp_iso8601() -> Result<Self, regex::Error> {
241 Self::regex(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}")
242 }
243
244 #[must_use]
258 pub fn error_indicator() -> Self {
259 Self::regex(r"(?i)\b(?:error|failed|fatal)\b")
260 .unwrap_or_else(|_| Self::Glob("*[Ee]rror*".to_string()))
261 }
262
263 #[must_use]
267 pub fn success_indicator() -> Self {
268 Self::regex(r"(?i)\b(?:success|successful|passed|complete|ok)\b")
269 .unwrap_or_else(|_| Self::Glob("*[Ss]uccess*".to_string()))
270 }
271
272 #[must_use]
286 pub fn password_prompt() -> Self {
287 Self::regex(r"(?i)(?:password|passphrase)\s*:\s*$")
288 .unwrap_or_else(|_| Self::Literal("password:".to_string()))
289 }
290
291 #[must_use]
295 pub fn login_prompt() -> Self {
296 Self::regex(r"(?i)(?:login|username|user)\s*:\s*$")
297 .unwrap_or_else(|_| Self::Literal("login:".to_string()))
298 }
299
300 #[must_use]
304 pub fn confirmation_prompt() -> Self {
305 Self::regex(r"\[([yYnN])/([yYnN])\]|\(([yY]es)/([nN]o)\)")
306 .unwrap_or_else(|_| Self::Glob("*[y/n]*".to_string()))
307 }
308
309 #[must_use]
313 pub fn continue_prompt() -> Self {
314 Self::regex(r"(?i)(?:continue\s*\?|press any key|hit enter)")
315 .unwrap_or_else(|_| Self::Glob("*continue*".to_string()))
316 }
317}
318
319impl fmt::Debug for Pattern {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 match self {
322 Self::Literal(s) => write!(f, "Literal({s:?})"),
323 Self::Regex(r) => write!(f, "Regex({:?})", r.pattern()),
324 Self::Glob(s) => write!(f, "Glob({s:?})"),
325 Self::Eof => write!(f, "Eof"),
326 Self::Timeout(d) => write!(f, "Timeout({d:?})"),
327 Self::Bytes(n) => write!(f, "Bytes({n})"),
328 }
329 }
330}
331
332impl From<&str> for Pattern {
333 fn from(s: &str) -> Self {
334 Self::Literal(s.to_string())
335 }
336}
337
338impl From<String> for Pattern {
339 fn from(s: String) -> Self {
340 Self::Literal(s)
341 }
342}
343
344#[derive(Clone)]
346pub struct CompiledRegex {
347 pattern: String,
348 regex: Regex,
349}
350
351impl CompiledRegex {
352 #[must_use]
354 pub const fn new(pattern: String, regex: Regex) -> Self {
355 Self { pattern, regex }
356 }
357
358 #[must_use]
360 pub fn pattern(&self) -> &str {
361 &self.pattern
362 }
363
364 #[must_use]
366 pub fn find<'a>(&self, text: &'a str) -> Option<regex::Match<'a>> {
367 self.regex.find(text)
368 }
369
370 #[must_use]
372 pub fn captures(&self, text: &str) -> Vec<String> {
373 self.regex
374 .captures(text)
375 .map(|caps| {
376 caps.iter()
377 .skip(1) .filter_map(|m| m.map(|m| m.as_str().to_string()))
379 .collect()
380 })
381 .unwrap_or_default()
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct PatternMatch {
388 pub start: usize,
390 pub end: usize,
392 pub captures: Vec<String>,
394}
395
396impl PatternMatch {
397 #[must_use]
399 pub fn as_str<'a>(&self, text: &'a str) -> &'a str {
400 &text[self.start..self.end]
401 }
402
403 #[must_use]
405 pub const fn len(&self) -> usize {
406 self.end - self.start
407 }
408
409 #[must_use]
411 pub const fn is_empty(&self) -> bool {
412 self.start == self.end
413 }
414}
415
416#[derive(Debug, Clone, Default)]
418pub struct PatternSet {
419 patterns: Vec<NamedPattern>,
420}
421
422#[derive(Clone)]
424pub struct NamedPattern {
425 pub pattern: Pattern,
427 pub name: Option<String>,
429 pub index: usize,
431}
432
433impl fmt::Debug for NamedPattern {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 f.debug_struct("NamedPattern")
436 .field("pattern", &self.pattern)
437 .field("name", &self.name)
438 .field("index", &self.index)
439 .finish()
440 }
441}
442
443impl PatternSet {
444 #[must_use]
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 #[must_use]
452 pub fn from_patterns(patterns: Vec<Pattern>) -> Self {
453 let patterns = patterns
454 .into_iter()
455 .enumerate()
456 .map(|(index, pattern)| NamedPattern {
457 pattern,
458 name: None,
459 index,
460 })
461 .collect();
462 Self { patterns }
463 }
464
465 pub fn add(&mut self, pattern: Pattern) -> &mut Self {
467 let index = self.patterns.len();
468 self.patterns.push(NamedPattern {
469 pattern,
470 name: None,
471 index,
472 });
473 self
474 }
475
476 pub fn add_named(&mut self, name: impl Into<String>, pattern: Pattern) -> &mut Self {
478 let index = self.patterns.len();
479 self.patterns.push(NamedPattern {
480 pattern,
481 name: Some(name.into()),
482 index,
483 });
484 self
485 }
486
487 #[must_use]
489 pub const fn len(&self) -> usize {
490 self.patterns.len()
491 }
492
493 #[must_use]
495 pub const fn is_empty(&self) -> bool {
496 self.patterns.is_empty()
497 }
498
499 #[must_use]
503 pub fn find_match(&self, text: &str) -> Option<(usize, PatternMatch)> {
504 let mut best_match: Option<(usize, PatternMatch)> = None;
505
506 for (idx, named) in self.patterns.iter().enumerate() {
507 if let Some(m) = named.pattern.matches(text) {
508 match &best_match {
509 None => best_match = Some((idx, m)),
510 Some((_, current)) if m.start < current.start => {
511 best_match = Some((idx, m));
512 }
513 _ => {}
514 }
515 }
516 }
517
518 best_match
519 }
520
521 #[must_use]
523 pub fn get(&self, index: usize) -> Option<&NamedPattern> {
524 self.patterns.get(index)
525 }
526
527 #[must_use]
529 pub fn min_timeout(&self) -> Option<Duration> {
530 self.patterns
531 .iter()
532 .filter_map(|p| p.pattern.timeout_duration())
533 .min()
534 }
535
536 #[must_use]
538 pub fn has_eof(&self) -> bool {
539 self.patterns.iter().any(|p| p.pattern.is_eof())
540 }
541
542 pub fn iter(&self) -> impl Iterator<Item = &NamedPattern> {
544 self.patterns.iter()
545 }
546}
547
548fn glob_match(pattern: &str, text: &str) -> Option<usize> {
552 let pattern_chars: Vec<char> = pattern.chars().collect();
553 let text_chars: Vec<char> = text.chars().collect();
554
555 (0..=text_chars.len()).find(|&start| glob_match_from(&pattern_chars, &text_chars[start..]))
556}
557
558const fn glob_match_from(pattern: &[char], text: &[char]) -> bool {
559 let mut p = 0;
560 let mut t = 0;
561 let mut star_p = None;
562 let mut star_t = 0;
563
564 while p < pattern.len() {
565 if pattern[p] == '*' {
566 star_p = Some(p);
567 star_t = t;
568 p += 1;
569 } else if t < text.len() && (pattern[p] == '?' || pattern[p] == text[t]) {
570 p += 1;
571 t += 1;
572 } else if let Some(sp) = star_p {
573 p = sp + 1;
574 star_t += 1;
575 if star_t > text.len() {
576 return false;
577 }
578 t = star_t;
579 } else {
580 return false;
581 }
582 }
583
584 true
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn literal_pattern_matches() {
595 let pattern = Pattern::literal("hello");
596 let result = pattern.matches("say hello world");
597 assert!(result.is_some());
598 let m = result.unwrap();
599 assert_eq!(m.start, 4);
600 assert_eq!(m.end, 9);
601 }
602
603 #[test]
604 fn regex_pattern_matches() {
605 let pattern = Pattern::regex(r"\d+").unwrap();
606 let result = pattern.matches("test 123 value");
607 assert!(result.is_some());
608 let m = result.unwrap();
609 assert_eq!(m.as_str("test 123 value"), "123");
610 }
611
612 #[test]
613 fn regex_pattern_captures() {
614 let pattern = Pattern::regex(r"(\w+)@(\w+)").unwrap();
615 let result = pattern.matches("email: user@domain here");
616 assert!(result.is_some());
617 let m = result.unwrap();
618 assert_eq!(m.captures, vec!["user", "domain"]);
619 }
620
621 #[test]
622 fn glob_pattern_matches() {
623 let pattern = Pattern::glob("hello*world");
624 let result = pattern.matches("say hello beautiful world!");
625 assert!(result.is_some());
626 }
627
628 #[test]
629 fn pattern_set_finds_first() {
630 let mut set = PatternSet::new();
631 set.add(Pattern::literal("world"))
632 .add(Pattern::literal("hello"));
633
634 let result = set.find_match("hello world");
635 assert!(result.is_some());
636 let (idx, _) = result.unwrap();
637 assert_eq!(idx, 1);
639 }
640
641 #[test]
642 fn pattern_set_min_timeout() {
643 let mut set = PatternSet::new();
644 set.add(Pattern::timeout(Duration::from_secs(10)))
645 .add(Pattern::timeout(Duration::from_secs(5)));
646
647 assert_eq!(set.min_timeout(), Some(Duration::from_secs(5)));
648 }
649}