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()
262 && pattern_idx + 1 >= pattern_segments.len();
263 }
264
265 let path_segment = path_segments[path_idx];
266 let matches = if self.case_sensitive {
267 literal == path_segment
268 } else {
269 literal.eq_ignore_ascii_case(path_segment)
270 };
271
272 if matches {
273 self.match_segments(
274 pattern_segments,
275 path_segments,
276 pattern_idx + 1,
277 path_idx + 1,
278 variables,
279 )
280 } else {
281 false
282 }
283 }
284 }
285 }
286
287 fn match_pattern(&self, pattern: &str, text: &str) -> bool {
289 let pattern_chars: Vec<char> = pattern.chars().collect();
290 let text_chars: Vec<char> = if self.case_sensitive {
291 text.chars().collect()
292 } else {
293 text.to_lowercase().chars().collect()
294 };
295
296 let pattern_lower: Vec<char> = if self.case_sensitive {
297 pattern_chars.clone()
298 } else {
299 pattern.to_lowercase().chars().collect()
300 };
301
302 self.match_pattern_chars(&pattern_lower, &text_chars, 0, 0)
303 }
304
305 fn match_pattern_chars(
307 &self,
308 pattern: &[char],
309 text: &[char],
310 p_idx: usize,
311 t_idx: usize,
312 ) -> bool {
313 if p_idx >= pattern.len() && t_idx >= text.len() {
315 return true;
316 }
317
318 if p_idx >= pattern.len() {
320 return false;
321 }
322
323 let p_char = pattern[p_idx];
324
325 match p_char {
326 '*' => {
327 for skip in 0..=(text.len() - t_idx) {
329 if self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + skip) {
330 return true;
331 }
332 }
333 false
334 }
335 '?' => {
336 if t_idx >= text.len() {
338 return false;
339 }
340 self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + 1)
341 }
342 _ => {
343 if t_idx >= text.len() {
345 return false;
346 }
347 if p_char == text[t_idx] {
348 self.match_pattern_chars(pattern, text, p_idx + 1, t_idx + 1)
349 } else {
350 false
351 }
352 }
353 }
354 }
355}
356
357#[derive(Debug, Clone, Default)]
359pub struct AntMatcherBuilder {
360 case_sensitive: bool,
361}
362
363impl AntMatcherBuilder {
364 pub fn new() -> Self {
366 Self {
367 case_sensitive: true,
368 }
369 }
370
371 pub fn case_sensitive(mut self, sensitive: bool) -> Self {
373 self.case_sensitive = sensitive;
374 self
375 }
376
377 pub fn build(&self, pattern: &str) -> AntMatcher {
379 let mut matcher = AntMatcher::new(pattern);
380 if !self.case_sensitive {
381 matcher = matcher.case_insensitive();
382 }
383 matcher
384 }
385}
386
387#[derive(Debug, Clone, Default)]
389pub struct AntMatchers {
390 matchers: Vec<AntMatcher>,
391}
392
393impl AntMatchers {
394 pub fn new() -> Self {
396 Self {
397 matchers: Vec::new(),
398 }
399 }
400
401 #[allow(clippy::should_implement_trait)]
403 pub fn add(mut self, pattern: &str) -> Self {
404 self.matchers.push(AntMatcher::new(pattern));
405 self
406 }
407
408 pub fn add_all(mut self, patterns: &[&str]) -> Self {
410 for pattern in patterns {
411 self.matchers.push(AntMatcher::new(pattern));
412 }
413 self
414 }
415
416 pub fn matches(&self, path: &str) -> bool {
418 self.matchers.iter().any(|m| m.matches(path))
419 }
420
421 pub fn find_match(&self, path: &str) -> Option<&AntMatcher> {
423 self.matchers.iter().find(|m| m.matches(path))
424 }
425
426 pub fn find_all_matches(&self, path: &str) -> Vec<&AntMatcher> {
428 self.matchers.iter().filter(|m| m.matches(path)).collect()
429 }
430
431 pub fn len(&self) -> usize {
433 self.matchers.len()
434 }
435
436 pub fn is_empty(&self) -> bool {
438 self.matchers.is_empty()
439 }
440}
441
442pub trait IntoAntMatcher {
444 fn into_ant_matcher(self) -> AntMatcher;
445}
446
447impl IntoAntMatcher for &str {
448 fn into_ant_matcher(self) -> AntMatcher {
449 AntMatcher::new(self)
450 }
451}
452
453impl IntoAntMatcher for String {
454 fn into_ant_matcher(self) -> AntMatcher {
455 AntMatcher::new(&self)
456 }
457}
458
459impl IntoAntMatcher for AntMatcher {
460 fn into_ant_matcher(self) -> AntMatcher {
461 self
462 }
463}
464
465#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_literal_match() {
475 let matcher = AntMatcher::new("/api/users");
476 assert!(matcher.matches("/api/users"));
477 assert!(matcher.matches("/api/users/"));
479 assert!(!matcher.matches("/api/user"));
480 assert!(!matcher.matches("/api/users/123"));
481 }
482
483 #[test]
484 fn test_single_wildcard() {
485 let matcher = AntMatcher::new("/users/*/profile");
486 assert!(matcher.matches("/users/123/profile"));
487 assert!(matcher.matches("/users/abc/profile"));
488 assert!(!matcher.matches("/users/profile"));
489 assert!(!matcher.matches("/users/123/456/profile"));
490 }
491
492 #[test]
493 fn test_double_wildcard() {
494 let matcher = AntMatcher::new("/api/**");
495 assert!(matcher.matches("/api/"));
496 assert!(matcher.matches("/api/users"));
497 assert!(matcher.matches("/api/users/123"));
498 assert!(matcher.matches("/api/users/123/posts"));
499 assert!(!matcher.matches("/other/path"));
500 }
501
502 #[test]
503 fn test_double_wildcard_middle() {
504 let matcher = AntMatcher::new("/api/**/edit");
505 assert!(matcher.matches("/api/edit"));
506 assert!(matcher.matches("/api/users/edit"));
507 assert!(matcher.matches("/api/users/123/edit"));
508 assert!(!matcher.matches("/api/users/123"));
509 }
510
511 #[test]
512 fn test_question_mark() {
513 let matcher = AntMatcher::new("/file?.txt");
514 assert!(matcher.matches("/file1.txt"));
515 assert!(matcher.matches("/fileA.txt"));
516 assert!(!matcher.matches("/file12.txt"));
517 assert!(!matcher.matches("/file.txt"));
518 }
519
520 #[test]
521 fn test_pattern_wildcard() {
522 let matcher = AntMatcher::new("/files/*.txt");
523 assert!(matcher.matches("/files/document.txt"));
524 assert!(matcher.matches("/files/test.txt"));
525 assert!(!matcher.matches("/files/document.pdf"));
526 assert!(!matcher.matches("/files/subdir/document.txt"));
527 }
528
529 #[test]
530 fn test_variable_extraction() {
531 let matcher = AntMatcher::new("/users/{id}");
532 let vars = matcher.extract_variables("/users/123");
533 assert!(vars.is_some());
534 let vars = vars.unwrap();
535 assert_eq!(vars.get("id"), Some(&"123".to_string()));
536 }
537
538 #[test]
539 fn test_multiple_variables() {
540 let matcher = AntMatcher::new("/users/{userId}/posts/{postId}");
541 let vars = matcher.extract_variables("/users/42/posts/99");
542 assert!(vars.is_some());
543 let vars = vars.unwrap();
544 assert_eq!(vars.get("userId"), Some(&"42".to_string()));
545 assert_eq!(vars.get("postId"), Some(&"99".to_string()));
546 }
547
548 #[test]
549 fn test_case_insensitive() {
550 let matcher = AntMatcher::new("/Api/Users").case_insensitive();
551 assert!(matcher.matches("/api/users"));
552 assert!(matcher.matches("/API/USERS"));
553 assert!(matcher.matches("/Api/Users"));
554 }
555
556 #[test]
557 fn test_root_path() {
558 let matcher = AntMatcher::new("/");
559 assert!(matcher.matches("/"));
560 }
561
562 #[test]
563 fn test_complex_pattern() {
564 let matcher = AntMatcher::new("/api/v*/users/**/profile");
565 assert!(matcher.matches("/api/v1/users/123/profile"));
566 assert!(matcher.matches("/api/v2/users/123/posts/456/profile"));
567 assert!(!matcher.matches("/api/users/123/profile"));
568 }
569
570 #[test]
571 fn test_ant_matchers_collection() {
572 let matchers = AntMatchers::new()
573 .add("/api/**")
574 .add("/public/**")
575 .add("/health");
576
577 assert!(matchers.matches("/api/users"));
578 assert!(matchers.matches("/public/images/logo.png"));
579 assert!(matchers.matches("/health"));
580 assert!(!matchers.matches("/private/data"));
581 }
582
583 #[test]
584 fn test_ant_matchers_find() {
585 let matchers = AntMatchers::new()
586 .add("/api/**")
587 .add("/admin/**");
588
589 let found = matchers.find_match("/api/users");
590 assert!(found.is_some());
591 assert_eq!(found.unwrap().pattern(), "/api/**");
592 }
593
594 #[test]
595 fn test_builder() {
596 let builder = AntMatcherBuilder::new().case_sensitive(false);
597 let matcher = builder.build("/API/USERS");
598 assert!(matcher.matches("/api/users"));
599 }
600
601 #[test]
602 fn test_into_ant_matcher() {
603 let m1: AntMatcher = "/api/**".into_ant_matcher();
604 let m2: AntMatcher = String::from("/users/*").into_ant_matcher();
605
606 assert!(m1.matches("/api/test"));
607 assert!(m2.matches("/users/123"));
608 }
609
610 #[test]
611 fn test_trailing_slash() {
612 let matcher = AntMatcher::new("/api/users/");
613 assert!(matcher.matches("/api/users")); assert!(matcher.matches("/api/users/")); }
617
618 #[test]
619 fn test_mixed_wildcards() {
620 let matcher = AntMatcher::new("/api/*/items/**");
621 assert!(matcher.matches("/api/v1/items/1"));
622 assert!(matcher.matches("/api/v1/items/1/2/3"));
623 assert!(matcher.matches("/api/v2/items/"));
624 assert!(!matcher.matches("/api/v1/v2/items/1"));
625 }
626
627 #[test]
628 fn test_pattern_segment_equality() {
629 assert_eq!(PatternSegment::SingleWildcard, PatternSegment::SingleWildcard);
630 assert_eq!(PatternSegment::DoubleWildcard, PatternSegment::DoubleWildcard);
631 assert_eq!(
632 PatternSegment::Literal("test".to_string()),
633 PatternSegment::Literal("test".to_string())
634 );
635 }
636}