1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5enum MatchToken {
6 Literal(char),
7 Star,
8 Question,
9}
10
11fn tokenize_wildcard_pattern(pattern: &str) -> Vec<MatchToken> {
12 let pattern_chars: Vec<char> = pattern.chars().collect();
13 let mut tokens = Vec::new();
14 let mut pattern_index = 0;
15
16 while pattern_index < pattern_chars.len() {
17 match pattern_chars[pattern_index] {
18 '\\' => {
19 if let Some(next_char) = pattern_chars.get(pattern_index + 1) {
20 tokens.push(MatchToken::Literal(*next_char));
21 pattern_index += 2;
22 } else {
23 tokens.push(MatchToken::Literal('\\'));
24 pattern_index += 1;
25 }
26 }
27 '*' => {
28 tokens.push(MatchToken::Star);
29 pattern_index += 1;
30 }
31 '?' => {
32 tokens.push(MatchToken::Question);
33 pattern_index += 1;
34 }
35 literal => {
36 tokens.push(MatchToken::Literal(literal));
37 pattern_index += 1;
38 }
39 }
40 }
41
42 tokens
43}
44
45fn escape_regex_char(character: char, output: &mut String) {
46 match character {
47 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\' => {
48 output.push('\\');
49 output.push(character);
50 }
51 _ => output.push(character),
52 }
53}
54
55fn wildcard_matches_impl(pattern: &str, input: &str) -> bool {
56 let tokens = tokenize_wildcard_pattern(pattern);
57 let input_chars: Vec<char> = input.chars().collect();
58 let token_count = tokens.len();
59 let input_count = input_chars.len();
60 let mut matrix = vec![vec![false; input_count + 1]; token_count + 1];
61
62 matrix[0][0] = true;
63
64 for token_index in 1..=token_count {
65 if matches!(tokens[token_index - 1], MatchToken::Star) {
66 matrix[token_index][0] = matrix[token_index - 1][0];
67 }
68 }
69
70 for token_index in 1..=token_count {
71 for input_index in 1..=input_count {
72 matrix[token_index][input_index] = match tokens[token_index - 1] {
73 MatchToken::Literal(expected) => {
74 matrix[token_index - 1][input_index - 1]
75 && input_chars[input_index - 1] == expected
76 }
77 MatchToken::Question => matrix[token_index - 1][input_index - 1],
78 MatchToken::Star => {
79 matrix[token_index - 1][input_index] || matrix[token_index][input_index - 1]
80 }
81 };
82 }
83 }
84
85 matrix[token_count][input_count]
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct WildcardPattern {
91 pub pattern: String,
92 pub case_sensitive: bool,
93}
94
95pub fn has_wildcard(input: &str) -> bool {
97 let tokens = tokenize_wildcard_pattern(input);
98
99 tokens
100 .iter()
101 .any(|token| matches!(token, MatchToken::Star | MatchToken::Question))
102}
103
104pub fn escape_wildcard(input: &str) -> String {
106 let mut escaped = String::new();
107
108 for character in input.chars() {
109 if matches!(character, '*' | '?' | '\\') {
110 escaped.push('\\');
111 }
112
113 escaped.push(character);
114 }
115
116 escaped
117}
118
119pub fn wildcard_to_regex(pattern: &str) -> String {
121 let tokens = tokenize_wildcard_pattern(pattern);
122 let mut regex_pattern = String::from("(?s)^");
123
124 for token in tokens {
125 match token {
126 MatchToken::Literal(character) => escape_regex_char(character, &mut regex_pattern),
127 MatchToken::Star => regex_pattern.push_str(".*"),
128 MatchToken::Question => regex_pattern.push('.'),
129 }
130 }
131
132 regex_pattern.push('$');
133 regex_pattern
134}
135
136pub fn wildcard_matches(pattern: &str, input: &str) -> bool {
138 wildcard_matches_impl(pattern, input)
139}
140
141pub fn wildcard_matches_case_insensitive(pattern: &str, input: &str) -> bool {
143 wildcard_matches_impl(&pattern.to_lowercase(), &input.to_lowercase())
144}
145
146pub fn starts_with_wildcard(pattern: &str) -> bool {
148 matches!(
149 tokenize_wildcard_pattern(pattern).first(),
150 Some(MatchToken::Star | MatchToken::Question)
151 )
152}
153
154pub fn ends_with_wildcard(pattern: &str) -> bool {
156 matches!(
157 tokenize_wildcard_pattern(pattern).last(),
158 Some(MatchToken::Star | MatchToken::Question)
159 )
160}