use crate::error::{MqttError, Result};
use crate::prelude::{format, Vec};
#[must_use]
pub fn matches(topic: &str, filter: &str) -> bool {
if topic.is_empty() {
return false;
}
if !is_valid_topic(topic) || !is_valid_filter(filter) {
return false;
}
if topic == filter {
return true;
}
if topic.starts_with('$') && (filter.starts_with('#') || filter.starts_with('+')) {
return false;
}
if filter == "#" {
return true;
}
let topic_parts: Vec<&str> = topic.split('/').collect();
let filter_parts: Vec<&str> = filter.split('/').collect();
match_parts(&topic_parts, &filter_parts)
}
fn match_parts(topic_parts: &[&str], filter_parts: &[&str]) -> bool {
match (topic_parts.first(), filter_parts.first()) {
(None, None) => true,
(_, Some(&"#")) => filter_parts.len() == 1,
(None, Some(_)) | (Some(_), None) => false,
(Some(&topic_part), Some(&filter_part)) => {
let level_match = filter_part == "+" || filter_part == topic_part;
level_match && match_parts(&topic_parts[1..], &filter_parts[1..])
}
}
}
#[must_use]
pub fn is_valid_topic(topic: &str) -> bool {
!topic.contains('\0') && !topic.contains('+') && !topic.contains('#') && topic.len() <= 65535
}
#[must_use]
pub fn is_valid_filter(filter: &str) -> bool {
if filter.is_empty() || filter.contains('\0') || filter.len() > 65535 {
return false;
}
let parts: Vec<&str> = filter.split('/').collect();
for (i, part) in parts.iter().enumerate() {
if part.contains('#') {
return *part == "#" && i == parts.len() - 1;
}
if part.contains('+') && *part != "+" {
return false;
}
}
true
}
pub fn validate_topic(topic: &str) -> Result<()> {
if !is_valid_topic(topic) {
return Err(MqttError::InvalidTopicName(format!(
"Invalid topic: {}",
if topic.is_empty() {
"empty topic"
} else if topic.contains('+') || topic.contains('#') {
"wildcards not allowed in topic names"
} else if topic.contains('\0') {
"null character not allowed"
} else if topic.len() > 65535 {
"topic too long"
} else {
"unknown error"
}
)));
}
Ok(())
}
pub fn validate_filter(filter: &str) -> Result<()> {
if !is_valid_filter(filter) {
return Err(MqttError::InvalidTopicFilter(format!(
"Invalid filter: {}",
if filter.is_empty() {
"empty filter"
} else if filter.contains('\0') {
"null character not allowed"
} else if filter.len() > 65535 {
"filter too long"
} else {
"invalid wildcard usage"
}
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exact_match() {
assert!(matches("sport/tennis", "sport/tennis"));
assert!(matches("/", "/"));
assert!(matches("sport", "sport"));
assert!(!matches("sport", "sports"));
assert!(!matches("sport/tennis", "sport/tennis/player1"));
}
#[test]
fn test_single_level_wildcard() {
assert!(matches("sport/tennis", "sport/+"));
assert!(matches("sport/", "sport/+"));
assert!(!matches("sport/tennis/player1", "sport/+"));
assert!(matches("sport/tennis/player1", "sport/+/+"));
assert!(matches("sport/tennis/player1", "+/+/+"));
assert!(!matches("sport/tennis", "+/+/+"));
assert!(matches("sport/tennis", "+/tennis"));
assert!(matches("sport/tennis/player1", "sport/tennis/+"));
assert!(matches("/tennis", "+/tennis"));
assert!(matches("sport/", "sport/+"));
}
#[test]
fn test_multi_level_wildcard() {
assert!(matches("sport", "sport/#"));
assert!(matches("sport/", "sport/#"));
assert!(matches("sport/tennis", "sport/#"));
assert!(matches("sport/tennis/player1", "sport/#"));
assert!(matches("sport/tennis/player1/ranking", "sport/#"));
assert!(matches("sport", "#"));
assert!(matches("sport/tennis", "#"));
assert!(!matches("", "#")); assert!(matches("/", "#"));
assert!(!matches("sports", "sport/#"));
assert!(!matches("", "sport/#"));
}
#[test]
fn test_mixed_wildcards() {
assert!(matches("sport/tennis/player1", "sport/+/#"));
assert!(matches("sport/tennis", "sport/+/#"));
assert!(!matches("sport", "sport/+/#"));
assert!(matches("/finance", "+/+/#"));
assert!(matches("/finance/", "+/+/#"));
assert!(matches("/finance/stock", "+/+/#"));
assert!(matches("/", "+/+/#")); }
#[test]
fn test_edge_cases() {
assert!(matches("/", "/"));
assert!(matches("/finance", "/finance"));
assert!(matches("//", "//"));
assert!(matches("/finance", "/+"));
assert!(matches("/", "/+")); assert!(!matches("//", "/+"));
assert!(matches("$SYS/broker/uptime", "$SYS/broker/uptime"));
assert!(matches("$SYS/broker/uptime", "$SYS/+/uptime"));
assert!(matches("$SYS/broker/uptime", "$SYS/#"));
assert!(!matches("$SYS/broker/uptime", "#"));
assert!(!matches("$SYS/broker/uptime", "+/broker/uptime"));
assert!(!matches("$SYS/broker/uptime", "+/#"));
let long_topic = "a/".repeat(100) + "end";
let long_filter = "a/".repeat(100) + "end";
assert!(matches(&long_topic, &long_filter));
assert!(matches(&long_topic, "#"));
let long_sys_topic = "$".to_string() + &"a/".repeat(100) + "end";
assert!(!matches(&long_sys_topic, "#"));
}
#[test]
fn test_dollar_prefix_wildcard_exclusion() {
assert!(!matches("$SYS/broker/uptime", "#"));
assert!(!matches("$data/sensor/temp", "#"));
assert!(!matches("$", "#"));
assert!(!matches("$SYS/broker/uptime", "+/broker/uptime"));
assert!(!matches("$data/sensor/temp", "+/sensor/temp"));
assert!(!matches("$SYS", "+"));
assert!(!matches("$SYS/broker/uptime", "+/#"));
assert!(!matches("$SYS/broker/uptime", "+/+/uptime"));
assert!(matches("$SYS/broker/uptime", "$SYS/broker/uptime"));
assert!(matches("$SYS/broker/uptime", "$SYS/+/uptime"));
assert!(matches("$SYS/broker/uptime", "$SYS/#"));
assert!(matches("$data/sensor/temp", "$data/#"));
assert!(matches("$SYS/broker/uptime", "$SYS/broker/+"));
assert!(matches("SYS/broker/uptime", "#"));
assert!(matches("data/sensor/temp", "+/sensor/temp"));
assert!(matches("normal/topic", "#"));
assert!(matches("prefix/$SYS/data", "#"));
assert!(matches("prefix/$SYS/data", "+/$SYS/data"));
assert!(matches("prefix/$SYS/data", "prefix/#"));
}
#[test]
fn test_invalid_inputs() {
assert!(!matches("sport/tennis+", "sport/tennis+"));
assert!(!matches("sport/tennis#", "sport/tennis#"));
assert!(!matches("", "")); assert!(!matches("sport\0tennis", "sport\0tennis"));
assert!(!matches("sport/tennis", "sport/tennis/#/extra"));
assert!(!matches("sport/tennis", "sport/+tennis"));
assert!(!matches("sport/tennis", "sport/#extra"));
}
#[test]
fn test_validation() {
assert!(is_valid_topic("sport/tennis"));
assert!(is_valid_topic("sport"));
assert!(is_valid_topic("/"));
assert!(is_valid_topic("a"));
assert!(is_valid_topic("")); assert!(!is_valid_topic("sport/+"));
assert!(!is_valid_topic("sport/#"));
assert!(!is_valid_topic("sport\0tennis"));
assert!(!is_valid_topic(&"a".repeat(65536)));
assert!(is_valid_filter("sport/tennis"));
assert!(is_valid_filter("sport/+"));
assert!(is_valid_filter("sport/#"));
assert!(is_valid_filter("+/+/+"));
assert!(is_valid_filter("#"));
assert!(!is_valid_filter(""));
assert!(!is_valid_filter("sport/+tennis"));
assert!(!is_valid_filter("sport/#/extra"));
assert!(!is_valid_filter("sport/tennis#"));
assert!(!is_valid_filter("sport\0tennis"));
assert!(!is_valid_filter(&"a".repeat(65536)));
}
#[test]
fn test_error_messages() {
assert!(validate_topic("").is_ok());
assert_eq!(
validate_topic("sport/+").unwrap_err().to_string(),
"Invalid topic name: Invalid topic: wildcards not allowed in topic names"
);
assert_eq!(
validate_filter("sport/+tennis").unwrap_err().to_string(),
"Invalid topic filter: Invalid filter: invalid wildcard usage"
);
}
}