Skip to main content

aster/permission/
pattern.rs

1//! 工具名模式匹配模块
2//!
3//! 本模块实现了工具名的通配符模式匹配功能,支持:
4//! - `*` 通配符:匹配任意数量的任意字符(包括零个)
5//! - `?` 通配符:匹配单个任意字符
6//!
7//! Requirements: 2.1
8
9/// 检查值是否匹配给定的模式
10///
11/// # Arguments
12/// * `value` - 要检查的字符串值
13/// * `pattern` - 包含通配符的模式字符串
14///
15/// # Returns
16/// 如果值匹配模式则返回 `true`,否则返回 `false`
17///
18/// # Examples
19/// ```
20/// use aster::permission::pattern::match_pattern;
21///
22/// assert!(match_pattern("file_read", "file_*"));
23/// assert!(match_pattern("file_write", "file_*"));
24/// assert!(match_pattern("bash_exec", "bash_?xec"));
25/// assert!(!match_pattern("other_tool", "file_*"));
26/// ```
27pub fn match_pattern(value: &str, pattern: &str) -> bool {
28    match_pattern_recursive(value.as_bytes(), pattern.as_bytes())
29}
30
31/// 递归实现模式匹配
32///
33/// 使用动态规划思想的递归实现,处理 `*` 和 `?` 通配符
34fn match_pattern_recursive(value: &[u8], pattern: &[u8]) -> bool {
35    // 使用迭代方式避免栈溢出
36    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            // 当前字符匹配或模式是 '?'
44            v_idx += 1;
45            p_idx += 1;
46        } else if p_idx < pattern.len() && pattern[p_idx] == b'*' {
47            // 遇到 '*',记录位置
48            star_idx = Some(p_idx);
49            match_idx = v_idx;
50            p_idx += 1;
51        } else if let Some(star) = star_idx {
52            // 回溯到上一个 '*' 的位置
53            p_idx = star + 1;
54            match_idx += 1;
55            v_idx = match_idx;
56        } else {
57            // 不匹配且没有 '*' 可以回溯
58            return false;
59        }
60    }
61
62    // 检查剩余的模式字符是否都是 '*'
63    while p_idx < pattern.len() && pattern[p_idx] == b'*' {
64        p_idx += 1;
65    }
66
67    p_idx == pattern.len()
68}
69
70/// 检查模式是否包含通配符
71///
72/// # Arguments
73/// * `pattern` - 要检查的模式字符串
74///
75/// # Returns
76/// 如果模式包含 `*` 或 `?` 通配符则返回 `true`
77pub fn has_wildcards(pattern: &str) -> bool {
78    pattern.contains('*') || pattern.contains('?')
79}
80
81/// 将模式转换为正则表达式字符串
82///
83/// # Arguments
84/// * `pattern` - 通配符模式
85///
86/// # Returns
87/// 等效的正则表达式字符串
88pub 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            // 转义正则表达式特殊字符
97            '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
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    // 基本匹配测试
114    #[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    // 星号通配符测试
128    #[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    // 问号通配符测试
172    #[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    // 混合通配符测试
194    #[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    // 边界情况测试
209    #[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    // has_wildcards 测试
230    #[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    // pattern_to_regex 测试
242    #[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    // 实际工具名匹配场景测试
252    #[test]
253    fn test_tool_name_patterns() {
254        // 文件操作工具
255        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        // Bash 工具
261        assert!(match_pattern("bash_exec", "bash_*"));
262        assert!(match_pattern("bash_run", "bash_*"));
263
264        // 不匹配的情况
265        assert!(!match_pattern("http_get", "file_*"));
266        assert!(!match_pattern("database_query", "bash_*"));
267    }
268
269    #[test]
270    fn test_complex_patterns() {
271        // 匹配所有以 _read 结尾的工具
272        assert!(match_pattern("file_read", "*_read"));
273        assert!(match_pattern("database_read", "*_read"));
274        assert!(!match_pattern("file_write", "*_read"));
275
276        // 匹配特定前缀和后缀
277        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/// Property-based tests for tool name pattern matching
284///
285/// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
286/// **Validates: Requirements 2.1**
287#[cfg(test)]
288mod property_tests {
289    use super::*;
290    use proptest::prelude::*;
291
292    /// 生成有效的工具名(字母数字和下划线)
293    fn tool_name_strategy() -> impl Strategy<Value = String> {
294        "[a-z][a-z0-9_]{0,20}".prop_map(|s| s)
295    }
296
297    /// 生成简单的模式(带有可选的通配符)
298    fn simple_pattern_strategy() -> impl Strategy<Value = String> {
299        prop_oneof![
300            // 精确匹配模式
301            tool_name_strategy(),
302            // 以 * 结尾的模式
303            tool_name_strategy().prop_map(|s| format!("{}*", s)),
304            // 以 * 开头的模式
305            tool_name_strategy().prop_map(|s| format!("*{}", s)),
306            // 只有 *
307            Just("*".to_string()),
308            // 带 ? 的模式
309            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        /// Property: 精确匹配 - 任何字符串都应该匹配自身
324        ///
325        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
326        /// **Validates: Requirements 2.1**
327        #[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        /// Property: 星号通配符匹配所有 - "*" 模式应该匹配任何字符串
337        ///
338        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
339        /// **Validates: Requirements 2.1**
340        #[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        /// Property: 前缀匹配 - "prefix*" 应该匹配所有以 prefix 开头的字符串
350        ///
351        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
352        /// **Validates: Requirements 2.1**
353        #[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        /// Property: 后缀匹配 - "*suffix" 应该匹配所有以 suffix 结尾的字符串
368        ///
369        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
370        /// **Validates: Requirements 2.1**
371        #[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        /// Property: 问号匹配单个字符 - "?" 应该只匹配单个字符
386        ///
387        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
388        /// **Validates: Requirements 2.1**
389        #[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        /// Property: 问号不匹配空字符串
399        ///
400        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
401        /// **Validates: Requirements 2.1**
402        #[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        /// Property: 问号不匹配多个字符
411        ///
412        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
413        /// **Validates: Requirements 2.1**
414        #[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        /// Property: 前缀不匹配 - 不以 prefix 开头的字符串不应匹配 "prefix*"
424        ///
425        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
426        /// **Validates: Requirements 2.1**
427        #[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            // 只有当 other_prefix 确实不以 prefix 开头时才测试
436            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        /// Property: 空模式只匹配空字符串
446        ///
447        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
448        /// **Validates: Requirements 2.1**
449        #[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        /// Property: 空字符串匹配空模式
459        ///
460        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
461        /// **Validates: Requirements 2.1**
462        #[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        /// Property: 中间通配符匹配 - "prefix*suffix" 应该匹配以 prefix 开头且以 suffix 结尾的字符串
471        ///
472        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
473        /// **Validates: Requirements 2.1**
474        #[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        /// Property: has_wildcards 正确检测通配符
490        ///
491        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
492        /// **Validates: Requirements 2.1**
493        #[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        /// Property: 无通配符的模式等同于精确匹配
505        ///
506        /// **Feature: tool-permission-system, Property 4: Tool Name Pattern Matching**
507        /// **Validates: Requirements 2.1**
508        #[test]
509        fn prop_no_wildcard_exact_match(
510            value in tool_name_strategy(),
511            pattern in tool_name_strategy()
512        ) {
513            // 无通配符时,匹配等同于字符串相等
514            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}