1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum GlobParseError {
9    #[error("No wildcard found: {src:?}")]
10    NoWildcard { src: String },
11    #[error("Multiple wildcards found: {src:?}")]
12    MultipleWildcards { src: String },
13    #[error("'**' appeared without '*': {src:?}")]
14    StrayRecursiveWildcard { src: String },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct GlobPattern {
20    branches: Vec<GlobBranch>,
21}
22
23impl GlobPattern {
24    pub fn new(src: &str) -> Self {
26        src.parse().unwrap()
27    }
28
29    pub fn do_match<'a>(&self, file_name: &'a str) -> Vec<&'a str> {
31        let mut matches = Vec::new();
32        for branch in &self.branches {
33            if let Some(m) = branch.do_match(file_name) {
34                matches.push(m);
35            }
36        }
37        matches
38    }
39
40    pub fn subst(&self, stem: &str) -> Vec<String> {
42        self.branches
43            .iter()
44            .filter_map(|branch| branch.subst(stem))
45            .collect::<Vec<_>>()
46    }
47
48    pub fn prefixes(&self) -> Vec<String> {
50        self.branches
51            .iter()
52            .map(|branch| branch.prefix.clone())
53            .collect::<Vec<_>>()
54    }
55}
56
57impl FromStr for GlobPattern {
58    type Err = GlobParseError;
59    fn from_str(src: &str) -> Result<Self, Self::Err> {
60        let branches = src
61            .split(",")
62            .map(|branch| branch.parse::<GlobBranch>())
63            .collect::<Result<Vec<_>, _>>()?;
64        Ok(Self { branches })
65    }
66}
67
68impl fmt::Display for GlobPattern {
69    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70        for (i, branch) in self.branches.iter().enumerate() {
71            if i > 0 {
72                f.write_str(",")?;
73            }
74            write!(f, "{}", branch)?;
75        }
76        Ok(())
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81struct GlobBranch {
82    prefix: String,
83    wildcard: Wildcard,
84    suffix: String,
85}
86
87impl GlobBranch {
88    fn do_match<'a>(&self, file_name: &'a str) -> Option<&'a str> {
89        if file_name.starts_with(&self.prefix) && file_name.ends_with(&self.suffix) {
90            let stem = &file_name[self.prefix.len()..file_name.len() - self.suffix.len()];
91            if self.wildcard == Wildcard::Recursive || !stem.contains('/') {
92                return Some(stem);
93            }
94        }
95        None
96    }
97
98    fn subst(&self, stem: &str) -> Option<String> {
99        if self.wildcard == Wildcard::Recursive || !stem.contains('/') {
100            Some(format!("{}{}{}", self.prefix, stem, self.suffix))
101        } else {
102            None
103        }
104    }
105}
106
107impl FromStr for GlobBranch {
108    type Err = GlobParseError;
109    fn from_str(src: &str) -> Result<Self, Self::Err> {
110        let pos = src.find('*').ok_or_else(|| GlobParseError::NoWildcard {
111            src: src.to_owned(),
112        })?;
113        let (pos2, glob_type) = if src[pos..].starts_with("**/*") {
114            (pos + 4, Wildcard::Recursive)
115        } else if src[pos..].starts_with("**") {
116            return Err(GlobParseError::StrayRecursiveWildcard {
117                src: src.to_owned(),
118            });
119        } else {
120            (pos + 1, Wildcard::Single)
121        };
122        let suffix = &src[pos2..];
123        if suffix.contains('*') {
124            return Err(GlobParseError::MultipleWildcards {
125                src: src.to_owned(),
126            });
127        }
128        Ok(Self {
129            prefix: src[..pos].to_owned(),
130            wildcard: glob_type,
131            suffix: suffix.to_owned(),
132        })
133    }
134}
135
136impl fmt::Display for GlobBranch {
137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138        write!(f, "{}{}{}", self.prefix, self.wildcard, self.suffix)
139    }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143enum Wildcard {
144    Recursive,
146    Single,
148}
149
150impl fmt::Display for Wildcard {
151    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152        use Wildcard::*;
153        match self {
154            Recursive => f.write_str("**/*"),
155            Single => f.write_str("*"),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_parse() {
166        assert_eq!(
167            GlobPattern::new("tests/fixtures/**/*-in.txt"),
168            GlobPattern {
169                branches: vec![GlobBranch {
170                    prefix: "tests/fixtures/".to_owned(),
171                    wildcard: Wildcard::Recursive,
172                    suffix: "-in.txt".to_owned(),
173                }]
174            }
175        );
176
177        assert_eq!(
178            GlobPattern::new("tests/fixtures/*-out.txt"),
179            GlobPattern {
180                branches: vec![GlobBranch {
181                    prefix: "tests/fixtures/".to_owned(),
182                    wildcard: Wildcard::Single,
183                    suffix: "-out.txt".to_owned(),
184                }]
185            }
186        );
187
188        assert_eq!(
189            GlobPattern::new("foo/*.txt,bar/*.txt"),
190            GlobPattern {
191                branches: vec![
192                    GlobBranch {
193                        prefix: "foo/".to_owned(),
194                        wildcard: Wildcard::Single,
195                        suffix: ".txt".to_owned(),
196                    },
197                    GlobBranch {
198                        prefix: "bar/".to_owned(),
199                        wildcard: Wildcard::Single,
200                        suffix: ".txt".to_owned(),
201                    }
202                ]
203            }
204        );
205    }
206
207    #[test]
208    fn test_parse_error() {
209        let e = "tests/fixtures/in.txt".parse::<GlobPattern>().unwrap_err();
210        assert_eq!(
211            e.to_string(),
212            "No wildcard found: \"tests/fixtures/in.txt\""
213        );
214
215        let e = "tests/fixtures/*/*/in.txt"
216            .parse::<GlobPattern>()
217            .unwrap_err();
218        assert_eq!(
219            e.to_string(),
220            "Multiple wildcards found: \"tests/fixtures/*/*/in.txt\""
221        );
222
223        let e = "tests/fixtures/**/in.txt"
224            .parse::<GlobPattern>()
225            .unwrap_err();
226        assert_eq!(
227            e.to_string(),
228            "'**' appeared without '*': \"tests/fixtures/**/in.txt\""
229        );
230    }
231
232    #[test]
233    fn test_stringify() {
234        let cases = [
235            "tests/fixtures/**/*-in.txt",
236            "tests/fixtures/*-out.txt",
237            "*.rs",
238            "tests/fixtures/**/*",
239            "foo/*.txt,bar/*.rs",
240        ];
241        for &case in &cases {
242            assert_eq!(GlobPattern::new(case).to_string(), case);
243        }
244    }
245
246    #[test]
247    fn test_match() {
248        let empty: Vec<&str> = vec![];
249
250        let pat = GlobPattern::new("tests/fixtures/**/*-in.txt");
251        assert_eq!(pat.do_match("tests/fixtures/foo-in.txt"), vec!["foo"]);
252        assert_eq!(
253            pat.do_match("tests/fixtures/foo/bar-in.txt"),
254            vec!["foo/bar"]
255        );
256        assert_eq!(pat.do_match("tests/fixtures/foo-out.txt"), empty);
257
258        let pat = GlobPattern::new("tests/fixtures/*-in.txt");
259        assert_eq!(pat.do_match("tests/fixtures/foo-in.txt"), vec!["foo"]);
260        assert_eq!(pat.do_match("tests/fixtures/foo/bar-in.txt"), empty);
261        assert_eq!(pat.do_match("tests/fixtures/foo-out.txt"), empty);
262
263        let pat = GlobPattern::new("foo/**/*.txt,foo/bar/**/*.txt");
264        assert_eq!(pat.do_match("foo/a.txt"), vec!["a"]);
265        assert_eq!(pat.do_match("foo/bar/a.txt"), vec!["bar/a", "a"]);
266    }
267
268    #[test]
269    fn test_subst() {
270        let empty: Vec<&str> = vec![];
271
272        let pat = GlobPattern::new("tests/fixtures/**/*-in.txt");
273        assert_eq!(
274            pat.subst("foo"),
275            vec!["tests/fixtures/foo-in.txt".to_owned()]
276        );
277        assert_eq!(
278            pat.subst("foo/bar"),
279            vec!["tests/fixtures/foo/bar-in.txt".to_owned()]
280        );
281
282        let pat = GlobPattern::new("tests/fixtures/*-in.txt");
283        assert_eq!(
284            pat.subst("foo"),
285            vec!["tests/fixtures/foo-in.txt".to_owned()]
286        );
287        assert_eq!(pat.subst("foo/bar"), empty);
288
289        let pat = GlobPattern::new("foo/*.txt,bar/*.rs");
290        assert_eq!(
291            pat.subst("a"),
292            vec!["foo/a.txt".to_owned(), "bar/a.rs".to_owned()]
293        );
294        assert_eq!(pat.subst("foo/bar"), empty);
295    }
296}