actix_security_core/http/security/
ant_matcher.rs1use std::collections::HashMap;
40
41#[derive(Debug, Clone)]
45pub struct AntMatcher {
46 pattern: String,
47 segments: Vec<PatternSegment>,
48 case_sensitive: bool,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53enum PatternSegment {
54 Literal(String),
56 SingleWildcard,
58 DoubleWildcard,
60 Pattern(String),
62 Variable(String),
64}
65
66impl AntMatcher {
67 pub fn new(pattern: &str) -> Self {
83 let segments = Self::parse_pattern(pattern);
84 Self {
85 pattern: pattern.to_string(),
86 segments,
87 case_sensitive: true,
88 }
89 }
90
91 pub fn case_insensitive(mut self) -> Self {
93 self.case_sensitive = false;
94 self
95 }
96
97 pub fn pattern(&self) -> &str {
99 &self.pattern
100 }
101
102 fn parse_pattern(pattern: &str) -> Vec<PatternSegment> {
104 let mut segments = Vec::new();
105 let trimmed = pattern.trim_start_matches('/');
106
107 if trimmed.is_empty() {
108 return vec![PatternSegment::Literal(String::new())];
109 }
110
111 for part in trimmed.split('/') {
112 let segment = if part == "**" {
113 PatternSegment::DoubleWildcard
114 } else if part == "*" {
115 PatternSegment::SingleWildcard
116 } else if part.starts_with('{') && part.ends_with('}') {
117 let var_name = part[1..part.len() - 1].to_string();
118 PatternSegment::Variable(var_name)
119 } else if part.contains('*') || part.contains('?') {
120 PatternSegment::Pattern(part.to_string())
121 } else {
122 PatternSegment::Literal(part.to_string())
123 };
124 segments.push(segment);
125 }
126
127 segments
128 }
129
130 pub fn matches(&self, path: &str) -> bool {
141 self.do_match(path, &mut None)
142 }
143
144 pub fn extract_variables(&self, path: &str) -> Option<HashMap<String, String>> {
159 let mut variables = HashMap::new();
160 if self.do_match(path, &mut Some(&mut variables)) {
161 Some(variables)
162 } else {
163 None
164 }
165 }
166
167 fn do_match(&self, path: &str, variables: &mut Option<&mut HashMap<String, String>>) -> bool {
169 let path_segments: Vec<&str> = path
170 .trim_start_matches('/')
171 .split('/')
172 .filter(|s| !s.is_empty())
173 .collect();
174
175 self.match_segments(&self.segments, &path_segments, 0, 0, variables)
176 }
177
178 fn match_segments(
180 &self,
181 pattern_segments: &[PatternSegment],
182 path_segments: &[&str],
183 pattern_idx: usize,
184 path_idx: usize,
185 variables: &mut Option<&mut HashMap<String, String>>,
186 ) -> bool {
187 if pattern_idx >= pattern_segments.len() && path_idx >= path_segments.len() {
189 return true;
190 }
191
192 if pattern_idx >= pattern_segments.len() {
194 return false;
195 }
196
197 let pattern_segment = &pattern_segments[pattern_idx];
198
199 match pattern_segment {
200 PatternSegment::DoubleWildcard => {
201 for skip in 0..=(path_segments.len() - path_idx) {
204 if self.match_segments(
205 pattern_segments,
206 path_segments,
207 pattern_idx + 1,
208 path_idx + skip,
209 variables,
210 ) {
211 return true;
212 }
213 }
214 false
215 }
216
217 PatternSegment::SingleWildcard | PatternSegment::Variable(_) => {
218 if path_idx >= path_segments.len() {
220 return false;
221 }
222
223 if let PatternSegment::Variable(name) = pattern_segment {
225 if let Some(ref mut vars) = variables {
226 vars.insert(name.clone(), path_segments[path_idx].to_string());
227 }
228 }
229
230 self.match_segments(
231 pattern_segments,
232 path_segments,
233 pattern_idx + 1,
234 path_idx + 1,
235 variables,
236 )
237 }
238
239 PatternSegment::Pattern(pattern) => {
240 if path_idx >= path_segments.len() {
242 return false;
243 }
244
245 if self.match_pattern(pattern, path_segments[path_idx]) {
246 self.match_segments(
247 pattern_segments,
248 path_segments,
249 pattern_idx + 1,
250 path_idx + 1,
251 variables,
252 )
253 } else {
254 false
255 }
256 }
257
258 PatternSegment::Literal(literal) => {
259 if path_idx >= path_segments.len() {
260 return literal.is_empty() && pattern_idx + 1 >= pattern_segments.len();
262 }
263
264 let path_segment = path_segments[path_idx];
265 let matches = if self.case_sensitive {
266 literal == path_segment
267 } else {
268 literal.eq_ignore_ascii_case(path_segment)
269 };
270
271 if matches {
272 self.match_segments(
273 pattern_segments,
274 path_segments,
275 pattern_idx + 1,
276 path_idx + 1,
277 variables,
278 )
279 } else {
280 false
281 }
282 }
283 }
284 }
285
286 fn match_pattern(&self, pattern: &str, text: &str) -> bool {
288 let pattern_chars: Vec<char> = pattern.chars().collect();
289 let text_chars: Vec<char> = if self.case_sensitive {
290 text.chars().collect()
291 } else {
292 text.to_lowercase().chars().collect()
293 };
294
295 let pattern_lower: Vec<char> = if self.case_sensitive {
296 pattern_chars.clone()
297 } else {
298 pattern.to_lowercase().chars().collect()
299 };
300
301 self.match_pattern_chars(&pattern_lower, &text_chars, 0, 0)
302 }
303
304 fn match_pattern_chars(
306 &self,
307 pattern: &[char],
308 text: &[char],
309 p_idx: usize,
310 t_idx: usize,
311 ) -> bool {
312 if p_idx >= pattern.len() && t_idx >= text.len() {
314 return true;
315 }
316
317 if p_idx >= pattern.len() {
319 return false;
320 }
321
322 let p_char = pattern[p_idx];
323
324 match p_char {
325 '*' => {
326 for skip in 0..=(text.len() - t_idx) {
328 if self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + skip) {
329 return true;
330 }
331 }
332 false
333 }
334 '?' => {
335 if t_idx >= text.len() {
337 return false;
338 }
339 self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + 1)
340 }
341 _ => {
342 if t_idx >= text.len() {
344 return false;
345 }
346 if p_char == text[t_idx] {
347 self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + 1)
348 } else {
349 false
350 }
351 }
352 }
353 }
354}
355
356#[derive(Debug, Clone, Default)]
358pub struct AntMatcherBuilder {
359 case_sensitive: bool,
360}
361
362impl AntMatcherBuilder {
363 pub fn new() -> Self {
365 Self {
366 case_sensitive: true,
367 }
368 }
369
370 pub fn case_sensitive(mut self, sensitive: bool) -> Self {
372 self.case_sensitive = sensitive;
373 self
374 }
375
376 pub fn build(&self, pattern: &str) -> AntMatcher {
378 let mut matcher = AntMatcher::new(pattern);
379 if !self.case_sensitive {
380 matcher = matcher.case_insensitive();
381 }
382 matcher
383 }
384}
385
386#[derive(Debug, Clone, Default)]
388pub struct AntMatchers {
389 matchers: Vec<AntMatcher>,
390}
391
392impl AntMatchers {
393 pub fn new() -> Self {
395 Self {
396 matchers: Vec::new(),
397 }
398 }
399
400 #[allow(clippy::should_implement_trait)]
402 pub fn add(mut self, pattern: &str) -> Self {
403 self.matchers.push(AntMatcher::new(pattern));
404 self
405 }
406
407 pub fn add_all(mut self, patterns: &[&str]) -> Self {
409 for pattern in patterns {
410 self.matchers.push(AntMatcher::new(pattern));
411 }
412 self
413 }
414
415 pub fn matches(&self, path: &str) -> bool {
417 self.matchers.iter().any(|m| m.matches(path))
418 }
419
420 pub fn find_match(&self, path: &str) -> Option<&AntMatcher> {
422 self.matchers.iter().find(|m| m.matches(path))
423 }
424
425 pub fn find_all_matches(&self, path: &str) -> Vec<&AntMatcher> {
427 self.matchers.iter().filter(|m| m.matches(path)).collect()
428 }
429
430 pub fn len(&self) -> usize {
432 self.matchers.len()
433 }
434
435 pub fn is_empty(&self) -> bool {
437 self.matchers.is_empty()
438 }
439}
440
441pub trait IntoAntMatcher {
443 fn into_ant_matcher(self) -> AntMatcher;
444}
445
446impl IntoAntMatcher for &str {
447 fn into_ant_matcher(self) -> AntMatcher {
448 AntMatcher::new(self)
449 }
450}
451
452impl IntoAntMatcher for String {
453 fn into_ant_matcher(self) -> AntMatcher {
454 AntMatcher::new(&self)
455 }
456}
457
458impl IntoAntMatcher for AntMatcher {
459 fn into_ant_matcher(self) -> AntMatcher {
460 self
461 }
462}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_literal_match() {
474 let matcher = AntMatcher::new("/api/users");
475 assert!(matcher.matches("/api/users"));
476 assert!(matcher.matches("/api/users/"));
478 assert!(!matcher.matches("/api/user"));
479 assert!(!matcher.matches("/api/users/123"));
480 }
481
482 #[test]
483 fn test_single_wildcard() {
484 let matcher = AntMatcher::new("/users/*/profile");
485 assert!(matcher.matches("/users/123/profile"));
486 assert!(matcher.matches("/users/abc/profile"));
487 assert!(!matcher.matches("/users/profile"));
488 assert!(!matcher.matches("/users/123/456/profile"));
489 }
490
491 #[test]
492 fn test_double_wildcard() {
493 let matcher = AntMatcher::new("/api/**");
494 assert!(matcher.matches("/api/"));
495 assert!(matcher.matches("/api/users"));
496 assert!(matcher.matches("/api/users/123"));
497 assert!(matcher.matches("/api/users/123/posts"));
498 assert!(!matcher.matches("/other/path"));
499 }
500
501 #[test]
502 fn test_double_wildcard_middle() {
503 let matcher = AntMatcher::new("/api/**/edit");
504 assert!(matcher.matches("/api/edit"));
505 assert!(matcher.matches("/api/users/edit"));
506 assert!(matcher.matches("/api/users/123/edit"));
507 assert!(!matcher.matches("/api/users/123"));
508 }
509
510 #[test]
511 fn test_question_mark() {
512 let matcher = AntMatcher::new("/file?.txt");
513 assert!(matcher.matches("/file1.txt"));
514 assert!(matcher.matches("/fileA.txt"));
515 assert!(!matcher.matches("/file12.txt"));
516 assert!(!matcher.matches("/file.txt"));
517 }
518
519 #[test]
520 fn test_pattern_wildcard() {
521 let matcher = AntMatcher::new("/files/*.txt");
522 assert!(matcher.matches("/files/document.txt"));
523 assert!(matcher.matches("/files/test.txt"));
524 assert!(!matcher.matches("/files/document.pdf"));
525 assert!(!matcher.matches("/files/subdir/document.txt"));
526 }
527
528 #[test]
529 fn test_variable_extraction() {
530 let matcher = AntMatcher::new("/users/{id}");
531 let vars = matcher.extract_variables("/users/123");
532 assert!(vars.is_some());
533 let vars = vars.unwrap();
534 assert_eq!(vars.get("id"), Some(&"123".to_string()));
535 }
536
537 #[test]
538 fn test_multiple_variables() {
539 let matcher = AntMatcher::new("/users/{userId}/posts/{postId}");
540 let vars = matcher.extract_variables("/users/42/posts/99");
541 assert!(vars.is_some());
542 let vars = vars.unwrap();
543 assert_eq!(vars.get("userId"), Some(&"42".to_string()));
544 assert_eq!(vars.get("postId"), Some(&"99".to_string()));
545 }
546
547 #[test]
548 fn test_case_insensitive() {
549 let matcher = AntMatcher::new("/Api/Users").case_insensitive();
550 assert!(matcher.matches("/api/users"));
551 assert!(matcher.matches("/API/USERS"));
552 assert!(matcher.matches("/Api/Users"));
553 }
554
555 #[test]
556 fn test_root_path() {
557 let matcher = AntMatcher::new("/");
558 assert!(matcher.matches("/"));
559 }
560
561 #[test]
562 fn test_complex_pattern() {
563 let matcher = AntMatcher::new("/api/v*/users/**/profile");
564 assert!(matcher.matches("/api/v1/users/123/profile"));
565 assert!(matcher.matches("/api/v2/users/123/posts/456/profile"));
566 assert!(!matcher.matches("/api/users/123/profile"));
567 }
568
569 #[test]
570 fn test_ant_matchers_collection() {
571 let matchers = AntMatchers::new()
572 .add("/api/**")
573 .add("/public/**")
574 .add("/health");
575
576 assert!(matchers.matches("/api/users"));
577 assert!(matchers.matches("/public/images/logo.png"));
578 assert!(matchers.matches("/health"));
579 assert!(!matchers.matches("/private/data"));
580 }
581
582 #[test]
583 fn test_ant_matchers_find() {
584 let matchers = AntMatchers::new().add("/api/**").add("/admin/**");
585
586 let found = matchers.find_match("/api/users");
587 assert!(found.is_some());
588 assert_eq!(found.unwrap().pattern(), "/api/**");
589 }
590
591 #[test]
592 fn test_builder() {
593 let builder = AntMatcherBuilder::new().case_sensitive(false);
594 let matcher = builder.build("/API/USERS");
595 assert!(matcher.matches("/api/users"));
596 }
597
598 #[test]
599 fn test_into_ant_matcher() {
600 let m1: AntMatcher = "/api/**".into_ant_matcher();
601 let m2: AntMatcher = String::from("/users/*").into_ant_matcher();
602
603 assert!(m1.matches("/api/test"));
604 assert!(m2.matches("/users/123"));
605 }
606
607 #[test]
608 fn test_trailing_slash() {
609 let matcher = AntMatcher::new("/api/users/");
610 assert!(matcher.matches("/api/users")); assert!(matcher.matches("/api/users/")); }
614
615 #[test]
616 fn test_mixed_wildcards() {
617 let matcher = AntMatcher::new("/api/*/items/**");
618 assert!(matcher.matches("/api/v1/items/1"));
619 assert!(matcher.matches("/api/v1/items/1/2/3"));
620 assert!(matcher.matches("/api/v2/items/"));
621 assert!(!matcher.matches("/api/v1/v2/items/1"));
622 }
623
624 #[test]
625 fn test_pattern_segment_equality() {
626 assert_eq!(
627 PatternSegment::SingleWildcard,
628 PatternSegment::SingleWildcard
629 );
630 assert_eq!(
631 PatternSegment::DoubleWildcard,
632 PatternSegment::DoubleWildcard
633 );
634 assert_eq!(
635 PatternSegment::Literal("test".to_string()),
636 PatternSegment::Literal("test".to_string())
637 );
638 }
639}