use core::str::Split;
pub fn topic_matches(pattern: &str, topic: &str) -> bool {
if pattern == topic {
return true;
}
if pattern == "#" {
return true;
}
if let Some(prefix) = pattern.strip_suffix(".#") {
if !prefix.contains('*') && !prefix.contains('#') {
return topic.starts_with(prefix)
&& (topic.len() == prefix.len()
|| topic.as_bytes().get(prefix.len()) == Some(&b'.'));
}
}
match_segments(pattern, topic, |p, t| p == "*" || p == t)
}
fn match_segments<F>(pattern: &str, subject: &str, covers: F) -> bool
where
F: Fn(&str, &str) -> bool,
{
let mut pat = pattern.split('.');
let mut sub = subject.split('.');
let mut resume: Option<(Split<'_, char>, Split<'_, char>)> = None;
loop {
let mut pat_rest = pat.clone();
let mut sub_rest = sub.clone();
match (pat_rest.next(), sub_rest.next()) {
(Some("#"), _) => {
pat = pat_rest;
resume = Some((pat.clone(), sub.clone()));
}
(Some(p), Some(s)) if covers(p, s) => {
pat = pat_rest;
sub = sub_rest;
}
(None, None) => return true,
_ => {
let Some((pat_after_hash, sub_at_hash)) = resume.take() else {
return false; };
let mut grown = sub_at_hash;
if grown.next().is_none() {
return false; }
pat = pat_after_hash.clone();
sub = grown.clone();
resume = Some((pat_after_hash, grown));
}
}
}
}
pub fn is_wildcard(pattern: &str) -> bool {
pattern.contains('#') || pattern.contains('*')
}
pub fn pattern_contains(grant: &str, requested: &str) -> bool {
match_segments(grant, requested, |g, r| {
if g == "*" {
r != "#"
} else {
g == r
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match() {
assert!(topic_matches("a.b.c", "a.b.c"));
assert!(!topic_matches("a.b.c", "a.b.d"));
}
#[test]
fn hash_wildcard() {
assert!(topic_matches("#", "anything.goes.here"));
assert!(topic_matches("#", "a"));
}
#[test]
fn prefix_hash_wildcard() {
assert!(topic_matches("sensors.#", "sensors.temperature.vienna"));
assert!(topic_matches("sensors.#", "sensors.humidity.berlin"));
assert!(!topic_matches("sensors.#", "commands.setpoint"));
assert!(topic_matches("sensors.#", "sensors"));
assert!(!topic_matches("sensors.#", "sensors_extra.temp"));
}
#[test]
fn star_wildcard() {
assert!(topic_matches(
"sensors.temperature.*",
"sensors.temperature.vienna"
));
assert!(topic_matches(
"sensors.temperature.*",
"sensors.temperature.berlin"
));
assert!(!topic_matches(
"sensors.temperature.*",
"sensors.humidity.vienna"
));
assert!(!topic_matches(
"sensors.temperature.*",
"sensors.temperature.a.b"
));
}
#[test]
fn star_matches_dotted_key_below_top_level() {
assert!(topic_matches("temp.*", "temp.vienna"));
assert!(topic_matches("temp.*", "temp.berlin"));
assert!(!topic_matches("temp.*", "temp"));
assert!(!topic_matches("temp.*", "temp.vienna.indoor"));
assert!(!topic_matches("temp.*", "humidity.vienna"));
}
#[test]
fn mixed_wildcards() {
assert!(topic_matches("a.*.c.#", "a.b.c.d.e.f"));
assert!(!topic_matches("a.*.c.#", "a.b.x.d"));
}
#[test]
fn non_terminal_hash_still_matches_its_suffix() {
assert!(!topic_matches("a.#.secret", "a.public"));
assert!(!topic_matches("a.#.secret", "a.b.c.public"));
assert!(!topic_matches("a.#.secret", "b.secret")); assert!(!topic_matches("a.#.secret", "a.secret.tail"));
assert!(topic_matches("a.#.secret", "a.secret"));
assert!(topic_matches("a.#.secret", "a.b.secret"));
assert!(topic_matches("a.#.secret", "a.b.c.d.secret"));
}
#[test]
fn hash_backtracks_to_find_a_later_suffix() {
assert!(topic_matches("a.#.c", "a.c.b.c"));
assert!(!topic_matches("a.#.c", "a.c.b.d"));
assert!(topic_matches("a.#.b.#.c", "a.x.y.b.z.c"));
assert!(!topic_matches("a.#.b.#.c", "a.x.y.b.z.d"));
assert!(topic_matches("a.#.b.#", "a.x.b.y.z"));
assert!(topic_matches("a.#.b.#", "a.b")); assert!(!topic_matches("a.#.b.#", "a.x.y"));
}
#[test]
fn hash_does_not_match_a_shorter_topic_than_its_suffix() {
assert!(!topic_matches("a.#.b.c", "a.b"));
assert!(!topic_matches("#.a", "b"));
assert!(topic_matches("#.a", "a")); assert!(topic_matches("#.a", "x.y.a"));
}
#[test]
fn slash_is_not_a_separator() {
assert!(!topic_matches("sensors.#", "sensors/temp/vienna"));
assert!(topic_matches("sensors/temp", "sensors/temp")); assert!(!is_wildcard("sensors/temp")); }
#[test]
fn wildcard_detection() {
assert!(is_wildcard("#"));
assert!(is_wildcard("sensors.#"));
assert!(is_wildcard("a.*.c"));
assert!(!is_wildcard("temp.vienna"));
assert!(is_wildcard("temp.*"));
}
#[test]
fn containment_denies_wildcard_escalation() {
assert!(!pattern_contains("sensors.*", "sensors.#"));
assert!(topic_matches("sensors.*", "sensors.#")); assert!(!pattern_contains("sensors.*", "sensors.temp.vienna"));
}
#[test]
fn containment_allows_when_grant_is_broader_or_equal() {
assert!(pattern_contains("#", "sensors.#"));
assert!(pattern_contains("#", "anything.deep.here"));
assert!(pattern_contains("sensors.#", "sensors.temp.#"));
assert!(pattern_contains("sensors.#", "sensors.*"));
assert!(pattern_contains("sensors.#", "sensors.temp"));
assert!(pattern_contains("sensors.#", "sensors")); assert!(pattern_contains("sensors.#", "sensors.#"));
assert!(pattern_contains("a.*.c", "a.*.c"));
assert!(pattern_contains("sensors.*", "sensors.temp"));
assert!(pattern_contains("sensors.*", "sensors.*"));
}
#[test]
fn containment_denies_out_of_scope_or_shallower() {
assert!(!pattern_contains("sensors.#", "commands.#"));
assert!(!pattern_contains("sensors.temp", "sensors.humidity"));
assert!(!pattern_contains("a.b.c", "a.b"));
assert!(!pattern_contains("sensors.temp", "sensors.*"));
assert!(!pattern_contains("sensors.temp", "sensors.#"));
}
#[test]
fn containment_honours_a_non_terminal_hash_in_the_grant() {
assert!(!pattern_contains("a.#.secret", "a.public"));
assert!(!pattern_contains("a.#.secret", "a.b.public"));
assert!(!pattern_contains("a.#.secret", "a.#")); assert!(!pattern_contains("a.#.secret", "a.*"));
assert!(pattern_contains("a.#.secret", "a.secret"));
assert!(pattern_contains("a.#.secret", "a.b.secret"));
assert!(pattern_contains("a.#.secret", "a.#.secret"));
assert!(pattern_contains("a.#.secret", "a.b.#.secret"));
}
#[test]
fn containment_denies_requests_reaching_past_an_interior_hash() {
assert!(!pattern_contains("a.#.b", "a.#"));
assert!(!pattern_contains("a.#.b", "#"));
assert!(!pattern_contains("a.#.b.c", "a.b"));
assert!(!pattern_contains("a.#.*", "a.#"));
assert!(pattern_contains("a.#.*", "a.b.c"));
assert!(!pattern_contains("a.#.*", "a"));
}
#[test]
fn containment_is_conservative_where_a_grant_star_meets_a_request_hash() {
assert!(!pattern_contains("a.*.#", "a.#.a"));
assert!(!pattern_contains("a.#.*", "a.b.#"));
for topic in ["a.a", "a.b.a", "a.b.c.a"] {
assert!(topic_matches("a.#.a", topic));
assert!(topic_matches("a.*.#", topic));
}
assert!(pattern_contains("a.#", "a.b.#"));
assert!(pattern_contains("a.#.b", "a.x.b"));
assert!(pattern_contains("a.*", "a.b"));
}
#[test]
fn containment_matches_topic_matches_on_concrete_requests() {
let grants = [
"#",
"sensors.#",
"sensors.*",
"sensors.temp",
"*.temp",
"sensors.#.temp",
"#.temp",
"sensors.#.*",
"a.#.b.#.c",
];
let topics = [
"sensors.temp",
"sensors.temp.vienna",
"sensors",
"commands.on",
"a.temp",
"sensors.rack.temp",
"sensors.rack.shelf.temp",
"a.x.b.y.c",
"a.b.c",
];
for g in grants {
for t in topics {
assert_eq!(
pattern_contains(g, t),
topic_matches(g, t),
"grant={g:?} topic={t:?}"
);
}
}
}
}