gix_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 = "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 /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root.
74 /// Note that `path` is assumed to be relative to the repository.
75 ///
76 /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play.
77 /// `basename_start_pos` is the index at which the `path`'s basename starts.
78 ///
79 /// `case` folding can be configured as well.
80 /// `mode` is used to control how [`crate::wildmatch()`] should operate.
81 pub fn matches_repo_relative_path(
82 &self,
83 path: &BStr,
84 basename_start_pos: Option<usize>,
85 is_dir: Option<bool>,
86 case: Case,
87 mode: wildmatch::Mode,
88 ) -> bool {
89 let is_dir = is_dir.unwrap_or(false);
90 if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
91 return false;
92 }
93
94 let flags = mode
95 | match case {
96 Case::Fold => wildmatch::Mode::IGNORE_CASE,
97 Case::Sensitive => wildmatch::Mode::empty(),
98 };
99 #[cfg(debug_assertions)]
100 {
101 if basename_start_pos.is_some() {
102 debug_assert_eq!(
103 basename_start_pos,
104 path.rfind_byte(b'/').map(|p| p + 1),
105 "BUG: invalid cached basename_start_pos provided"
106 );
107 }
108 }
109 debug_assert!(!path.starts_with(b"/"), "input path must be relative");
110
111 if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
112 let basename = &path[basename_start_pos.unwrap_or_default()..];
113 self.matches(basename, flags)
114 } else {
115 self.matches(path, flags)
116 }
117 }
118
119 /// See if `value` matches this pattern in the given `mode`.
120 ///
121 /// `mode` can identify `value` as path which won't match the slash character, and can match
122 /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.
123 ///
124 /// Note that this method uses some shortcuts to accelerate simple patterns, but falls back to
125 /// [wildmatch()][crate::wildmatch()] if these fail.
126 pub fn matches(&self, value: &BStr, mode: wildmatch::Mode) -> bool {
127 match self.first_wildcard_pos {
128 // "*literal" case, overrides starts-with
129 Some(pos)
130 if self.mode.contains(pattern::Mode::ENDS_WITH)
131 && (!mode.contains(wildmatch::Mode::NO_MATCH_SLASH_LITERAL) || !value.contains(&b'/')) =>
132 {
133 let text = &self.text[pos + 1..];
134 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
135 value
136 .len()
137 .checked_sub(text.len())
138 .is_some_and(|start| text.eq_ignore_ascii_case(&value[start..]))
139 } else {
140 value.ends_with(text.as_ref())
141 }
142 }
143 Some(pos) => {
144 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
145 if !value
146 .get(..pos)
147 .is_some_and(|value| value.eq_ignore_ascii_case(&self.text[..pos]))
148 {
149 return false;
150 }
151 } else if !value.starts_with(&self.text[..pos]) {
152 return false;
153 }
154 crate::wildmatch(self.text.as_bstr(), value, mode)
155 }
156 None => {
157 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
158 self.text.eq_ignore_ascii_case(value)
159 } else {
160 self.text == value
161 }
162 }
163 }
164 }
165}
166
167impl fmt::Display for Pattern {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 if self.mode.contains(Mode::NEGATIVE) {
170 "!".fmt(f)?;
171 }
172 if self.mode.contains(Mode::ABSOLUTE) {
173 "/".fmt(f)?;
174 }
175 self.text.fmt(f)?;
176 if self.mode.contains(Mode::MUST_BE_DIR) {
177 "/".fmt(f)?;
178 }
179 Ok(())
180 }
181}