aboutsummaryrefslogtreecommitdiffstats
path: root/src/mqtt.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/mqtt.rs')
-rw-r--r--src/mqtt.rs114
1 files changed, 114 insertions, 0 deletions
diff --git a/src/mqtt.rs b/src/mqtt.rs
new file mode 100644
index 0000000..f12d51c
--- /dev/null
+++ b/src/mqtt.rs
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2025 Tomasz Kramkowski <tomasz@kramkow.ski>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+pub fn topic_match(filter: &str, topic: &str) -> bool {
+ // TODO: Should probably just be a panic or prevented using types
+ if filter.is_empty() || topic.is_empty() {
+ return false;
+ }
+ if topic.starts_with('$') && (filter.starts_with('+') || filter.starts_with('#')) {
+ return false;
+ }
+
+ // zip_longest would be nice
+ let mut topic = topic.split('/');
+ let mut filter = filter.split('/');
+ loop {
+ let topic_level = topic.next();
+ return match filter.next() {
+ Some("#") => filter.next().is_none(),
+ Some("+") => {
+ if topic_level.is_none() {
+ false
+ } else {
+ continue;
+ }
+ }
+ Some(filter_level) => match topic_level {
+ Some(topic_level) if topic_level == filter_level => {
+ continue;
+ }
+ _ => false,
+ },
+ None => topic_level.is_none(),
+ };
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::topic_match;
+
+ #[test]
+ fn topic_match_basic() {
+ assert!(topic_match("foo/bar/baz", "foo/bar/baz"));
+ assert!(!topic_match("foo/bar/baz", "foo/bar/qux"));
+ assert!(!topic_match("foo/bar", "foo/bar/baz"));
+ assert!(!topic_match("foo/bar/baz", "foo/bar"));
+ }
+
+ #[test]
+ fn topic_match_wildcard_hash() {
+ assert!(topic_match("foo/bar/baz/#", "foo/bar/baz"));
+ assert!(topic_match("foo/bar/baz/#", "foo/bar/baz/qux"));
+ assert!(topic_match("foo/bar/baz/#", "foo/bar/baz/qux/quux"));
+ assert!(topic_match("#", "foo/bar/baz"));
+ assert!(topic_match("#", "foo"));
+ assert!(topic_match("#", "/"));
+ assert!(topic_match("#", "/foo"));
+ assert!(!topic_match("foo/bar/#", "foo/baz/bar"));
+ assert!(!topic_match("foo/bar/#", "foo"));
+ }
+
+ #[test]
+ fn topic_match_wildcard_plus() {
+ assert!(topic_match("foo/bar/+", "foo/bar/baz"));
+ assert!(topic_match("foo/bar/+", "foo/bar/qux"));
+ assert!(!topic_match("foo/bar/+", "foo/bar/baz/qux"));
+ assert!(topic_match("foo/+", "foo/"));
+ assert!(!topic_match("foo/+", "foo"));
+ assert!(topic_match("+", "foo"));
+ assert!(topic_match("+/bar/#", "foo/bar/baz/qux"));
+ assert!(topic_match("+/bar/#", "qux/bar"));
+ assert!(topic_match("foo/+/baz", "foo/bar/baz"));
+ assert!(topic_match("foo/+/baz", "foo/qux/baz"));
+ assert!(!topic_match("foo/+/baz", "foo/bar/qux"));
+ assert!(topic_match("+/+", "/foo"));
+ assert!(topic_match("/+", "/foo"));
+ assert!(!topic_match("+", "/foo"));
+ }
+
+ #[test]
+ fn topic_match_dollar() {
+ assert!(!topic_match("#", "$foo/bar"));
+ assert!(!topic_match("+/bar/baz", "$foo/bar/baz"));
+ assert!(topic_match("$foo/#", "$foo/bar"));
+ assert!(topic_match("$foo/#", "$foo/bar/baz"));
+ assert!(topic_match("$foo/#", "$foo"));
+ assert!(topic_match("$foo/bar/+", "$foo/bar/baz"));
+ assert!(!topic_match("$foo/#", "foo/bar"));
+ }
+
+ #[test]
+ fn topic_match_edge_cases() {
+ assert!(!topic_match("foo", "FOO"));
+ assert!(topic_match("foo bar", "foo bar"));
+ assert!(!topic_match("foo bar", "foo bar"));
+ assert!(!topic_match("foo bar", "foo bar"));
+ assert!(!topic_match("/foo", "foo"));
+ assert!(!topic_match("foo", "/foo"));
+ assert!(topic_match("foo//bar", "foo//bar"));
+ assert!(!topic_match("foo/bar", "foo//bar"));
+ assert!(!topic_match("foo//bar", "foo/bar"));
+ assert!(!topic_match("foo//baz", "foo/bar/baz"));
+ assert!(!topic_match("foo/bar/baz", "foo//baz"));
+ assert!(topic_match("foo/+/baz", "foo//baz"));
+ assert!(topic_match("/", "/"));
+ assert!(!topic_match("/", "foo"));
+ assert!(!topic_match("foo", "/"));
+ assert!(!topic_match("", ""));
+ assert!(!topic_match("+", ""));
+ assert!(!topic_match("#", ""));
+ assert!(!topic_match("foo/#/baz", "foo/bar/baz"));
+ }
+}