pathpatterns/
match_list.rs

1//! Helpers for include/exclude lists.
2
3use bitflags::bitflags;
4
5use crate::PatternFlag;
6
7#[rustfmt::skip]
8bitflags! {
9    /// These flags influence what kind of paths should be matched.
10    pub struct MatchFlag: u16 {
11        /// Match only a complete entry. The pattern `bar` will not match `/foo/bar`.
12        const ANCHORED            = 0x00_01;
13
14        const MATCH_DIRECTORIES   = 0x01_00;
15        const MATCH_REGULAR_FILES = 0x02_00;
16        const MATCH_SYMLINKS      = 0x04_00;
17        const MATCH_SOCKETS       = 0x08_00;
18        const MATCH_FIFOS         = 0x10_00;
19        const MATCH_CHARDEVS      = 0x20_00;
20        const MATCH_BLOCKDEVS     = 0x40_00;
21        const MATCH_DEVICES =
22            MatchFlag::MATCH_CHARDEVS.bits() | MatchFlag::MATCH_BLOCKDEVS.bits();
23
24        /// This is the default.
25        const ANY_FILE_TYPE =
26              MatchFlag::MATCH_DIRECTORIES.bits()
27            | MatchFlag::MATCH_REGULAR_FILES.bits()
28            | MatchFlag::MATCH_SYMLINKS.bits()
29            | MatchFlag::MATCH_SOCKETS.bits()
30            | MatchFlag::MATCH_FIFOS.bits()
31            | MatchFlag::MATCH_CHARDEVS.bits()
32            | MatchFlag::MATCH_BLOCKDEVS.bits();
33    }
34}
35
36impl Default for MatchFlag {
37    fn default() -> Self {
38        Self::ANY_FILE_TYPE
39    }
40}
41
42/// A pattern entry. For now this only contains glob patterns, but we may want to add regex
43/// patterns or user defined callback functions later on as well.
44///
45/// For regex we'd likely use the POSIX extended REs via `regexec(3)`, since we're targetting
46/// command line interfaces and want something command line users are used to.
47#[derive(Clone, Debug)]
48pub enum MatchPattern {
49    /// A glob pattern.
50    Pattern(crate::Pattern),
51
52    /// A literal match.
53    Literal(Vec<u8>),
54}
55
56impl From<crate::Pattern> for MatchPattern {
57    fn from(pattern: crate::Pattern) -> Self {
58        MatchPattern::Pattern(pattern)
59    }
60}
61
62impl MatchPattern {
63    pub fn literal(literal: impl Into<Vec<u8>>) -> Self {
64        MatchPattern::Literal(literal.into())
65    }
66}
67
68/// A pattern can be used as an include or an exclude pattern. In a list of `MatchEntry`s, later
69/// patterns take precedence over earlier patterns and the order of includes vs excludes makes a
70/// difference.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum MatchType {
73    Include,
74    Exclude,
75}
76
77/// Convenience helpers
78impl MatchType {
79    pub fn is_include(self) -> bool {
80        self == MatchType::Include
81    }
82
83    pub fn is_exclude(self) -> bool {
84        self == MatchType::Exclude
85    }
86}
87
88impl std::ops::Not for MatchType {
89    type Output = MatchType;
90
91    fn not(self) -> Self::Output {
92        match self {
93            MatchType::Include => MatchType::Exclude,
94            MatchType::Exclude => MatchType::Include,
95        }
96    }
97}
98
99/// A single entry in a `MatchList`.
100#[derive(Clone, Debug)]
101pub struct MatchEntry {
102    pattern: MatchPattern,
103    ty: MatchType,
104    flags: MatchFlag,
105}
106
107impl MatchEntry {
108    /// Create a new match entry.
109    pub fn new<T: Into<MatchPattern>>(pattern: T, ty: MatchType) -> Self {
110        Self {
111            pattern: pattern.into(),
112            ty,
113            flags: MatchFlag::default(),
114        }
115    }
116
117    /// Create a new include-type match entry with default flags.
118    pub fn include<T: Into<MatchPattern>>(pattern: T) -> Self {
119        Self::new(pattern.into(), MatchType::Include)
120    }
121
122    /// Create a new exclude-type match entry with default flags.
123    pub fn exclude<T: Into<MatchPattern>>(pattern: T) -> Self {
124        Self::new(pattern.into(), MatchType::Exclude)
125    }
126
127    /// Builder method to set the match flags to a specific value.
128    pub fn flags(mut self, flags: MatchFlag) -> Self {
129        self.flags = flags;
130        self
131    }
132
133    /// Builder method to add flag bits to the already present ones.
134    pub fn add_flags(mut self, flags: MatchFlag) -> Self {
135        self.flags.insert(flags);
136        self
137    }
138
139    /// Builder method to remove match flag bits.
140    pub fn remove_flags(mut self, flags: MatchFlag) -> Self {
141        self.flags.remove(flags);
142        self
143    }
144
145    /// Builder method to toggle flag bits.
146    pub fn toggle_flags(mut self, flags: MatchFlag) -> Self {
147        self.flags.toggle(flags);
148        self
149    }
150
151    #[inline]
152    pub fn match_type(&self) -> MatchType {
153        self.ty
154    }
155
156    /// Non-Builder method to change the match type.
157    pub fn match_type_mut(&mut self) -> &mut MatchType {
158        &mut self.ty
159    }
160
161    /// Directly access the pattern.
162    pub fn pattern(&self) -> &MatchPattern {
163        &self.pattern
164    }
165
166    /// Non-Builder method to change the pattern.
167    pub fn pattern_mut(&mut self) -> &mut MatchPattern {
168        &mut self.pattern
169    }
170
171    /// Directly access the match flags.
172    pub fn match_flags(&self) -> MatchFlag {
173        self.flags
174    }
175
176    /// Non-Builder method to change the flags.
177    pub fn match_flags_mut(&mut self) -> &mut MatchFlag {
178        &mut self.flags
179    }
180
181    /// Parse a pattern into a `MatchEntry` while interpreting a leading exclamation mark as
182    /// inversion and trailing slashes to match only directories.
183    pub fn parse_pattern<T: AsRef<[u8]>>(
184        pattern: T,
185        pattern_flags: PatternFlag,
186        ty: MatchType,
187    ) -> Result<Self, crate::ParseError> {
188        Self::parse_pattern_do(pattern.as_ref(), pattern_flags, ty)
189    }
190
191    fn parse_pattern_do(
192        pattern: &[u8],
193        pattern_flags: PatternFlag,
194        ty: MatchType,
195    ) -> Result<Self, crate::ParseError> {
196        let (pattern, ty) = if pattern.get(0).copied() == Some(b'!') {
197            (&pattern[1..], !ty)
198        } else {
199            (pattern, ty)
200        };
201
202        let (pattern, flags) = match pattern.iter().rposition(|&b| b != b'/') {
203            Some(pos) if (pos + 1) == pattern.len() => (pattern, MatchFlag::default()),
204            Some(pos) => (&pattern[..=pos], MatchFlag::MATCH_DIRECTORIES),
205            None => (b"/".as_ref(), MatchFlag::MATCH_DIRECTORIES),
206        };
207
208        Ok(Self::new(crate::Pattern::new(pattern, pattern_flags)?, ty).flags(flags))
209    }
210
211    /// Test this entry's file type restrictions against a file mode retrieved from `stat()`.
212    pub fn matches_mode(&self, file_mode: u32) -> bool {
213        // bitflags' `.contains` means ALL bits must be set, if they are all set we don't
214        // need to check the mode...
215        if self.flags.contains(MatchFlag::ANY_FILE_TYPE) {
216            return true;
217        }
218
219        let flag = match file_mode & libc::S_IFMT {
220            libc::S_IFDIR => MatchFlag::MATCH_DIRECTORIES,
221            libc::S_IFREG => MatchFlag::MATCH_REGULAR_FILES,
222            libc::S_IFLNK => MatchFlag::MATCH_SYMLINKS,
223            libc::S_IFSOCK => MatchFlag::MATCH_SOCKETS,
224            libc::S_IFIFO => MatchFlag::MATCH_FIFOS,
225            libc::S_IFCHR => MatchFlag::MATCH_CHARDEVS,
226            libc::S_IFBLK => MatchFlag::MATCH_BLOCKDEVS,
227            _unknown => return false,
228        };
229        self.flags.intersects(flag)
230    }
231
232    /// Test whether this entry's pattern matches any complete suffix of a path.
233    ///
234    /// For the path `/foo/bar/baz`, this tests whether `baz`, `bar/baz` or `foo/bar/baz` is
235    /// matched.
236    pub fn matches_path_suffix<T: AsRef<[u8]>>(&self, path: T) -> bool {
237        self.matches_path_suffix_do(path.as_ref())
238    }
239
240    fn matches_path_suffix_do(&self, path: &[u8]) -> bool {
241        if self.flags.intersects(MatchFlag::ANCHORED) {
242            return self.matches_path_exact(path);
243        }
244
245        if path.is_empty() {
246            return false;
247        }
248
249        for start in (0..path.len()).rev() {
250            if path[start] == b'/' && self.matches_path_exact(&path[(start + 1)..]) {
251                return true;
252            }
253        }
254
255        // and try the whole string as well:
256        self.matches_path_exact(path)
257    }
258
259    /// Test whether this entry's pattern matches a path exactly.
260    pub fn matches_path_exact<T: AsRef<[u8]>>(&self, path: T) -> bool {
261        self.matches_path_exact_do(path.as_ref())
262    }
263
264    fn matches_path_exact_do(&self, path: &[u8]) -> bool {
265        match &self.pattern {
266            MatchPattern::Pattern(pattern) => pattern.matches(path),
267            MatchPattern::Literal(literal) => path == &literal[..],
268        }
269    }
270
271    /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
272    /// This is a combination of using `.matches_mode()` and `.matches_path_suffix()`.
273    pub fn matches<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
274        self.matches_do(path.as_ref(), file_mode)
275    }
276
277    fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
278        if let Some(mode) = file_mode {
279            if !self.matches_mode(mode) {
280                return false;
281            }
282        }
283
284        self.matches_path_suffix(path)
285    }
286
287    /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
288    /// This is a combination of using `.matches_mode()` and `.matches_path_exact()`.
289    pub fn matches_exact<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
290        self.matches_exact_do(path.as_ref(), file_mode)
291    }
292
293    fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
294        if let Some(mode) = file_mode {
295            if !self.matches_mode(mode) {
296                return false;
297            }
298        }
299
300        self.matches_path_exact(path)
301    }
302}
303
304#[doc(hidden)]
305pub trait MatchListEntry {
306    fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
307    fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
308}
309
310impl MatchListEntry for &'_ MatchEntry {
311    fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
312        if self.matches(path, file_mode) {
313            Some(self.match_type())
314        } else {
315            None
316        }
317    }
318
319    fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
320        if self.matches_exact(path, file_mode) {
321            Some(self.match_type())
322        } else {
323            None
324        }
325    }
326}
327
328impl MatchListEntry for &'_ &'_ MatchEntry {
329    fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
330        if self.matches(path, file_mode) {
331            Some(self.match_type())
332        } else {
333            None
334        }
335    }
336
337    fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
338        if self.matches_exact(path, file_mode) {
339            Some(self.match_type())
340        } else {
341            None
342        }
343    }
344}
345
346/// This provides `matches` and `matches_exact` methods to lists of `MatchEntry`s.
347///
348/// Technically this is implemented for anything you can turn into a `DoubleEndedIterator` over
349/// `MatchEntry` or `&MatchEntry`.
350///
351/// In practice this means you can use it with slices or references to `Vec` or `VecDeque` etc.
352/// This makes it easier to use slices over entries or references to entries.
353pub trait MatchList {
354    /// Check whether this list contains anything matching a prefix of the specified path, and the
355    /// specified file mode.
356    fn matches<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> Option<MatchType> {
357        self.matches_do(path.as_ref(), file_mode)
358    }
359
360    fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
361
362    /// Check whether this list contains anything exactly matching the path and mode.
363    fn matches_exact<T: AsRef<[u8]>>(
364        &self,
365        path: T,
366        file_mode: Option<u32>,
367    ) -> Option<MatchType> {
368        self.matches_exact_do(path.as_ref(), file_mode)
369    }
370
371    fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
372}
373
374impl<'a, T> MatchList for T
375where
376    T: 'a + ?Sized,
377    &'a T: IntoIterator,
378    <&'a T as IntoIterator>::IntoIter: DoubleEndedIterator,
379    <&'a T as IntoIterator>::Item: MatchListEntry,
380{
381    fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
382        // This is an &self method on a `T where T: 'a`.
383        let this: &'a Self = unsafe { std::mem::transmute(self) };
384
385        for m in this.into_iter().rev() {
386            if let Some(mt) = m.entry_matches(path, file_mode) {
387                return Some(mt);
388            }
389        }
390
391        None
392    }
393
394    fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
395        // This is an &self method on a `T where T: 'a`.
396        let this: &'a Self = unsafe { std::mem::transmute(self) };
397
398        for m in this.into_iter().rev() {
399            if let Some(mt) = m.entry_matches_exact(path, file_mode) {
400                return Some(mt);
401            }
402        }
403
404        None
405    }
406}
407
408#[test]
409fn assert_containers_implement_match_list() {
410    use std::iter::FromIterator;
411
412    let vec = vec![MatchEntry::include(crate::Pattern::path("a*").unwrap())];
413    assert_eq!(vec.matches("asdf", None), Some(MatchType::Include));
414
415    // FIXME: ideally we can make this work as well!
416    let vd = std::collections::VecDeque::<MatchEntry>::from_iter(vec.clone());
417    assert_eq!(vd.matches("asdf", None), Some(MatchType::Include));
418
419    let list: &[MatchEntry] = &vec[..];
420    assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
421
422    let list: Vec<&MatchEntry> = vec.iter().collect();
423    assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
424
425    let list: &[&MatchEntry] = &list[..];
426    assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
427}
428
429#[test]
430fn test_file_type_matches() {
431    let matchlist = vec![
432        MatchEntry::parse_pattern("a_dir/", PatternFlag::PATH_NAME, MatchType::Include)
433            .unwrap(),
434        MatchEntry::parse_pattern("!a_file", PatternFlag::PATH_NAME, MatchType::Include)
435            .unwrap()
436            .flags(MatchFlag::MATCH_REGULAR_FILES),
437        MatchEntry::parse_pattern("!another_dir//", PatternFlag::PATH_NAME, MatchType::Include)
438            .unwrap(),
439    ];
440    assert_eq!(
441        matchlist.matches("a_dir", Some(libc::S_IFDIR)),
442        Some(MatchType::Include)
443    );
444    assert_eq!(
445        matchlist.matches("/a_dir", Some(libc::S_IFDIR)),
446        Some(MatchType::Include)
447    );
448    assert_eq!(matchlist.matches("/a_dir", Some(libc::S_IFREG)), None);
449
450    assert_eq!(
451        matchlist.matches("/a_file", Some(libc::S_IFREG)),
452        Some(MatchType::Exclude)
453    );
454    assert_eq!(matchlist.matches("/a_file", Some(libc::S_IFDIR)), None);
455
456    assert_eq!(
457        matchlist.matches("/another_dir", Some(libc::S_IFDIR)),
458        Some(MatchType::Exclude)
459    );
460    assert_eq!(matchlist.matches("/another_dir", Some(libc::S_IFREG)), None);
461}
462
463#[test]
464fn test_anchored_matches() {
465    use crate::Pattern;
466
467    let matchlist = vec![
468        MatchEntry::new(Pattern::path("file-a").unwrap(), MatchType::Include),
469        MatchEntry::new(Pattern::path("some/path").unwrap(), MatchType::Include)
470            .flags(MatchFlag::ANCHORED),
471    ];
472
473    assert_eq!(matchlist.matches("file-a", None), Some(MatchType::Include));
474    assert_eq!(
475        matchlist.matches("another/file-a", None),
476        Some(MatchType::Include)
477    );
478
479    assert_eq!(matchlist.matches("some", None), None);
480    assert_eq!(matchlist.matches("path", None), None);
481    assert_eq!(
482        matchlist.matches("some/path", None),
483        Some(MatchType::Include)
484    );
485    assert_eq!(matchlist.matches("another/some/path", None), None);
486}
487
488#[test]
489fn test_literal_matches() {
490    let matchlist = vec![
491        MatchEntry::new(MatchPattern::Literal(b"/bin/mv".to_vec()), MatchType::Include),
492    ];
493    assert_eq!(matchlist.matches("/bin/mv", None), Some(MatchType::Include));
494}