1use alloc::string::String;
2use core::fmt;
3use core::hash::{Hash, Hasher};
4use core::str::FromStr;
5
6use crate::error::Error;
7use crate::parse;
8
9#[derive(Clone, Debug)]
24pub struct Glob {
25 pattern: String,
26}
27
28impl Glob {
29 pub fn new(pattern: &str) -> Result<Self, Error> {
36 parse::validate(pattern)?;
37 Ok(Self {
38 pattern: String::from(pattern),
39 })
40 }
41
42 pub fn glob(&self) -> &str {
44 &self.pattern
45 }
46
47 pub fn compile_matcher(&self) -> GlobMatcher {
49 GlobMatcher { glob: self.clone() }
50 }
51}
52
53impl Eq for Glob {}
54
55impl PartialEq for Glob {
56 fn eq(&self, other: &Self) -> bool {
57 self.pattern == other.pattern
58 }
59}
60
61impl Hash for Glob {
62 fn hash<H: Hasher>(&self, state: &mut H) {
63 self.pattern.hash(state);
64 }
65}
66
67impl fmt::Display for Glob {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{}", self.pattern)
70 }
71}
72
73impl FromStr for Glob {
74 type Err = Error;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 Self::new(s)
78 }
79}
80
81#[derive(Clone, Debug)]
86#[allow(clippy::struct_excessive_bools)]
87pub struct GlobBuilder {
88 pattern: String,
89 case_insensitive: bool,
90 literal_separator: bool,
91 backslash_escape: bool,
92 empty_alternates: bool,
93}
94
95impl GlobBuilder {
96 pub fn new(pattern: &str) -> Self {
98 Self {
99 pattern: String::from(pattern),
100 case_insensitive: false,
101 literal_separator: false,
102 backslash_escape: true,
103 empty_alternates: false,
104 }
105 }
106
107 pub fn case_insensitive(&mut self, yes: bool) -> &mut Self {
112 self.case_insensitive = yes;
113 self
114 }
115
116 pub fn literal_separator(&mut self, yes: bool) -> &mut Self {
122 self.literal_separator = yes;
123 self
124 }
125
126 pub fn backslash_escape(&mut self, yes: bool) -> &mut Self {
130 self.backslash_escape = yes;
131 self
132 }
133
134 pub fn empty_alternates(&mut self, yes: bool) -> &mut Self {
138 self.empty_alternates = yes;
139 self
140 }
141
142 pub fn build(&self) -> Result<Glob, Error> {
149 let pattern = if self.case_insensitive {
150 self.pattern.to_ascii_lowercase()
151 } else {
152 self.pattern.clone()
153 };
154 parse::validate(&pattern)?;
155 Ok(Glob { pattern })
156 }
157}
158
159#[derive(Clone, Debug)]
163pub struct GlobMatcher {
164 glob: Glob,
165}
166
167impl GlobMatcher {
168 pub fn glob(&self) -> &Glob {
170 &self.glob
171 }
172
173 pub fn is_match(&self, path: impl AsRef<str>) -> bool {
175 glob_matcher::glob_match(self.glob.pattern.as_str(), path.as_ref())
176 }
177
178 pub fn is_match_candidate(&self, candidate: &Candidate<'_>) -> bool {
180 glob_matcher::glob_match(self.glob.pattern.as_str(), candidate.path())
181 }
182}
183
184#[derive(Clone, Debug)]
189pub struct Candidate<'a> {
190 path: CandidatePath<'a>,
192}
193
194#[derive(Clone, Debug)]
195enum CandidatePath<'a> {
196 Borrowed(&'a str),
197 Owned(String),
198}
199
200impl<'a> Candidate<'a> {
201 pub fn new(path: &'a str) -> Self {
205 if path.contains('\\') {
206 Self {
207 path: CandidatePath::Owned(path.replace('\\', "/")),
208 }
209 } else {
210 Self {
211 path: CandidatePath::Borrowed(path),
212 }
213 }
214 }
215
216 pub fn path(&self) -> &str {
218 match &self.path {
219 CandidatePath::Borrowed(s) => s,
220 CandidatePath::Owned(s) => s.as_str(),
221 }
222 }
223}
224
225#[cfg(test)]
226#[allow(clippy::unwrap_used)]
227mod tests {
228 use super::*;
229 use alloc::string::ToString;
230
231 #[test]
232 fn glob_new_valid() {
233 assert!(Glob::new("*.rs").is_ok());
234 assert!(Glob::new("**/*.txt").is_ok());
235 assert!(Glob::new("{a,b}").is_ok());
236 }
237
238 #[test]
239 fn glob_new_invalid() {
240 assert!(Glob::new("[unclosed").is_err());
241 assert!(Glob::new("{unclosed").is_err());
242 }
243
244 #[test]
245 fn glob_matcher_basic() {
246 let m = Glob::new("*.rs").unwrap().compile_matcher();
247 assert!(m.is_match("foo.rs"));
248 assert!(m.is_match("bar.rs"));
249 assert!(!m.is_match("foo.txt"));
250 assert!(!m.is_match("src/foo.rs"));
251 }
252
253 #[test]
254 fn glob_matcher_globstar() {
255 let m = Glob::new("**/*.rs").unwrap().compile_matcher();
256 assert!(m.is_match("foo.rs"));
257 assert!(m.is_match("src/foo.rs"));
258 assert!(m.is_match("a/b/c/foo.rs"));
259 assert!(!m.is_match("foo.txt"));
260 }
261
262 #[test]
263 fn glob_matcher_braces() {
264 let m = Glob::new("*.{rs,toml}").unwrap().compile_matcher();
265 assert!(m.is_match("Cargo.toml"));
266 assert!(m.is_match("main.rs"));
267 assert!(!m.is_match("main.js"));
268 }
269
270 #[test]
271 fn glob_builder_case_insensitive() {
272 let g = GlobBuilder::new("*.RS")
273 .case_insensitive(true)
274 .build()
275 .unwrap();
276 let m = g.compile_matcher();
277 assert!(m.is_match("foo.rs"));
279 }
282
283 #[test]
284 fn glob_display() {
285 let g = Glob::new("**/*.rs").unwrap();
286 assert_eq!(g.to_string(), "**/*.rs");
287 }
288
289 #[test]
290 fn glob_from_str() {
291 let g: Glob = "*.txt".parse().unwrap();
292 assert_eq!(g.glob(), "*.txt");
293 }
294
295 #[test]
296 fn candidate_no_backslash() {
297 let c = Candidate::new("a/b/c");
298 assert_eq!(c.path(), "a/b/c");
299 }
300
301 #[test]
302 fn candidate_backslash_normalization() {
303 let c = Candidate::new("a\\b\\c");
304 assert_eq!(c.path(), "a/b/c");
305 }
306
307 #[test]
308 fn candidate_matching() {
309 let m = Glob::new("**/*.rs").unwrap().compile_matcher();
310 let c = Candidate::new("src\\main.rs");
311 assert!(m.is_match_candidate(&c));
312 }
313}