1use std::sync::OnceLock;
4use std::{fmt, str};
5
6use regex_lite::Regex;
7
8#[derive(Debug)]
10struct GlobPatternGroups<'g> {
11 star: &'g str,
12 double_star: &'g str,
13 question_mark: &'g str,
14}
15
16#[derive(Debug)]
19pub struct GlobBuilder<'g> {
20 value: &'g str,
21 groups: GlobPatternGroups<'g>,
22}
23
24impl<'g> GlobBuilder<'g> {
25 pub fn new(value: &'g str) -> Self {
27 let opts = GlobPatternGroups {
28 star: "([^/]*?)",
29 double_star: "(.*?)",
30 question_mark: "(.)",
31 };
32 Self {
33 value,
34 groups: opts,
35 }
36 }
37
38 pub fn capture_star(mut self, enable: bool) -> Self {
40 if !enable {
41 self.groups.star = "(?:[^/]*?)";
42 }
43 self
44 }
45
46 pub fn capture_double_star(mut self, enable: bool) -> Self {
48 if !enable {
49 self.groups.double_star = "(?:.*?)";
50 }
51 self
52 }
53
54 pub fn capture_question_mark(mut self, enable: bool) -> Self {
56 if !enable {
57 self.groups.question_mark = "(?:.)";
58 }
59 self
60 }
61
62 pub fn build(self) -> Glob {
64 let mut pattern = String::with_capacity(&self.value.len() + 100);
65 let mut last = 0;
66
67 pattern.push('^');
68
69 static GLOB_RE: OnceLock<Regex> = OnceLock::new();
70 let regex = GLOB_RE.get_or_init(|| Regex::new(r"\\\?|\\\*\\\*|\\\*|\?|\*\*|\*").unwrap());
71
72 for m in regex.find_iter(self.value) {
73 pattern.push_str(®ex_lite::escape(&self.value[last..m.start()]));
74 match m.as_str() {
75 "?" => pattern.push_str(self.groups.question_mark),
76 "**" => pattern.push_str(self.groups.double_star),
77 "*" => pattern.push_str(self.groups.star),
78 _ => pattern.push_str(m.as_str()),
79 }
80 last = m.end();
81 }
82 pattern.push_str(®ex_lite::escape(&self.value[last..]));
83 pattern.push('$');
84
85 Glob {
86 value: self.value.to_owned(),
87 pattern: Regex::new(&pattern).unwrap(),
88 }
89 }
90}
91
92#[derive(Clone)]
97pub struct Glob {
98 value: String,
99 pattern: Regex,
100}
101
102impl Glob {
103 pub fn builder(glob: &'_ str) -> GlobBuilder {
105 GlobBuilder::new(glob)
106 }
107
108 pub fn new(glob: &str) -> Glob {
113 GlobBuilder::new(glob).build()
114 }
115
116 pub fn pattern(&self) -> &str {
118 &self.value
119 }
120
121 pub fn is_match(&self, value: &str) -> bool {
123 self.pattern.is_match(value)
124 }
125
126 pub fn replace_captures(&self, input: &str, replacement: &str) -> String {
129 let mut output = String::new();
130 let mut current = 0;
131
132 for caps in self.pattern.captures_iter(input) {
133 for cap in caps.iter().flatten().skip(1) {
136 output.push_str(&input[current..cap.start()]);
137 output.push_str(replacement);
138 current = cap.end();
139 }
140 }
141
142 output.push_str(&input[current..]);
143 output
144 }
145
146 pub fn matches<'t>(&self, value: &'t str) -> Option<Vec<&'t str>> {
148 self.pattern.captures(value).map(|caps| {
149 caps.iter()
150 .skip(1)
151 .map(|x| x.map_or("", |x| x.as_str()))
152 .collect()
153 })
154 }
155}
156
157impl PartialEq for Glob {
158 fn eq(&self, other: &Self) -> bool {
159 self.value == other.value
160 }
161}
162
163impl Eq for Glob {}
164
165impl str::FromStr for Glob {
166 type Err = ();
167
168 fn from_str(value: &str) -> Result<Glob, ()> {
169 Ok(Glob::new(value))
170 }
171}
172
173impl fmt::Display for Glob {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.pad(self.pattern())
176 }
177}
178
179impl fmt::Debug for Glob {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.debug_tuple("Glob").field(&self.pattern()).finish()
182 }
183}
184
185impl<'a> From<&'a str> for Glob {
186 fn from(value: &'a str) -> Glob {
187 Glob::new(value)
188 }
189}
190
191impl From<String> for Glob {
192 fn from(value: String) -> Glob {
193 Glob::new(&value)
194 }
195}
196
197crate::impl_str_serde!(Glob, "a glob pattern");
198
199#[derive(Debug)]
201pub struct GlobMatcher<T> {
202 globs: Vec<(Glob, T)>,
203}
204
205impl<T: Clone> Default for GlobMatcher<T> {
206 fn default() -> GlobMatcher<T> {
207 GlobMatcher::new()
208 }
209}
210
211impl<T: Clone> GlobMatcher<T> {
212 pub fn new() -> GlobMatcher<T> {
214 GlobMatcher { globs: vec![] }
215 }
216
217 pub fn add<G: Into<Glob>>(&mut self, glob: G, ident: T) {
219 self.globs.push((glob.into(), ident));
220 }
221
222 pub fn test(&self, s: &str) -> Option<T> {
224 for (glob, ident) in &self.globs {
225 if glob.is_match(s) {
226 return Some(ident.clone());
227 }
228 }
229 None
230 }
231
232 pub fn matches<'a>(&self, s: &'a str) -> Option<(Vec<&'a str>, T)> {
234 for (pat, ident) in &self.globs {
235 if let Some(matches) = pat.matches(s) {
236 return Some((matches, ident.clone()));
237 }
238 }
239 None
240 }
241}
242
243#[derive(Clone, Eq, PartialEq)]
247pub struct LazyGlob {
248 raw: String,
249 glob: OnceLock<Glob>,
250}
251
252impl LazyGlob {
253 pub fn new(raw: impl Into<String>) -> Self {
255 Self {
256 raw: raw.into(),
257 glob: OnceLock::new(),
258 }
259 }
260
261 pub fn compiled(&self) -> &Glob {
263 self.glob.get_or_init(|| {
264 Glob::builder(&self.raw)
265 .capture_star(true)
266 .capture_double_star(false)
267 .capture_question_mark(false)
268 .build()
269 })
270 }
271
272 pub fn as_str(&self) -> &str {
274 &self.raw
275 }
276}
277
278impl fmt::Debug for LazyGlob {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 write!(f, "LazyGlob({:?})", self.raw)
281 }
282}
283
284impl serde::Serialize for LazyGlob {
285 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
286 where
287 S: serde::Serializer,
288 {
289 serializer.serialize_str(&self.raw)
290 }
291}
292
293impl<'de> serde::Deserialize<'de> for LazyGlob {
294 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
295 where
296 D: serde::Deserializer<'de>,
297 {
298 String::deserialize(deserializer).map(LazyGlob::new)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_glob() {
308 let g = Glob::new("foo/*/bar");
309 assert!(g.is_match("foo/blah/bar"));
310 assert!(!g.is_match("foo/blah/bar/aha"));
311
312 let g = Glob::new("foo/???/bar");
313 assert!(g.is_match("foo/aha/bar"));
314 assert!(!g.is_match("foo/ah/bar"));
315 assert!(!g.is_match("foo/ahah/bar"));
316
317 let g = Glob::new("*/foo.txt");
318 assert!(g.is_match("prefix/foo.txt"));
319 assert!(!g.is_match("double/prefix/foo.txt"));
320
321 let g = Glob::new("api/**/store/");
322 assert!(g.is_match("api/some/stuff/here/store/"));
323 assert!(g.is_match("api/some/store/"));
324
325 let g = Glob::new("/api/*/stuff/**");
326 assert!(g.is_match("/api/some/stuff/here/store/"));
327 assert!(!g.is_match("/api/some/store/"));
328
329 let g = Glob::new(r"/api/\*/stuff");
330 assert!(g.is_match("/api/*/stuff"));
331 assert!(!g.is_match("/api/some/stuff"));
332
333 let g = Glob::new(r"*stuff");
334 assert!(g.is_match("some-stuff"));
335 assert!(!g.is_match("not-stuff-but-things"));
336
337 let g = Glob::new(r"\*stuff");
338 assert!(g.is_match("*stuff"));
339 assert!(!g.is_match("some-stuff"));
340 }
341
342 #[test]
343 fn test_glob_replace() {
344 for (transaction, pattern, result, star, double_star, question_mark) in [
345 (
346 "/foo/some/bar/here/store",
347 "/foo/*/bar/**",
348 "/foo/*/bar/here/store",
349 true,
350 false,
351 false,
352 ),
353 (
354 "/foo/some/bar/here/store",
355 "/foo/*/bar/*/**",
356 "/foo/*/bar/*/store",
357 true,
358 false,
359 false,
360 ),
361 (
362 "/foo/some/bar/here/store/1234",
363 "/foo/*/bar/*/**",
364 "/foo/*/bar/*/*",
365 true,
366 true,
367 false,
368 ),
369 ("/foo/1/", "/foo/?/**", "/foo/*/", false, false, true),
370 ("/foo/1/end", "/foo/*/**", "/foo/*/end", true, false, true),
371 (
372 "/foo/1/this/and/that/end",
373 "/foo/**/end",
374 "/foo/*/end",
375 false,
376 true,
377 false,
378 ),
379 ] {
380 let g = Glob::builder(pattern)
381 .capture_star(star)
382 .capture_double_star(double_star)
383 .capture_question_mark(question_mark)
384 .build();
385
386 assert_eq!(g.replace_captures(transaction, "*"), result);
387 }
388 }
389
390 #[test]
391 fn test_do_not_replace() {
392 let g = Glob::builder(r"/foo/\*/*")
393 .capture_star(true)
394 .capture_double_star(false)
395 .capture_question_mark(false)
396 .build();
397
398 assert_eq!(g.replace_captures("/foo/*/bar", "_"), "/foo/*/_");
400
401 assert_eq!(g.replace_captures("/foo/nope/bar", "_"), "/foo/nope/bar");
403 }
404
405 #[test]
406 fn test_glob_matcher() {
407 #[derive(Clone, Copy, Debug, PartialEq)]
408 enum Paths {
409 Root,
410 Store,
411 }
412 let mut matcher = GlobMatcher::new();
413 matcher.add("/api/*/store/", Paths::Store);
414 matcher.add("/api/0/", Paths::Root);
415
416 assert_eq!(matcher.test("/api/42/store/"), Some(Paths::Store));
417 assert_eq!(matcher.test("/api/0/"), Some(Paths::Root));
418 assert_eq!(matcher.test("/api/0/"), Some(Paths::Root));
419
420 assert_eq!(matcher.matches("/api/0/"), Some((vec![], Paths::Root)));
421 assert_eq!(
422 matcher.matches("/api/42/store/"),
423 Some((vec!["42"], Paths::Store))
424 );
425 }
426}