1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//! Include/exclude name selectors. `*` is a glob wildcard; patterns are anchored.
use regex::Regex;
use crate::error::ReplicatorError;
/// Matches topic or consumer-group names against include/exclude pattern lists.
///
/// Rules:
/// - `*` is a glob wildcard (maps to `.*` in regex).
/// - Patterns are fully anchored (`^...$`).
/// - Exclude wins over include.
/// - An empty include list matches nothing.
#[derive(Debug, Clone)]
pub struct Selector {
include: Vec<Regex>,
exclude: Vec<Regex>,
}
fn glob_to_regex(pat: &str) -> Result<Regex, ReplicatorError> {
let mut re = String::from("^");
for ch in pat.chars() {
match ch {
'*' => re.push_str(".*"),
c if "\\.[]{}()+?^$|".contains(c) => {
re.push('\\');
re.push(c);
}
c => re.push(c),
}
}
re.push('$');
Regex::new(&re).map_err(|e| ReplicatorError::Config(format!("bad selector `{pat}`: {e}")))
}
impl Selector {
/// Compile include and exclude glob patterns into a `Selector`.
///
/// # Errors
/// Returns [`ReplicatorError::Config`] if any pattern produces an invalid regex.
pub fn compile(include: &[String], exclude: &[String]) -> Result<Self, ReplicatorError> {
Ok(Self {
include: include
.iter()
.map(|p| glob_to_regex(p))
.collect::<Result<_, _>>()?,
exclude: exclude
.iter()
.map(|p| glob_to_regex(p))
.collect::<Result<_, _>>()?,
})
}
/// Returns `true` if `name` is matched by the include list and not the exclude list.
#[must_use]
pub fn matches(&self, name: &str) -> bool {
if self.exclude.iter().any(|r| r.is_match(name)) {
return false;
}
self.include.iter().any(|r| r.is_match(name))
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use super::*;
#[test]
fn include_exclude_with_glob_and_regex() {
let s = Selector::compile(
&["orders".into(), "telemetry.*".into()],
&["*.internal".into()],
)
.unwrap();
assert!(s.matches("orders"));
assert!(s.matches("telemetry.cpu"));
assert!(!s.matches("payments"));
assert!(!s.matches("telemetry.internal")); // excluded wins
}
#[test]
fn empty_include_matches_nothing() {
let s = Selector::compile(&[], &[]).unwrap();
assert!(!s.matches("anything"));
}
#[test]
fn regex_metacharacters_are_escaped_to_literals() {
// A literal `.` in a pattern must match only a literal `.`, never an
// arbitrary character. If the metachar-escape guard is disabled, `a.b`
// compiles to the regex `^a.b$` and would wrongly match `axb`.
let s = Selector::compile(&["a.b".into()], &[]).unwrap();
assert!(s.matches("a.b"));
assert!(!s.matches("axb"));
}
}