Skip to main content

mqttbytes_core/
topic.rs

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