use super::mstr::{MStr, Pattern, Topic};
pub fn is_matching_backtracking(topic: MStr<Topic>, pattern: MStr<Pattern>) -> bool {
is_matching(topic.as_bytes(), pattern.as_bytes())
}
#[must_use]
#[inline]
pub fn is_matching(topic: &[u8], pattern: &[u8]) -> bool {
if topic.len() == pattern.len() && !pattern.contains(&b'*') && !pattern.contains(&b'?') {
return topic == pattern;
}
is_matching_greedy(topic, pattern)
}
#[inline]
fn is_matching_greedy(topic: &[u8], pattern: &[u8]) -> bool {
let mut i = 0;
let mut j = 0;
let mut star_idx: Option<usize> = None;
let mut match_idx = 0;
while i < topic.len() {
if j < pattern.len() && (pattern[j] == b'?' || pattern[j] == topic[i]) {
i += 1;
j += 1;
} else if j < pattern.len() && pattern[j] == b'*' {
star_idx = Some(j);
match_idx = i;
j += 1;
} else if let Some(si) = star_idx {
j = si + 1;
match_idx += 1;
i = match_idx;
} else {
return false;
}
}
while j < pattern.len() && pattern[j] == b'*' {
j += 1;
}
j == pattern.len()
}
#[cfg(test)]
mod tests {
use rand::{RngExt, SeedableRng, rngs::StdRng};
use regex::Regex;
use rstest::rstest;
use super::*;
#[rstest]
#[case("a", "*", true)]
#[case("a", "a", true)]
#[case("a", "b", false)]
#[case("data.quotes.BINANCE", "data.*", true)]
#[case("data.quotes.BINANCE", "data.quotes*", true)]
#[case("data.quotes.BINANCE", "data.*.BINANCE", true)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.*", true)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ETH*", true)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ETH???", false)]
#[case("data.trades.BINANCE.ETHUSD", "data.*.BINANCE.ETH???", true)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ET[HC]USDT", false)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ET[!ABC]USDT", false)]
#[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ET[^ABC]USDT", false)]
fn test_is_matching(#[case] topic: &str, #[case] pattern: &str, #[case] expected: bool) {
assert_eq!(
is_matching_backtracking(topic.into(), pattern.into()),
expected
);
}
#[rstest]
#[case(b"", b"", true)]
#[case(b"", b"*", true)]
#[case(b"", b"?", false)]
#[case(b"", b"a", false)]
#[case(b"a", b"", false)]
#[case(b"abc", b"*", true)]
#[case(b"abc", b"***", true)]
#[case(b"abc", b"???", true)]
#[case(b"abc", b"????", false)]
#[case(b"abc", b"??", false)]
#[case(b"abc", b"a**c", true)]
#[case(b"abc", b"**c", true)]
#[case(b"abc", b"a**", true)]
#[case(b"abc", b"*?*", true)]
#[case(b"ab", b"*?*", true)]
#[case(b"a", b"*?*", true)]
#[case(b"", b"*?*", false)]
#[case(b"ab", b"abc", false)]
#[case(b"ab", b"ab?", false)]
fn test_is_matching_bytes(
#[case] topic: &[u8],
#[case] pattern: &[u8],
#[case] expected: bool,
) {
assert_eq!(is_matching(topic, pattern), expected);
}
fn convert_pattern_to_regex(pattern: &str) -> String {
let mut regex = String::new();
regex.push('^');
for c in pattern.chars() {
match c {
'.' => regex.push_str("\\."),
'*' => regex.push_str(".*"),
'?' => regex.push('.'),
_ => regex.push(c),
}
}
regex.push('$');
regex
}
#[rstest]
#[case("a??.quo*es.?I?AN*ET?US*T", "^a..\\.quo.*es\\..I.AN.*ET.US.*T$")]
#[case("da?*.?u*?s??*NC**ETH?", "^da..*\\..u.*.s...*NC.*.*ETH.$")]
fn test_convert_pattern_to_regex(#[case] pat: &str, #[case] regex: &str) {
assert_eq!(convert_pattern_to_regex(pat), regex);
}
fn generate_pattern_from_topic(topic: &str, rng: &mut StdRng) -> String {
let mut pattern = String::new();
for c in topic.chars() {
let val: f64 = rng.random();
if val < 0.1 {
pattern.push('*');
}
else if val < 0.3 {
pattern.push('?');
}
else if val >= 0.5 {
pattern.push(c);
}
}
pattern
}
#[rstest]
fn test_matching_backtracking() {
let topic = "data.quotes.BINANCE.ETHUSDT";
let mut rng = StdRng::seed_from_u64(42);
for i in 0..1000 {
let pattern = generate_pattern_from_topic(topic, &mut rng);
let regex_pattern = convert_pattern_to_regex(&pattern);
let regex = Regex::new(®ex_pattern).unwrap();
assert_eq!(
is_matching_backtracking(topic.into(), pattern.as_str().into()),
regex.is_match(topic),
"Failed to match on iteration: {i}, pattern: \"{pattern}\", topic: {topic}, regex: \"{regex_pattern}\""
);
}
}
}