Skip to main content

gix_glob/
pattern.rs

1use std::fmt;
2
3use bitflags::bitflags;
4use bstr::{BStr, ByteSlice};
5
6use crate::{Pattern, pattern, wildmatch};
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 = "serde", derive(serde::Serialize, serde::Deserialize))]
16    #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
17    pub struct Mode: u32 {
18        /// The pattern does not contain a sub-directory and - it doesn't contain slashes after removing the trailing one.
19        const NO_SUB_DIR = 1 << 0;
20        /// A pattern that is '*literal', meaning that it ends with what's given here
21        const ENDS_WITH = 1 << 1;
22        /// The pattern must match a directory, and not a file.
23        const MUST_BE_DIR = 1 << 2;
24        /// The pattern matches, but should be negated. Note that this mode has to be checked and applied by the caller.
25        const NEGATIVE = 1 << 3;
26        /// The pattern starts with a slash and thus matches only from the beginning.
27        const ABSOLUTE = 1 << 4;
28    }
29}
30
31/// Describes whether to match a path case sensitively or not.
32///
33/// Used in [`Pattern::matches_repo_relative_path()`].
34#[derive(Default, Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)]
35pub enum Case {
36    /// The case affects the match
37    #[default]
38    Sensitive,
39    /// Ignore the case of ascii characters.
40    Fold,
41}
42
43/// Instantiation
44impl Pattern {
45    /// Parse the given `text` as pattern, or return `None` if `text` was empty.
46    pub fn from_bytes(text: &[u8]) -> Option<Self> {
47        crate::parse::pattern(text, true).map(|(text, mode, first_wildcard_pos)| Pattern {
48            text: text.into(),
49            mode,
50            first_wildcard_pos,
51        })
52    }
53
54    /// Parse the given `text` as pattern without supporting leading `!` or `\\!` , or return `None` if `text` was empty.
55    ///
56    /// This assures that `text` remains entirely unaltered, but removes built-in support for negation as well.
57    pub fn from_bytes_without_negation(text: &[u8]) -> Option<Self> {
58        crate::parse::pattern(text, false).map(|(text, mode, first_wildcard_pos)| Pattern {
59            text: text.into(),
60            mode,
61            first_wildcard_pos,
62        })
63    }
64}
65
66/// Access
67impl Pattern {
68    /// Return true if a match is negated.
69    pub fn is_negative(&self) -> bool {
70        self.mode.contains(Mode::NEGATIVE)
71    }
72
73    /// Return true if `text` contains `*`, `?`, or `[`, which classify it as a wildcard pattern.
74    ///
75    /// Use it to know if [matches](Self::matches()) must be used, or if equality under some definition is sufficient.
76    ///
77    /// These bytes count even when escaped. Unlike [`Self::first_wildcard_pos`], this does not consider
78    /// `\` alone sufficient: an escape ends the literal prefix but does not itself classify the pattern as a wildcard.
79    pub fn has_wildcard(&self) -> bool {
80        self.text.find_byteset(b"*?[").is_some()
81    }
82
83    /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root.
84    /// Note that `path` is assumed to be relative to the repository.
85    ///
86    /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play.
87    /// `basename_start_pos` is the index at which the `path`'s basename starts.
88    ///
89    /// `case` folding can be configured as well.
90    /// `mode` is used to control how [`crate::wildmatch()`] should operate.
91    pub fn matches_repo_relative_path(
92        &self,
93        path: &BStr,
94        basename_start_pos: Option<usize>,
95        is_dir: Option<bool>,
96        case: Case,
97        mode: wildmatch::Mode,
98    ) -> bool {
99        let is_dir = is_dir.unwrap_or(false);
100        if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
101            return false;
102        }
103
104        let flags = mode
105            | match case {
106                Case::Fold => wildmatch::Mode::IGNORE_CASE,
107                Case::Sensitive => wildmatch::Mode::empty(),
108            };
109        #[cfg(debug_assertions)]
110        {
111            if basename_start_pos.is_some() {
112                debug_assert_eq!(
113                    basename_start_pos,
114                    path.rfind_byte(b'/').map(|p| p + 1),
115                    "BUG: invalid cached basename_start_pos provided"
116                );
117            }
118        }
119        debug_assert!(!path.starts_with(b"/"), "input path must be relative");
120
121        if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
122            let basename = &path[basename_start_pos.unwrap_or_default()..];
123            self.matches(basename, flags)
124        } else {
125            self.matches(path, flags)
126        }
127    }
128
129    /// See if `value` matches this pattern in the given `mode`.
130    ///
131    /// `mode` can identify `value` as path which won't match the slash character, and can match
132    /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.
133    ///
134    /// Note that this method uses some shortcuts to accelerate simple patterns, but falls back to
135    /// [wildmatch()][crate::wildmatch()] if these fail.
136    pub fn matches(&self, value: &BStr, mode: wildmatch::Mode) -> bool {
137        match self.first_wildcard_pos {
138            // "*literal" case, overrides starts-with
139            Some(pos)
140                if self.mode.contains(pattern::Mode::ENDS_WITH)
141                    && (!mode.contains(wildmatch::Mode::NO_MATCH_SLASH_LITERAL) || !value.contains(&b'/')) =>
142            {
143                let text = &self.text[pos + 1..];
144                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
145                    value
146                        .len()
147                        .checked_sub(text.len())
148                        .is_some_and(|start| text.eq_ignore_ascii_case(&value[start..]))
149                } else {
150                    value.ends_with(text.as_ref())
151                }
152            }
153            Some(pos) => {
154                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
155                    if !value
156                        .get(..pos)
157                        .is_some_and(|value| value.eq_ignore_ascii_case(&self.text[..pos]))
158                    {
159                        return false;
160                    }
161                } else if !value.starts_with(&self.text[..pos]) {
162                    return false;
163                }
164                crate::wildmatch(self.text.as_bstr(), value, mode)
165            }
166            None => {
167                if mode.contains(wildmatch::Mode::IGNORE_CASE) {
168                    self.text.eq_ignore_ascii_case(value)
169                } else {
170                    self.text == value
171                }
172            }
173        }
174    }
175}
176
177impl fmt::Display for Pattern {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        if self.mode.contains(Mode::NEGATIVE) {
180            "!".fmt(f)?;
181        }
182        if self.mode.contains(Mode::ABSOLUTE) {
183            "/".fmt(f)?;
184        }
185        self.text.fmt(f)?;
186        if self.mode.contains(Mode::MUST_BE_DIR) {
187            "/".fmt(f)?;
188        }
189        Ok(())
190    }
191}