Skip to main content

stix_matcher/
pattern_ops.rs

1//! The `LIKE` (SQL wildcard) and `MATCHES` (regex) operators.
2
3use regex::Regex;
4
5/// Tests whether `value` matches the SQL-style `LIKE` pattern.
6///
7/// `%` matches any run of characters and `_` exactly one; all other characters are
8/// literal. The pattern is anchored (must match the whole value). An invalid
9/// translation never matches.
10pub fn like_matches(value: &str, pattern: &str) -> bool {
11    let mut regex = String::with_capacity(pattern.len() * 2 + 2);
12    regex.push('^');
13    for ch in pattern.chars() {
14        match ch {
15            '%' => regex.push_str(".*"),
16            '_' => regex.push('.'),
17            other => regex.push_str(&regex::escape(&other.to_string())),
18        }
19    }
20    regex.push('$');
21    match Regex::new(&regex) {
22        Ok(re) => re.is_match(value),
23        Err(_) => false,
24    }
25}
26
27/// Tests whether `value` matches the regular expression (the `MATCHES` operator).
28///
29/// The match is unanchored, mirroring the reference implementation. An invalid
30/// regex never matches (returns false rather than erroring).
31pub fn regex_matches(value: &str, pattern: &str) -> bool {
32    match Regex::new(pattern) {
33        Ok(re) => re.is_match(value),
34        Err(_) => false,
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn like_percent_matches_any_run() {
44        assert!(like_matches("foobar.evil.example", "%.evil.example"));
45        assert!(!like_matches("foobar.good.example", "%.evil.example"));
46    }
47
48    #[test]
49    fn like_underscore_matches_single_char() {
50        assert!(like_matches("cat", "c_t"));
51        assert!(!like_matches("coat", "c_t"));
52    }
53
54    #[test]
55    fn like_escapes_regex_metachars() {
56        // '.' in the pattern is a literal dot, not "any char".
57        assert!(like_matches("a.b", "a.b"));
58        assert!(!like_matches("axb", "a.b"));
59    }
60
61    #[test]
62    fn matches_uses_regex() {
63        assert!(regex_matches("invoice12", "invoice[0-9]+"));
64        assert!(!regex_matches("invoice", "invoice[0-9]+"));
65    }
66
67    #[test]
68    fn invalid_regex_does_not_match() {
69        assert!(!regex_matches("anything", "("));
70    }
71}