git_glob/
pattern.rs

1use std::fmt;
2
3use bitflags::bitflags;
4use bstr::{BStr, ByteSlice};
5
6use crate::{pattern, wildmatch, Pattern};
7
8bitflags! {
9    /// Information about a [`Pattern`].
10    ///
11    /// Its main purpose is to accelerate pattern matching, or to negate the match result or to
12    /// keep special rules only applicable when matching paths.
13    ///
14    /// The mode is typically created when parsing the pattern by inspecting it and isn't typically handled by the user.
15    #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
16    pub struct Mode: u32 {
17        /// The pattern does not contain a sub-directory and - it doesn't contain slashes after removing the trailing one.
18        const NO_SUB_DIR = 1 << 0;
19        /// A pattern that is '*literal', meaning that it ends with what's given here
20        const ENDS_WITH = 1 << 1;
21        /// The pattern must match a directory, and not a file.
22        const MUST_BE_DIR = 1 << 2;
23        /// The pattern matches, but should be negated. Note that this mode has to be checked and applied by the caller.
24        const NEGATIVE = 1 << 3;
25        /// The pattern starts with a slash and thus matches only from the beginning.
26        const ABSOLUTE = 1 << 4;
27    }
28}
29
30/// Describes whether to match a path case sensitively or not.
31///
32/// Used in [Pattern::matches_repo_relative_path()].
33#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)]
34pub enum Case {
35    /// The case affects the match
36    Sensitive,
37    /// Ignore the case of ascii characters.
38    Fold,
39}
40
41impl Default for Case {
42    fn default() -> Self {
43        Case::Sensitive
44    }
45}
46
47impl Pattern {
48    /// Parse the given `text` as pattern, or return `None` if `text` was empty.
49    pub fn from_bytes(text: &[u8]) -> Option<Self> {
50        crate::parse::pattern(text).map(|(text, mode, first_wildcard_pos)| Pattern {
51            text,
52            mode,
53            first_wildcard_pos,
54        })
55    }
56
57    /// Return true if a match is negated.
58    pub fn is_negative(&self) -> bool {
59        self.mode.contains(Mode::NEGATIVE)
60    }
61
62    /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root.
63    /// Note that `path` is assumed to be relative to the repository.
64    ///
65    /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play.
66    /// `basename_start_pos` is the index at which the `path`'s basename starts.
67    ///
68    /// Lastly, `case` folding can be configured as well.
69    pub fn matches_repo_relative_path<'a>(
70        &self,
71        path: impl Into<&'a BStr>,
72        basename_start_pos: Option<usize>,
73        is_dir: Option<bool>,
74        case: Case,
75    ) -> bool {
76        let is_dir = is_dir.unwrap_or(false);
77        if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
78            return false;
79        }
80
81        let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL
82            | match case {
83                Case::Fold => wildmatch::Mode::IGNORE_CASE,
84                Case::Sensitive => wildmatch::Mode::empty(),
85            };
86        let path = path.into();
87        debug_assert_eq!(
88            basename_start_pos,
89            path.rfind_byte(b'/').map(|p| p + 1),
90            "BUG: invalid cached basename_start_pos provided"
91        );
92        debug_assert!(!path.starts_with(b"/"), "input path must be relative");
93
94        if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
95            let basename = &path[basename_start_pos.unwrap_or_default()..];
96            self.matches(basename, flags)
97        } else {
98            self.matches(path, flags)
99        }
100    }
101
102    /// See if `value` matches this pattern in the given `mode`.
103    ///
104    /// `mode` can identify `value` as path which won't match the slash character, and can match
105    /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.
106    ///
107    /// Note that this method uses some shortcuts to accelerate simple patterns.
108    fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool {
109        let value = value.into();
110        match self.first_wildcard_pos {
111            // "*literal" case, overrides starts-with
112            Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => {
113                let text = &self.text[pos + 1..];
114                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
115                    value
116                        .len()
117                        .checked_sub(text.len())
118                        .map(|start| text.eq_ignore_ascii_case(&value[start..]))
119                        .unwrap_or(false)
120                } else {
121                    value.ends_with(text.as_ref())
122                }
123            }
124            Some(pos) => {
125                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
126                    if !value
127                        .get(..pos)
128                        .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos]))
129                    {
130                        return false;
131                    }
132                } else if !value.starts_with(&self.text[..pos]) {
133                    return false;
134                }
135                crate::wildmatch(self.text.as_bstr(), value, mode)
136            }
137            None => {
138                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
139                    self.text.eq_ignore_ascii_case(value)
140                } else {
141                    self.text == value
142                }
143            }
144        }
145    }
146}
147
148impl fmt::Display for Pattern {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        if self.mode.contains(Mode::NEGATIVE) {
151            "!".fmt(f)?;
152        }
153        if self.mode.contains(Mode::ABSOLUTE) {
154            "/".fmt(f)?;
155        }
156        self.text.fmt(f)?;
157        if self.mode.contains(Mode::MUST_BE_DIR) {
158            "/".fmt(f)?;
159        }
160        Ok(())
161    }
162}