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