1pub fn match_pattern(value: &str, pattern: &str) -> bool {
28 match_pattern_recursive(value.as_bytes(), pattern.as_bytes())
29}
30
31fn match_pattern_recursive(value: &[u8], pattern: &[u8]) -> bool {
35 let mut v_idx = 0;
37 let mut p_idx = 0;
38 let mut star_idx: Option<usize> = None;
39 let mut match_idx = 0;
40
41 while v_idx < value.len() {
42 if p_idx < pattern.len() && (pattern[p_idx] == b'?' || pattern[p_idx] == value[v_idx]) {
43 v_idx += 1;
45 p_idx += 1;
46 } else if p_idx < pattern.len() && pattern[p_idx] == b'*' {
47 star_idx = Some(p_idx);
49 match_idx = v_idx;
50 p_idx += 1;
51 } else if let Some(star) = star_idx {
52 p_idx = star + 1;
54 match_idx += 1;
55 v_idx = match_idx;
56 } else {
57 return false;
59 }
60 }
61
62 while p_idx < pattern.len() && pattern[p_idx] == b'*' {
64 p_idx += 1;
65 }
66
67 p_idx == pattern.len()
68}
69
70pub fn has_wildcards(pattern: &str) -> bool {
78 pattern.contains('*') || pattern.contains('?')
79}
80
81pub fn pattern_to_regex(pattern: &str) -> String {
89 let mut regex = String::with_capacity(pattern.len() * 2);
90 regex.push('^');
91
92 for ch in pattern.chars() {
93 match ch {
94 '*' => regex.push_str(".*"),
95 '?' => regex.push('.'),
96 '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
98 regex.push('\\');
99 regex.push(ch);
100 }
101 _ => regex.push(ch),
102 }
103 }
104
105 regex.push('$');
106 regex
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
115 fn test_exact_match() {
116 assert!(match_pattern("file_read", "file_read"));
117 assert!(match_pattern("bash_exec", "bash_exec"));
118 assert!(match_pattern("", ""));
119 }
120
121 #[test]
122 fn test_exact_no_match() {
123 assert!(!match_pattern("file_read", "file_write"));
124 assert!(!match_pattern("bash", "bash_exec"));
125 }
126
127 #[test]
129 fn test_star_at_end() {
130 assert!(match_pattern("file_read", "file_*"));
131 assert!(match_pattern("file_write", "file_*"));
132 assert!(match_pattern("file_", "file_*"));
133 assert!(match_pattern("file_read_all", "file_*"));
134 }
135
136 #[test]
137 fn test_star_at_start() {
138 assert!(match_pattern("read_file", "*_file"));
139 assert!(match_pattern("write_file", "*_file"));
140 assert!(match_pattern("_file", "*_file"));
141 }
142
143 #[test]
144 fn test_star_in_middle() {
145 assert!(match_pattern("file_read_all", "file_*_all"));
146 assert!(match_pattern("file__all", "file_*_all"));
147 assert!(match_pattern("file_xyz_all", "file_*_all"));
148 }
149
150 #[test]
151 fn test_multiple_stars() {
152 assert!(match_pattern("file_read_write", "*_*_*"));
153 assert!(match_pattern("a_b_c", "*_*_*"));
154 assert!(match_pattern("__", "*_*_*"));
155 }
156
157 #[test]
158 fn test_star_matches_empty() {
159 assert!(match_pattern("file", "file*"));
160 assert!(match_pattern("file", "*file"));
161 assert!(match_pattern("file", "*file*"));
162 }
163
164 #[test]
165 fn test_only_star() {
166 assert!(match_pattern("anything", "*"));
167 assert!(match_pattern("", "*"));
168 assert!(match_pattern("file_read_write_delete", "*"));
169 }
170
171 #[test]
173 fn test_question_mark() {
174 assert!(match_pattern("file_read", "file_rea?"));
175 assert!(match_pattern("file_reax", "file_rea?"));
176 assert!(!match_pattern("file_re", "file_rea?"));
177 assert!(!match_pattern("file_read_", "file_rea?"));
178 }
179
180 #[test]
181 fn test_multiple_question_marks() {
182 assert!(match_pattern("abc", "???"));
183 assert!(!match_pattern("ab", "???"));
184 assert!(!match_pattern("abcd", "???"));
185 }
186
187 #[test]
188 fn test_question_mark_in_middle() {
189 assert!(match_pattern("file_read", "file_?ead"));
190 assert!(match_pattern("file_xead", "file_?ead"));
191 }
192
193 #[test]
195 fn test_mixed_wildcards() {
196 assert!(match_pattern("file_read", "f*_?ead"));
197 assert!(match_pattern("file_xead", "f*_?ead"));
198 assert!(match_pattern("f_read", "f*_?ead"));
199 }
200
201 #[test]
202 fn test_star_and_question() {
203 assert!(match_pattern("bash_exec", "bash_*?"));
204 assert!(match_pattern("bash_e", "bash_*?"));
205 assert!(!match_pattern("bash_", "bash_*?"));
206 }
207
208 #[test]
210 fn test_empty_pattern() {
211 assert!(match_pattern("", ""));
212 assert!(!match_pattern("a", ""));
213 }
214
215 #[test]
216 fn test_empty_value() {
217 assert!(match_pattern("", "*"));
218 assert!(!match_pattern("", "?"));
219 assert!(!match_pattern("", "a"));
220 }
221
222 #[test]
223 fn test_special_characters() {
224 assert!(match_pattern("file.txt", "file.txt"));
225 assert!(match_pattern("file.txt", "file.*"));
226 assert!(match_pattern("file.txt", "*.txt"));
227 }
228
229 #[test]
231 fn test_has_wildcards() {
232 assert!(has_wildcards("file_*"));
233 assert!(has_wildcards("file_?"));
234 assert!(has_wildcards("*"));
235 assert!(has_wildcards("?"));
236 assert!(has_wildcards("file_*_?"));
237 assert!(!has_wildcards("file_read"));
238 assert!(!has_wildcards(""));
239 }
240
241 #[test]
243 fn test_pattern_to_regex() {
244 assert_eq!(pattern_to_regex("file_*"), "^file_.*$");
245 assert_eq!(pattern_to_regex("file_?"), "^file_.$");
246 assert_eq!(pattern_to_regex("file.txt"), "^file\\.txt$");
247 assert_eq!(pattern_to_regex("*"), "^.*$");
248 assert_eq!(pattern_to_regex("?"), "^.$");
249 }
250
251 #[test]
253 fn test_tool_name_patterns() {
254 assert!(match_pattern("file_read", "file_*"));
256 assert!(match_pattern("file_write", "file_*"));
257 assert!(match_pattern("file_delete", "file_*"));
258 assert!(match_pattern("file_list", "file_*"));
259
260 assert!(match_pattern("bash_exec", "bash_*"));
262 assert!(match_pattern("bash_run", "bash_*"));
263
264 assert!(!match_pattern("http_get", "file_*"));
266 assert!(!match_pattern("database_query", "bash_*"));
267 }
268
269 #[test]
270 fn test_complex_patterns() {
271 assert!(match_pattern("file_read", "*_read"));
273 assert!(match_pattern("database_read", "*_read"));
274 assert!(!match_pattern("file_write", "*_read"));
275
276 assert!(match_pattern("file_read_async", "file_*_async"));
278 assert!(match_pattern("file_write_async", "file_*_async"));
279 assert!(!match_pattern("file_read_sync", "file_*_async"));
280 }
281}
282
283#[cfg(test)]
288mod property_tests {
289 use super::*;
290 use proptest::prelude::*;
291
292 fn tool_name_strategy() -> impl Strategy<Value = String> {
294 "[a-z][a-z0-9_]{0,20}".prop_map(|s| s)
295 }
296
297 fn simple_pattern_strategy() -> impl Strategy<Value = String> {
299 prop_oneof![
300 tool_name_strategy(),
302 tool_name_strategy().prop_map(|s| format!("{}*", s)),
304 tool_name_strategy().prop_map(|s| format!("*{}", s)),
306 Just("*".to_string()),
308 tool_name_strategy().prop_map(|s| {
310 if s.len() > 1 {
311 let prefix: String = s.chars().take(s.len() - 1).collect();
312 format!("{}?", prefix)
313 } else {
314 format!("{}?", s)
315 }
316 }),
317 ]
318 }
319
320 proptest! {
321 #![proptest_config(ProptestConfig::with_cases(100))]
322
323 #[test]
328 fn prop_exact_match_self(value in tool_name_strategy()) {
329 prop_assert!(
330 match_pattern(&value, &value),
331 "Value '{}' should match itself as pattern",
332 value
333 );
334 }
335
336 #[test]
341 fn prop_star_matches_all(value in tool_name_strategy()) {
342 prop_assert!(
343 match_pattern(&value, "*"),
344 "Pattern '*' should match any value, but failed for '{}'",
345 value
346 );
347 }
348
349 #[test]
354 fn prop_prefix_match(
355 prefix in "[a-z]{1,5}",
356 suffix in "[a-z0-9_]{0,10}"
357 ) {
358 let value = format!("{}{}", prefix, suffix);
359 let pattern = format!("{}*", prefix);
360 prop_assert!(
361 match_pattern(&value, &pattern),
362 "Value '{}' should match pattern '{}'",
363 value, pattern
364 );
365 }
366
367 #[test]
372 fn prop_suffix_match(
373 prefix in "[a-z0-9_]{0,10}",
374 suffix in "[a-z]{1,5}"
375 ) {
376 let value = format!("{}{}", prefix, suffix);
377 let pattern = format!("*{}", suffix);
378 prop_assert!(
379 match_pattern(&value, &pattern),
380 "Value '{}' should match pattern '{}'",
381 value, pattern
382 );
383 }
384
385 #[test]
390 fn prop_question_mark_single_char(ch in "[a-z]") {
391 prop_assert!(
392 match_pattern(&ch, "?"),
393 "Pattern '?' should match single char '{}'",
394 ch
395 );
396 }
397
398 #[test]
403 fn prop_question_mark_not_empty(_dummy in Just(())) {
404 prop_assert!(
405 !match_pattern("", "?"),
406 "Pattern '?' should not match empty string"
407 );
408 }
409
410 #[test]
415 fn prop_question_mark_not_multiple(value in "[a-z]{2,5}") {
416 prop_assert!(
417 !match_pattern(&value, "?"),
418 "Pattern '?' should not match multi-char string '{}'",
419 value
420 );
421 }
422
423 #[test]
428 fn prop_prefix_no_match(
429 prefix in "[a-m]{2,4}",
430 other_prefix in "[n-z]{2,4}",
431 suffix in "[a-z0-9_]{0,5}"
432 ) {
433 let value = format!("{}{}", other_prefix, suffix);
434 let pattern = format!("{}*", prefix);
435 if !value.starts_with(&prefix) {
437 prop_assert!(
438 !match_pattern(&value, &pattern),
439 "Value '{}' should not match pattern '{}'",
440 value, pattern
441 );
442 }
443 }
444
445 #[test]
450 fn prop_empty_pattern_only_empty(value in "[a-z]{1,10}") {
451 prop_assert!(
452 !match_pattern(&value, ""),
453 "Empty pattern should not match non-empty value '{}'",
454 value
455 );
456 }
457
458 #[test]
463 fn prop_empty_matches_empty(_dummy in Just(())) {
464 prop_assert!(
465 match_pattern("", ""),
466 "Empty string should match empty pattern"
467 );
468 }
469
470 #[test]
475 fn prop_middle_star_match(
476 prefix in "[a-z]{1,3}",
477 middle in "[a-z0-9_]{0,5}",
478 suffix in "[a-z]{1,3}"
479 ) {
480 let value = format!("{}{}{}", prefix, middle, suffix);
481 let pattern = format!("{}*{}", prefix, suffix);
482 prop_assert!(
483 match_pattern(&value, &pattern),
484 "Value '{}' should match pattern '{}'",
485 value, pattern
486 );
487 }
488
489 #[test]
494 fn prop_has_wildcards_detection(pattern in simple_pattern_strategy()) {
495 let expected = pattern.contains('*') || pattern.contains('?');
496 prop_assert_eq!(
497 has_wildcards(&pattern),
498 expected,
499 "has_wildcards('{}') should be {}",
500 pattern, expected
501 );
502 }
503
504 #[test]
509 fn prop_no_wildcard_exact_match(
510 value in tool_name_strategy(),
511 pattern in tool_name_strategy()
512 ) {
513 if !has_wildcards(&pattern) {
515 prop_assert_eq!(
516 match_pattern(&value, &pattern),
517 value == pattern,
518 "Without wildcards, match_pattern('{}', '{}') should equal string equality",
519 value, pattern
520 );
521 }
522 }
523 }
524}