relay_common/
glob2.rs

1//! Serializable glob patterns for the API.
2
3use std::sync::OnceLock;
4use std::{fmt, str};
5
6use regex_lite::Regex;
7
8/// Glob options represent the underlying regex emulating the globs.
9#[derive(Debug)]
10struct GlobPatternGroups<'g> {
11    star: &'g str,
12    double_star: &'g str,
13    question_mark: &'g str,
14}
15
16/// `GlobBuilder` provides the posibility to fine tune the final [`Glob`], mainly what capture
17/// groups will be enabled in the underlying regex.
18#[derive(Debug)]
19pub struct GlobBuilder<'g> {
20    value: &'g str,
21    groups: GlobPatternGroups<'g>,
22}
23
24impl<'g> GlobBuilder<'g> {
25    /// Create a new builder with all the captures enabled by default.
26    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    /// Enable capture groups for `*` in the pattern.
39    pub fn capture_star(mut self, enable: bool) -> Self {
40        if !enable {
41            self.groups.star = "(?:[^/]*?)";
42        }
43        self
44    }
45
46    /// Enable capture groups for `**` in the pattern.
47    pub fn capture_double_star(mut self, enable: bool) -> Self {
48        if !enable {
49            self.groups.double_star = "(?:.*?)";
50        }
51        self
52    }
53
54    /// Enable capture groups for `?` in the pattern.
55    pub fn capture_question_mark(mut self, enable: bool) -> Self {
56        if !enable {
57            self.groups.question_mark = "(?:.)";
58        }
59        self
60    }
61
62    /// Create a new [`Glob`] from this builder.
63    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(&regex_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(&regex_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/// A simple glob matcher.
93///
94/// Supported are `?` for a single char, `*` for all but a slash and
95/// `**` to match with slashes.
96#[derive(Clone)]
97pub struct Glob {
98    value: String,
99    pattern: Regex,
100}
101
102impl Glob {
103    /// Creates the [`GlobBuilder`], which can be fine-tunned using helper methods.
104    pub fn builder(glob: &'_ str) -> GlobBuilder {
105        GlobBuilder::new(glob)
106    }
107
108    /// Creates a new glob from a string.
109    ///
110    /// All the glob patterns (wildcards) are enabled in the captures, and can be returned by
111    /// `matches` function.
112    pub fn new(glob: &str) -> Glob {
113        GlobBuilder::new(glob).build()
114    }
115
116    /// Returns the pattern as str.
117    pub fn pattern(&self) -> &str {
118        &self.value
119    }
120
121    /// Checks if some value matches the glob.
122    pub fn is_match(&self, value: &str) -> bool {
123        self.pattern.is_match(value)
124    }
125
126    /// Currently support replacing only all `*` in the input string with provided replacement.
127    /// If no match is found, then a copy of the string is returned unchanged.
128    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            // Create the iter on subcaptures and ignore the first capture, since this is always
134            // the entire string.
135            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    /// Checks if the value matches and returns the wildcard matches.
147    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/// Helper for glob matching
200#[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    /// Initializes an empty matcher
213    pub fn new() -> GlobMatcher<T> {
214        GlobMatcher { globs: vec![] }
215    }
216
217    /// Adds a new glob to the matcher
218    pub fn add<G: Into<Glob>>(&mut self, glob: G, ident: T) {
219        self.globs.push((glob.into(), ident));
220    }
221
222    /// Matches a string against the stored globs.
223    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    /// Matches a string against the stored glob and get the matches.
233    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/// Wrapper type around the raw string pattern and the [`Glob`].
244///
245/// This allows to compile the Glob with internal regexes only then whent they are used.
246#[derive(Clone, Eq, PartialEq)]
247pub struct LazyGlob {
248    raw: String,
249    glob: OnceLock<Glob>,
250}
251
252impl LazyGlob {
253    /// Create a new [`LazyGlob`] from the raw string.
254    pub fn new(raw: impl Into<String>) -> Self {
255        Self {
256            raw: raw.into(),
257            glob: OnceLock::new(),
258        }
259    }
260
261    /// Returns the compiled version of the [`Glob`].
262    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    /// Returns the glob pattern as string.
273    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        // A literal asterisk matches
399        assert_eq!(g.replace_captures("/foo/*/bar", "_"), "/foo/*/_");
400
401        // But only a literal asterisk
402        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}