1use fancy_regex::Regex;
2
3use crate::{make_re, CompileOptions};
4
5#[derive(Debug)]
6pub enum MatchError {
7 EmptyPattern,
8 UnsupportedPattern(String),
9 InvalidRegex(String),
10}
11
12pub struct Matcher {
13 glob: String,
14 options: CompileOptions,
15 regex: Regex,
16}
17
18impl Matcher {
19 pub fn is_match(&self, input: &str) -> Result<bool, MatchError> {
20 if input.is_empty() {
21 return Ok(false);
22 }
23
24 let output = normalize_input(input, &self.options);
25
26 if input == self.glob || output == self.glob {
27 return Ok(true);
28 }
29
30 let candidate = if self.options.match_base || self.options.basename {
31 basename(input, self.options.windows)
32 } else {
33 output
34 };
35
36 self.regex
37 .is_match(&candidate)
38 .map_err(|err| MatchError::InvalidRegex(err.to_string()))
39 }
40}
41
42pub fn compile_matcher(pattern: &str, options: &CompileOptions) -> Result<Matcher, MatchError> {
43 if pattern.is_empty() {
44 return Err(MatchError::EmptyPattern);
45 }
46
47 let descriptor = make_re(pattern, options, false)
48 .ok_or_else(|| MatchError::UnsupportedPattern(pattern.to_string()))?;
49 let regex = Regex::new(®ex_source(&descriptor.source, &descriptor.flags))
50 .map_err(|err| MatchError::InvalidRegex(err.to_string()))?;
51
52 Ok(Matcher {
53 glob: pattern.to_string(),
54 options: options.clone(),
55 regex,
56 })
57}
58
59fn regex_source(source: &str, flags: &str) -> String {
60 if flags.is_empty() {
61 return source.to_string();
62 }
63
64 let mut inline = String::new();
65 if flags.contains('i') {
66 inline.push('i');
67 }
68
69 if inline.is_empty() {
70 source.to_string()
71 } else {
72 format!("(?{inline}){source}")
73 }
74}
75
76pub fn is_match(input: &str, pattern: &str, options: &CompileOptions) -> Result<bool, MatchError> {
77 compile_matcher(pattern, options)?.is_match(input)
78}
79
80pub fn is_match_any<'a, I>(
81 input: &str,
82 patterns: I,
83 options: &CompileOptions,
84) -> Result<bool, MatchError>
85where
86 I: IntoIterator<Item = &'a str>,
87{
88 for pattern in patterns {
89 if is_match(input, pattern, options)? {
90 return Ok(true);
91 }
92 }
93
94 Ok(false)
95}
96
97fn normalize_input(input: &str, options: &CompileOptions) -> String {
98 let _ = options;
99 input.to_string()
100}
101
102fn basename(input: &str, windows: bool) -> String {
103 let parts: Vec<&str> = if windows {
104 input.split(['/', '\\']).collect()
105 } else {
106 input.split('/').collect()
107 };
108
109 match parts.last().copied() {
110 Some("") => parts
111 .get(parts.len().saturating_sub(2))
112 .copied()
113 .unwrap_or_default()
114 .to_string(),
115 Some(value) => value.to_string(),
116 None => String::new(),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::{basename, is_match, is_match_any};
123 use crate::CompileOptions;
124
125 #[test]
126 fn matches_windows_literals() {
127 let options = CompileOptions {
128 windows: true,
129 ..CompileOptions::default()
130 };
131
132 assert!(is_match("aaa\\bbb", "aaa/bbb", &options).unwrap());
133 assert!(is_match("aaa/bbb", "aaa/bbb", &options).unwrap());
134 }
135
136 #[test]
137 fn matches_against_any_pattern() {
138 assert!(is_match_any("ab", ["*b", "foo"], &CompileOptions::default()).unwrap());
139 assert!(!is_match_any("ab", ["foo", "bar"], &CompileOptions::default()).unwrap());
140 }
141
142 #[test]
143 fn extracts_basename() {
144 assert_eq!(basename("a/b/c.md", false), "c.md");
145 assert_eq!(basename("a\\b\\c.md", true), "c.md");
146 assert_eq!(basename("a/b/", false), "b");
147 }
148
149 #[test]
152 fn should_basename_paths() {
153 assert_eq!(basename("/a/b/c", false), "c");
154 assert_eq!(basename("/a/b/c/", false), "c");
155 assert_eq!(basename("/a\\b/c", true), "c");
156 assert_eq!(basename("/a\\b/c\\", true), "c");
157 assert_eq!(basename("\\a/b\\c", true), "c");
158 assert_eq!(basename("\\a/b\\c/", true), "c");
159 }
160
161 #[test]
162 fn honors_case_insensitive_flag() {
163 let options = CompileOptions {
164 flags: "i".to_string(),
165 ..CompileOptions::default()
166 };
167
168 assert!(is_match("A/B/C.MD", "a/b/*.md", &options).unwrap());
169 }
170}