1use crate::error::{MqttError, Result};
5
6#[must_use]
25pub fn matches(topic: &str, filter: &str) -> bool {
26 if topic.is_empty() {
28 return false;
29 }
30
31 if !is_valid_topic(topic) || !is_valid_filter(filter) {
33 return false;
34 }
35
36 if topic == filter {
38 return true;
39 }
40
41 if topic.starts_with('$') && (filter.starts_with('#') || filter.starts_with('+')) {
43 return false;
44 }
45
46 if filter == "#" {
48 return true;
49 }
50
51 let topic_parts: Vec<&str> = topic.split('/').collect();
52 let filter_parts: Vec<&str> = filter.split('/').collect();
53
54 match_parts(&topic_parts, &filter_parts)
55}
56
57fn match_parts(topic_parts: &[&str], filter_parts: &[&str]) -> bool {
59 match (topic_parts.first(), filter_parts.first()) {
60 (None, None) => true,
62
63 (_, Some(&"#")) => filter_parts.len() == 1, (None, Some(_)) | (Some(_), None) => false,
68
69 (Some(&topic_part), Some(&filter_part)) => {
71 let level_match = filter_part == "+" || filter_part == topic_part;
73
74 level_match && match_parts(&topic_parts[1..], &filter_parts[1..])
76 }
77 }
78}
79
80#[must_use]
82pub fn is_valid_topic(topic: &str) -> bool {
83 !topic.contains('\0') && !topic.contains('+') && !topic.contains('#') && topic.len() <= 65535
85}
86
87#[must_use]
89pub fn is_valid_filter(filter: &str) -> bool {
90 if filter.is_empty() || filter.contains('\0') || filter.len() > 65535 {
91 return false;
92 }
93
94 let parts: Vec<&str> = filter.split('/').collect();
95
96 for (i, part) in parts.iter().enumerate() {
97 if part.contains('#') {
99 return *part == "#" && i == parts.len() - 1;
100 }
101
102 if part.contains('+') && *part != "+" {
104 return false;
105 }
106 }
107
108 true
109}
110
111pub fn validate_topic(topic: &str) -> Result<()> {
116 if !is_valid_topic(topic) {
117 return Err(MqttError::InvalidTopicName(format!(
118 "Invalid topic: {}",
119 if topic.is_empty() {
120 "empty topic"
121 } else if topic.contains('+') || topic.contains('#') {
122 "wildcards not allowed in topic names"
123 } else if topic.contains('\0') {
124 "null character not allowed"
125 } else if topic.len() > 65535 {
126 "topic too long"
127 } else {
128 "unknown error"
129 }
130 )));
131 }
132 Ok(())
133}
134
135pub fn validate_filter(filter: &str) -> Result<()> {
140 if !is_valid_filter(filter) {
141 return Err(MqttError::InvalidTopicFilter(format!(
142 "Invalid filter: {}",
143 if filter.is_empty() {
144 "empty filter"
145 } else if filter.contains('\0') {
146 "null character not allowed"
147 } else if filter.len() > 65535 {
148 "filter too long"
149 } else {
150 "invalid wildcard usage"
151 }
152 )));
153 }
154 Ok(())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_exact_match() {
163 assert!(matches("sport/tennis", "sport/tennis"));
164 assert!(matches("/", "/"));
165 assert!(matches("sport", "sport"));
166 assert!(!matches("sport", "sports"));
167 assert!(!matches("sport/tennis", "sport/tennis/player1"));
168 }
169
170 #[test]
171 fn test_single_level_wildcard() {
172 assert!(matches("sport/tennis", "sport/+"));
174 assert!(matches("sport/", "sport/+"));
175 assert!(!matches("sport/tennis/player1", "sport/+"));
176
177 assert!(matches("sport/tennis/player1", "sport/+/+"));
179 assert!(matches("sport/tennis/player1", "+/+/+"));
180 assert!(!matches("sport/tennis", "+/+/+"));
181
182 assert!(matches("sport/tennis", "+/tennis"));
184 assert!(matches("sport/tennis/player1", "sport/tennis/+"));
185 assert!(matches("/tennis", "+/tennis"));
186 assert!(matches("sport/", "sport/+"));
187 }
188
189 #[test]
190 fn test_multi_level_wildcard() {
191 assert!(matches("sport", "sport/#"));
193 assert!(matches("sport/", "sport/#"));
194 assert!(matches("sport/tennis", "sport/#"));
195 assert!(matches("sport/tennis/player1", "sport/#"));
196 assert!(matches("sport/tennis/player1/ranking", "sport/#"));
197
198 assert!(matches("sport", "#"));
200 assert!(matches("sport/tennis", "#"));
201 assert!(!matches("", "#")); assert!(matches("/", "#"));
203
204 assert!(!matches("sports", "sport/#"));
206 assert!(!matches("", "sport/#"));
207 }
208
209 #[test]
210 fn test_mixed_wildcards() {
211 assert!(matches("sport/tennis/player1", "sport/+/#"));
212 assert!(matches("sport/tennis", "sport/+/#"));
213 assert!(!matches("sport", "sport/+/#"));
214
215 assert!(matches("/finance", "+/+/#"));
216 assert!(matches("/finance/", "+/+/#"));
217 assert!(matches("/finance/stock", "+/+/#"));
218 assert!(matches("/", "+/+/#")); }
220
221 #[test]
222 fn test_edge_cases() {
223 assert!(matches("/", "/"));
225 assert!(matches("/finance", "/finance"));
226 assert!(matches("//", "//"));
227 assert!(matches("/finance", "/+"));
228 assert!(matches("/", "/+")); assert!(!matches("//", "/+")); assert!(matches("$SYS/broker/uptime", "$SYS/broker/uptime"));
233 assert!(matches("$SYS/broker/uptime", "$SYS/+/uptime"));
234 assert!(matches("$SYS/broker/uptime", "$SYS/#"));
235 assert!(!matches("$SYS/broker/uptime", "#"));
236 assert!(!matches("$SYS/broker/uptime", "+/broker/uptime"));
237 assert!(!matches("$SYS/broker/uptime", "+/#"));
238
239 let long_topic = "a/".repeat(100) + "end";
241 let long_filter = "a/".repeat(100) + "end";
242 assert!(matches(&long_topic, &long_filter));
243 assert!(matches(&long_topic, "#"));
244
245 let long_sys_topic = "$".to_string() + &"a/".repeat(100) + "end";
247 assert!(!matches(&long_sys_topic, "#"));
248 }
249
250 #[test]
251 fn test_dollar_prefix_wildcard_exclusion() {
252 assert!(!matches("$SYS/broker/uptime", "#"));
257 assert!(!matches("$data/sensor/temp", "#"));
258 assert!(!matches("$", "#"));
259
260 assert!(!matches("$SYS/broker/uptime", "+/broker/uptime"));
262 assert!(!matches("$data/sensor/temp", "+/sensor/temp"));
263 assert!(!matches("$SYS", "+"));
264
265 assert!(!matches("$SYS/broker/uptime", "+/#"));
267 assert!(!matches("$SYS/broker/uptime", "+/+/uptime"));
268
269 assert!(matches("$SYS/broker/uptime", "$SYS/broker/uptime"));
271 assert!(matches("$SYS/broker/uptime", "$SYS/+/uptime"));
272 assert!(matches("$SYS/broker/uptime", "$SYS/#"));
273 assert!(matches("$data/sensor/temp", "$data/#"));
274 assert!(matches("$SYS/broker/uptime", "$SYS/broker/+"));
275
276 assert!(matches("SYS/broker/uptime", "#"));
278 assert!(matches("data/sensor/temp", "+/sensor/temp"));
279 assert!(matches("normal/topic", "#"));
280
281 assert!(matches("prefix/$SYS/data", "#"));
283 assert!(matches("prefix/$SYS/data", "+/$SYS/data"));
284 assert!(matches("prefix/$SYS/data", "prefix/#"));
285 }
286
287 #[test]
288 fn test_invalid_inputs() {
289 assert!(!matches("sport/tennis+", "sport/tennis+"));
291 assert!(!matches("sport/tennis#", "sport/tennis#"));
292 assert!(!matches("", "")); assert!(!matches("sport\0tennis", "sport\0tennis"));
294
295 assert!(!matches("sport/tennis", "sport/tennis/#/extra"));
297 assert!(!matches("sport/tennis", "sport/+tennis"));
298 assert!(!matches("sport/tennis", "sport/#extra"));
299 }
300
301 #[test]
302 fn test_validation() {
303 assert!(is_valid_topic("sport/tennis"));
305 assert!(is_valid_topic("sport"));
306 assert!(is_valid_topic("/"));
307 assert!(is_valid_topic("a"));
308
309 assert!(is_valid_topic("")); assert!(!is_valid_topic("sport/+"));
312 assert!(!is_valid_topic("sport/#"));
313 assert!(!is_valid_topic("sport\0tennis"));
314 assert!(!is_valid_topic(&"a".repeat(65536)));
315
316 assert!(is_valid_filter("sport/tennis"));
318 assert!(is_valid_filter("sport/+"));
319 assert!(is_valid_filter("sport/#"));
320 assert!(is_valid_filter("+/+/+"));
321 assert!(is_valid_filter("#"));
322
323 assert!(!is_valid_filter(""));
325 assert!(!is_valid_filter("sport/+tennis"));
326 assert!(!is_valid_filter("sport/#/extra"));
327 assert!(!is_valid_filter("sport/tennis#"));
328 assert!(!is_valid_filter("sport\0tennis"));
329 assert!(!is_valid_filter(&"a".repeat(65536)));
330 }
331
332 #[test]
333 fn test_error_messages() {
334 assert!(validate_topic("").is_ok()); assert_eq!(
338 validate_topic("sport/+").unwrap_err().to_string(),
339 "Invalid topic name: Invalid topic: wildcards not allowed in topic names"
340 );
341
342 assert_eq!(
343 validate_filter("sport/+tennis").unwrap_err().to_string(),
344 "Invalid topic filter: Invalid filter: invalid wildcard usage"
345 );
346 }
347}