1use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16pub fn is_valid(ke: &str) -> bool {
28 if ke.is_empty() || ke.starts_with('/') || ke.ends_with('/') {
29 return false;
30 }
31 ke.split('/').all(chunk_valid)
32}
33
34pub fn is_canon(ke: &str) -> bool {
45 canonize(ke).as_deref() == Some(ke)
46}
47
48pub fn canonize(ke: &str) -> Option<String> {
70 if !is_valid(ke) {
71 return None;
72 }
73 let canon_chunks: Vec<String> = ke.split('/').map(canon_chunk).collect();
74 let mut out: Vec<String> = Vec::new();
75 let mut i = 0;
76 while i < canon_chunks.len() {
77 if is_wildcard(canon_chunks[i].as_str()) {
78 let mut stars = 0;
79 let mut has_multi = false;
80 while i < canon_chunks.len() && is_wildcard(canon_chunks[i].as_str()) {
81 if canon_chunks[i].as_str() == "*" {
82 stars += 1;
83 } else {
84 has_multi = true;
85 }
86 i += 1;
87 }
88 out.extend((0..stars).map(|_| String::from("*")));
89 if has_multi {
90 out.push(String::from("**"));
91 }
92 } else {
93 out.push(canon_chunks[i].clone());
94 i += 1;
95 }
96 }
97 Some(out.join("/"))
98}
99
100pub fn matches(pattern: &str, key: &str) -> bool {
123 if !is_valid(pattern) || !is_valid(key) || key.contains('*') {
124 return false;
125 }
126 let pattern_chunks: Vec<&str> = pattern.split('/').collect();
127 let key_chunks: Vec<&str> = key.split('/').collect();
128 match_chunks(&pattern_chunks, &key_chunks)
129}
130
131fn is_wildcard(chunk: &str) -> bool {
132 chunk == "*" || chunk == "**"
133}
134
135fn chunk_valid(chunk: &str) -> bool {
136 if chunk.is_empty() {
137 return false;
138 }
139 if is_wildcard(chunk) {
140 return true;
141 }
142 let bytes = chunk.as_bytes();
143 let mut i = 0;
144 while i < bytes.len() {
145 match bytes[i] {
146 b'?' | b'#' | b'/' => return false,
147 b'$' => {
148 if i + 1 >= bytes.len() || bytes[i + 1] != b'*' {
149 return false;
150 }
151 i += 2;
152 }
153 b'*' => return false, _ => i += 1,
155 }
156 }
157 true
158}
159
160fn canon_chunk(chunk: &str) -> String {
161 if is_wildcard(chunk) {
162 return chunk.to_string();
163 }
164 let mut s = chunk.to_string();
165 while s.contains("$*$*") {
166 s = s.replace("$*$*", "$*");
167 }
168 if s == "$*" {
169 return String::from("*");
170 }
171 s
172}
173
174fn match_chunks(pattern: &[&str], key: &[&str]) -> bool {
175 let Some((&head, rest)) = pattern.split_first() else {
176 return key.is_empty();
177 };
178 if head == "**" {
179 (0..=key.len()).any(|skip| match_chunks(rest, &key[skip..]))
181 } else {
182 match key.split_first() {
183 Some((&first_key, rest_key)) => {
184 chunk_matches(head, first_key) && match_chunks(rest, rest_key)
185 }
186 None => false,
187 }
188 }
189}
190
191fn chunk_matches(pattern: &str, literal: &str) -> bool {
192 if pattern == "*" {
193 return true; }
195 glob_match(pattern, literal)
196}
197
198fn glob_match(pattern: &str, s: &str) -> bool {
200 if !pattern.contains("$*") {
201 return pattern == s;
202 }
203 let parts: Vec<&str> = pattern.split("$*").collect();
204 let first = parts[0];
205 if !s.starts_with(first) {
206 return false;
207 }
208 let mut idx = first.len();
209 for part in &parts[1..parts.len() - 1] {
210 if part.is_empty() {
211 continue;
212 }
213 match s[idx..].find(part) {
214 Some(pos) => idx += pos + part.len(),
215 None => return false,
216 }
217 }
218 let last = parts[parts.len() - 1];
219 if last.is_empty() {
220 return true;
221 }
222 s.len() >= idx + last.len() && s[idx..].ends_with(last)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn validity_follows_the_chunk_rules() {
231 assert!(is_valid("a/b/c"));
232 assert!(is_valid("a/*/c"));
233 assert!(is_valid("a/**/c"));
234 assert!(is_valid("thermometer$*/temperature"));
235 assert!(!is_valid("")); assert!(!is_valid("/a")); assert!(!is_valid("a/")); assert!(!is_valid("a//b")); assert!(!is_valid("a/b*")); assert!(!is_valid("a/$x")); assert!(!is_valid("a/**b")); assert!(!is_valid("a/b?")); }
244
245 #[test]
246 fn matching_against_concrete_keys() {
247 assert!(matches(
249 "room275/*/temperature",
250 "room275/device1/temperature"
251 ));
252 assert!(!matches("room275/*/temperature", "room275/temperature"));
253 assert!(!matches("room275/*/temperature", "room275/a/b/temperature"));
254
255 assert!(matches(
257 "organizationA/**/temperature",
258 "organizationA/temperature"
259 ));
260 assert!(matches(
261 "organizationA/**/temperature",
262 "organizationA/b8/r275/temperature"
263 ));
264
265 assert!(matches("**", "anything/at/all"));
267 assert!(matches("demo/**", "demo/a/b/c"));
268 }
269
270 #[test]
271 fn sub_chunk_wildcard_matches_within_a_chunk() {
272 assert!(matches(
273 "thermometer$*/temperature",
274 "thermometer1/temperature"
275 ));
276 assert!(matches(
277 "thermometer$*/temperature",
278 "thermometerA/temperature"
279 ));
280 assert!(matches(
281 "thermometer$*/temperature",
282 "thermometer/temperature"
283 )); assert!(!matches(
285 "thermometer$*/temperature",
286 "xthermometer1/temperature"
287 ));
288 assert!(matches("a$*b$*c", "aXXbYYc"));
289 assert!(!matches("a$*b$*c", "aXXc")); }
291
292 #[test]
293 fn a_pattern_does_not_match_a_key_with_wildcards() {
294 assert!(!matches("a/*", "a/*"));
296 assert!(!matches("a/b", "a/*"));
297 }
298
299 #[test]
300 fn canonical_form_examples() {
301 assert_eq!(
303 canonize("robot/sensor/**/*").as_deref(),
304 Some("robot/sensor/*/**")
305 );
306 assert_eq!(canonize("a/**/**/b").as_deref(), Some("a/**/b"));
308 assert_eq!(canonize("a/x$*$*y/$*").as_deref(), Some("a/x$*y/*"));
310 assert_eq!(canonize("**/*/*").as_deref(), Some("*/*/**"));
312
313 assert!(is_canon("robot/sensor/*/**"));
314 assert!(!is_canon("robot/sensor/**/*"));
315 assert!(!is_canon("a//b")); }
317}