relative_path_utils/glob/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use core::fmt;
5use core::mem;
6
7use alloc::borrow::ToOwned;
8use alloc::boxed::Box;
9use alloc::collections::VecDeque;
10use alloc::vec::Vec;
11
12use std::io;
13
14use relative_path::{RelativePath, RelativePathBuf};
15
16use crate::Root;
17
18type Result<T> = std::result::Result<T, Error>;
19
20#[derive(Debug)]
21pub struct Error {
22    kind: ErrorKind,
23}
24
25impl Error {
26    fn new(kind: ErrorKind) -> Self {
27        Self { kind }
28    }
29}
30
31impl fmt::Display for Error {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match &self.kind {
34            ErrorKind::ReadDir(..) => write!(f, "Error reading directory"),
35            ErrorKind::DirEntry(..) => write!(f, "Error getting directory entry"),
36        }
37    }
38}
39
40impl From<ErrorKind> for Error {
41    #[inline]
42    fn from(kind: ErrorKind) -> Self {
43        Self::new(kind)
44    }
45}
46
47#[derive(Debug)]
48enum ErrorKind {
49    ReadDir(io::Error),
50    DirEntry(io::Error),
51}
52
53impl std::error::Error for Error {
54    #[allow(clippy::match_same_arms)]
55    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
56        match &self.kind {
57            ErrorKind::ReadDir(error) => Some(error),
58            ErrorKind::DirEntry(error) => Some(error),
59        }
60    }
61}
62
63/// A compiled glob expression.
64///
65/// This is returned by [`Root::glob`].
66pub struct Glob<'a> {
67    root: &'a Root,
68    components: Vec<Component<'a>>,
69}
70
71impl<'a> Glob<'a> {
72    /// Construct a new glob pattern.
73    pub(super) fn new(root: &'a Root, pattern: &'a RelativePath) -> Self {
74        let components = compile_pattern(pattern);
75        Self { root, components }
76    }
77
78    /// Construct a new matcher over the compiled glob pattern.
79    ///
80    /// # Examples
81    ///
82    /// ```no_run
83    /// use relative_path_utils::Root;
84    ///
85    /// let root = Root::new("src")?;
86    ///
87    /// let glob = root.glob("**/*.rs");
88    ///
89    /// let mut results = Vec::new();
90    ///
91    /// for e in glob.matcher() {
92    ///     results.push(e?);
93    /// }
94    ///
95    /// results.sort();
96    /// assert_eq!(results, vec!["lib.rs", "main.rs"]);
97    /// # Ok::<_, Box<dyn std::error::Error>>(())
98    /// ```
99    #[must_use]
100    pub fn matcher(&self) -> Matcher<'_> {
101        Matcher {
102            root: self.root,
103            queue: [(RelativePathBuf::new(), self.components.as_ref())]
104                .into_iter()
105                .collect(),
106        }
107    }
108}
109
110impl<'a> Matcher<'a> {
111    /// Perform an expansion in the filesystem.
112    fn expand_filesystem<M>(
113        &mut self,
114        current: &RelativePath,
115        rest: &'a [Component<'a>],
116        mut m: M,
117    ) -> Result<()>
118    where
119        M: FnMut(&str) -> bool,
120    {
121        if let Ok(m) = self.root.metadata(current) {
122            if !m.is_dir() {
123                return Ok(());
124            }
125        } else {
126            return Ok(());
127        }
128
129        for e in self.root.read_dir(current).map_err(ErrorKind::ReadDir)? {
130            let e = e.map_err(ErrorKind::DirEntry)?;
131            let c = e.file_name();
132            let c = c.to_string_lossy();
133
134            if !m(c.as_ref()) {
135                continue;
136            }
137
138            let mut new = current.to_owned();
139            new.push(c.as_ref());
140            self.queue.push_back((new, rest));
141        }
142
143        Ok(())
144    }
145
146    /// Perform star star expansion.
147    fn walk(&mut self, current: &RelativePathBuf, rest: &'a [Component<'a>]) -> Result<()> {
148        self.queue.push_back((current.clone(), rest));
149
150        let mut queue = VecDeque::new();
151        queue.push_back(current.clone());
152
153        while let Some(current) = queue.pop_front() {
154            let Ok(m) = self.root.metadata(&current) else {
155                continue;
156            };
157
158            if !m.is_dir() {
159                continue;
160            }
161
162            for e in self.root.read_dir(&current).map_err(ErrorKind::ReadDir)? {
163                let e = e.map_err(ErrorKind::DirEntry)?;
164                let c = e.file_name();
165                let c = c.to_string_lossy();
166                let next = current.join(c.as_ref());
167                self.queue.push_back((next.clone(), rest));
168                queue.push_back(next);
169            }
170        }
171
172        Ok(())
173    }
174}
175
176/// A matcher that can be iterated over for matched relative path buffers.
177pub struct Matcher<'a> {
178    root: &'a Root,
179    queue: VecDeque<(RelativePathBuf, &'a [Component<'a>])>,
180}
181
182impl Iterator for Matcher<'_> {
183    type Item = Result<RelativePathBuf>;
184
185    fn next(&mut self) -> Option<Self::Item> {
186        'outer: loop {
187            let (mut path, mut components) = self.queue.pop_front()?;
188
189            while let [first, rest @ ..] = components {
190                match first {
191                    Component::ParentDir => {
192                        path = path.join(relative_path::Component::ParentDir);
193                    }
194                    Component::Normal(normal) => {
195                        path = path.join(normal);
196                    }
197                    Component::Fragment(fragment) => {
198                        if let Err(e) =
199                            self.expand_filesystem(&path, rest, |name| fragment.is_match(name))
200                        {
201                            return Some(Err(e));
202                        }
203
204                        continue 'outer;
205                    }
206                    Component::StarStar => {
207                        if let Err(e) = self.walk(&path, rest) {
208                            return Some(Err(e));
209                        }
210
211                        continue 'outer;
212                    }
213                }
214
215                components = rest;
216            }
217
218            return Some(Ok(path));
219        }
220    }
221}
222
223#[derive(Clone)]
224enum Component<'a> {
225    /// Parent directory.
226    ParentDir,
227    /// A normal component.
228    Normal(&'a str),
229    /// Normal component, compiled into a fragment.
230    Fragment(Fragment<'a>),
231    /// `**` component, which keeps expanding.
232    StarStar,
233}
234
235fn compile_pattern(pattern: &RelativePath) -> Vec<Component<'_>> {
236    let mut output = Vec::new();
237
238    for c in pattern.components() {
239        output.push(match c {
240            relative_path::Component::CurDir => continue,
241            relative_path::Component::ParentDir => Component::ParentDir,
242            relative_path::Component::Normal("**") => Component::StarStar,
243            relative_path::Component::Normal(normal) => {
244                let fragment = Fragment::parse(normal);
245
246                if let Some(normal) = fragment.as_literal() {
247                    Component::Normal(normal)
248                } else {
249                    Component::Fragment(fragment)
250                }
251            }
252        });
253    }
254
255    output
256}
257
258#[derive(Debug, Clone, Copy)]
259enum Part<'a> {
260    Star,
261    Literal(&'a str),
262}
263
264/// A match fragment.
265#[derive(Debug, Clone)]
266pub(crate) struct Fragment<'a> {
267    parts: Box<[Part<'a>]>,
268}
269
270impl<'a> Fragment<'a> {
271    pub(crate) fn parse(string: &'a str) -> Fragment<'a> {
272        let mut literal = true;
273        let mut parts = Vec::new();
274        let mut start = None;
275
276        for (n, c) in string.char_indices() {
277            if c == '*' {
278                if let Some(s) = start.take() {
279                    parts.push(Part::Literal(&string[s..n]));
280                }
281
282                if mem::take(&mut literal) {
283                    parts.push(Part::Star);
284                }
285            } else {
286                if start.is_none() {
287                    start = Some(n);
288                }
289
290                literal = true;
291            }
292        }
293
294        if let Some(s) = start {
295            parts.push(Part::Literal(&string[s..]));
296        }
297
298        Fragment {
299            parts: parts.into(),
300        }
301    }
302
303    /// Test if the given string matches the current fragment.
304    pub(crate) fn is_match(&self, string: &str) -> bool {
305        let mut backtrack = VecDeque::new();
306        backtrack.push_back((self.parts.as_ref(), string));
307
308        while let Some((mut parts, mut string)) = backtrack.pop_front() {
309            while let Some(part) = parts.first() {
310                match part {
311                    Part::Star => {
312                        // Peek the next literal component. If we have a
313                        // trailing wildcard (which this constitutes) then it
314                        // is by definition a match.
315                        let Some(Part::Literal(peek)) = parts.get(1) else {
316                            return true;
317                        };
318
319                        let Some(peek) = peek.chars().next() else {
320                            return true;
321                        };
322
323                        while let Some(c) = string.chars().next() {
324                            if c == peek {
325                                backtrack.push_front((
326                                    parts,
327                                    string.get(c.len_utf8()..).unwrap_or_default(),
328                                ));
329                                break;
330                            }
331
332                            string = string.get(c.len_utf8()..).unwrap_or_default();
333                        }
334                    }
335                    Part::Literal(literal) => {
336                        // The literal component must be an exact prefix of the
337                        // current string.
338                        let Some(remainder) = string.strip_prefix(literal) else {
339                            return false;
340                        };
341
342                        string = remainder;
343                    }
344                }
345
346                parts = parts.get(1..).unwrap_or_default();
347            }
348
349            if string.is_empty() {
350                return true;
351            }
352        }
353
354        false
355    }
356
357    /// Treat the fragment as a single normal component.
358    fn as_literal(&self) -> Option<&'a str> {
359        if let [Part::Literal(one)] = self.parts.as_ref() {
360            Some(one)
361        } else {
362            None
363        }
364    }
365}