diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 115 | ||||
| -rw-r--r-- | src/mqtt.rs | 114 | 
2 files changed, 116 insertions, 113 deletions
| diff --git a/src/main.rs b/src/main.rs index b071620..c2a3797 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use rumqttc::{Event::Incoming, Packet, Publish, QoS};  use tokio::{io::AsyncWriteExt, process::Command, time::timeout};  mod config; +mod mqtt;  const PROGRAM: &str = "mqttr"; @@ -41,40 +42,6 @@ async fn run(program: &[String], message: &Publish) -> anyhow::Result<ExitStatus      Ok(result)  } -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(), -        }; -    } -} -  #[tokio::main(flavor = "current_thread")]  async fn main() -> anyhow::Result<()> {      let mut conf_path: PathBuf = option_env!("SYSCONFDIR").unwrap_or("/usr/local/etc").into(); @@ -109,7 +76,7 @@ async fn main() -> anyhow::Result<()> {                  debug!("Received message: {p:?}");                  let p = Rc::new(p);                  for (topic, route) in conf.routes.iter() { -                    if !topic_match(topic, &p.topic) { +                    if !mqtt::topic_match(topic, &p.topic) {                          continue;                      }                      debug!("Message {p:?} matched topic {topic}"); @@ -149,81 +116,3 @@ async fn main() -> anyhow::Result<()> {      })      .await  } - -#[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")); -    } -} 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")); +    } +} | 
