rumq_core/mqtt4/
topic.rs

1/// Checks if a topic or topic filter has wildcards
2pub fn has_wildcards(s: &str) -> bool {
3    s.contains("+") || s.contains("#")
4}
5
6/// Checks if a topic is valid
7pub fn valid_topic(topic: &str) -> bool {
8    if topic.contains("+") {
9        return false;
10    }
11
12    if topic.contains("#") {
13        return false;
14    }
15
16    true
17}
18
19/// Checks if the filter is valid
20/// https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106
21pub fn valid_filter(filter: &str) -> bool {
22    if filter.len() == 0 {
23        return false;
24    }
25
26    let hirerarchy = filter.split("/").collect::<Vec<&str>>();
27    if let Some((last, remaining)) = hirerarchy.split_last() {
28        // # is not allowed in filer except as a last entry
29        // invalid: sport/tennis#/player
30        // invalid: sport/tennis/#/ranking
31        for entry in remaining.iter() {
32            if entry.contains("#") {
33                return false;
34            }
35        }
36
37        // only single '#" is allowed in last entry
38        // invalid: sport/tennis#
39        if last.len() != 1 && last.contains("#") {
40            return false;
41        }
42    }
43
44    true
45}
46
47/// Checks if topic matches a filter. topic and filter validation isn't done here.
48///  **note** 'topic' is a misnomer in the arg. This can also be used to match 2 wild subscriptions.
49///  **note** Make sure a topic is validated during a publish and filter is validated during a subscribe
50pub fn matches(topic: &str, filter: &str) -> bool {
51    if topic.len() > 0 && topic[..1].contains("$") {
52        return false;
53    }
54
55    let mut topics = topic.split("/");
56    let mut filters = filter.split("/");
57
58    for f in filters.by_ref() {
59        // "#" being the last element is validated by the broker with 'valid_filter'
60        if f == "#" {
61            return true;
62        }
63
64        // filter still has remaining elements
65        // filter = a/b/c/# should match topci = a/b/c
66        // filter = a/b/c/d should not match topic = a/b/c
67        let top = topics.next();
68        match top {
69            Some(t) if t == "#" => return false,
70            Some(_) if f == "+" => continue,
71            Some(t) if f != t => return false,
72            Some(_) => continue,
73            None => return false,
74        }
75    }
76
77    // topic has remaining elements and filter's last element isn't "#"
78    if topics.next().is_some() {
79        return false;
80    }
81
82    true
83}
84
85#[cfg(test)]
86mod test {
87    #[test]
88    fn wildcards_are_detected_correctly() {
89        assert!(!super::has_wildcards("a/b/c"));
90        assert!(super::has_wildcards("a/+/c"));
91        assert!(super::has_wildcards("a/b/#"));
92    }
93
94    #[test]
95    fn topics_are_validated_correctly() {
96        assert!(!super::valid_topic("+wrong"));
97        assert!(!super::valid_topic("wro#ng"));
98        assert!(!super::valid_topic("w/r/o/n/g+"));
99        assert!(!super::valid_topic("wrong/#/path"));
100    }
101
102    #[test]
103    fn filters_are_validated_correctly() {
104        assert!(!super::valid_filter("wrong/#/filter"));
105        assert!(!super::valid_filter("wrong/wr#ng/filter"));
106        assert!(!super::valid_filter("wrong/filter#"));
107        assert!(super::valid_filter("correct/filter/#"));
108    }
109
110    #[test]
111    fn zero_len_subscriptions_are_not_allowed() {
112        assert!(!super::valid_filter(""));
113    }
114
115    #[test]
116    fn dollar_subscriptions_doesnt_match_dollar_topic() {
117        assert!(super::matches("sy$tem/metrics", "sy$tem/+"));
118        assert!(!super::matches("$system/metrics", "$system/+"));
119        assert!(!super::matches("$system/metrics", "+/+"));
120    }
121
122    #[test]
123    fn topics_match_with_filters_as_expected() {
124        let topic = "a/b/c";
125        let filter = "a/b/c";
126        assert!(super::matches(topic, filter));
127
128        let topic = "a/b/c";
129        let filter = "d/b/c";
130        assert!(!super::matches(topic, filter));
131
132        let topic = "a/b/c";
133        let filter = "a/b/e";
134        assert!(!super::matches(topic, filter));
135
136        let topic = "a/b/c";
137        let filter = "a/b/c/d";
138        assert!(!super::matches(topic, filter));
139
140        let topic = "a/b/c";
141        let filter = "#";
142        assert!(super::matches(topic, filter));
143
144        let topic = "a/b/c";
145        let filter = "a/b/c/#";
146        assert!(super::matches(topic, filter));
147
148        let topic = "a/b/c/d";
149        let filter = "a/b/c";
150        assert!(!super::matches(topic, filter));
151
152        let topic = "a/b/c/d";
153        let filter = "a/b/c/#";
154        assert!(super::matches(topic, filter));
155
156        let topic = "a/b/c/d/e/f";
157        let filter = "a/b/c/#";
158        assert!(super::matches(topic, filter));
159
160        let topic = "a/b/c";
161        let filter = "a/+/c";
162        assert!(super::matches(topic, filter));
163        let topic = "a/b/c/d/e";
164        let filter = "a/+/c/+/e";
165        assert!(super::matches(topic, filter));
166
167        let topic = "a/b";
168        let filter = "a/b/+";
169        assert!(!super::matches(topic, filter));
170
171        let filter1 = "a/b/+";
172        let filter2 = "a/b/#";
173        assert!(super::matches(filter1, filter2));
174        assert!(!super::matches(filter2, filter1));
175
176        let filter1 = "a/b/+";
177        let filter2 = "#";
178        assert!(super::matches(filter1, filter2));
179
180        let filter1 = "a/+/c/d";
181        let filter2 = "a/+/+/d";
182        assert!(super::matches(filter1, filter2));
183        assert!(!super::matches(filter2, filter1));
184    }
185}