gix_pathspec/search/
matching.rs1use bstr::{BStr, ByteSlice};
2use gix_glob::pattern::Case;
3
4use crate::{
5 MagicSignature, Search, SearchMode,
6 search::{Match, MatchKind, MatchKind::*, Spec},
7};
8
9impl Search {
10 pub fn pattern_matching_relative_path(
28 &mut self,
29 relative_path: &BStr,
30 is_dir: Option<bool>,
31 attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool,
32 ) -> Option<Match<'_>> {
33 if relative_path.is_empty() {
34 return Some(Match {
35 pattern: &self.match_all,
36 sequence_number: 0,
37 kind: Always,
38 });
39 }
40 let basename_not_important = None;
41 if relative_path
42 .get(..self.common_prefix_len)
43 .is_none_or(|rela_path_prefix| rela_path_prefix != self.common_prefix())
44 {
45 return None;
46 }
47
48 let is_dir = is_dir.unwrap_or(false);
49 let patterns_len = self.patterns.len();
50 let res = self.patterns.iter_mut().find_map(|mapping| {
51 let ignore_case = mapping.value.pattern.signature.contains(MagicSignature::ICASE);
52 let prefix = mapping.value.pattern.prefix_directory();
53 if ignore_case && !prefix.is_empty() {
54 let pattern_requirement_is_met = relative_path.get(prefix.len()).map_or_else(|| is_dir, |b| *b == b'/');
55 if !pattern_requirement_is_met
56 || relative_path.get(..prefix.len()).map(ByteSlice::as_bstr) != Some(prefix)
57 {
58 return None;
59 }
60 }
61
62 let case = if ignore_case { Case::Fold } else { Case::Sensitive };
63 let mut is_match = mapping.value.pattern.always_matches();
64 let mut how = Always;
65 if !is_match {
66 is_match = if mapping.pattern.first_wildcard_pos.is_none() {
67 match_verbatim(mapping, relative_path, is_dir, case, &mut how)
68 } else {
69 let wildmatch_mode = match mapping.value.pattern.search_mode {
70 SearchMode::ShellGlob => Some(gix_glob::wildmatch::Mode::empty()),
71 SearchMode::Literal => None,
72 SearchMode::PathAwareGlob => Some(gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL),
73 };
74 match wildmatch_mode {
75 Some(wildmatch_mode) => {
76 let is_match = mapping.pattern.matches_repo_relative_path(
77 relative_path,
78 basename_not_important,
79 Some(is_dir),
80 case,
81 wildmatch_mode,
82 );
83 if !is_match {
84 match_verbatim(mapping, relative_path, is_dir, case, &mut how)
85 } else {
86 how = mapping.pattern.first_wildcard_pos.map_or(Verbatim, |_| WildcardMatch);
87 true
88 }
89 }
90 None => match_verbatim(mapping, relative_path, is_dir, case, &mut how),
91 }
92 }
93 }
94
95 if let Some(attrs) = mapping.value.attrs_match.as_mut() {
96 if !attributes(relative_path, Case::Sensitive, is_dir, attrs) {
97 return None;
99 }
100 for (actual, expected) in attrs.iter_selected().zip(mapping.value.pattern.attributes.iter()) {
101 if actual.assignment != expected.as_ref() {
102 return None;
103 }
104 }
105 }
106
107 is_match.then_some(Match {
108 pattern: &mapping.value.pattern,
109 sequence_number: mapping.sequence_number,
110 kind: how,
111 })
112 });
113
114 if res.is_none() && self.all_patterns_are_excluded {
115 Some(Match {
116 pattern: &self.match_all,
117 sequence_number: patterns_len,
118 kind: Always,
119 })
120 } else {
121 res
122 }
123 }
124
125 pub fn can_match_relative_path(&self, relative_path: &BStr, is_dir: Option<bool>) -> bool {
134 if self.patterns.is_empty() || relative_path.is_empty() {
135 return true;
136 }
137 let common_prefix_len = self.common_prefix_len.min(relative_path.len());
138 if relative_path
139 .get(..common_prefix_len)
140 .is_none_or(|rela_path_prefix| rela_path_prefix != self.common_prefix()[..common_prefix_len])
141 {
142 return false;
143 }
144 for mapping in &self.patterns {
145 let pattern = &mapping.value.pattern;
146 if mapping.pattern.first_wildcard_pos == Some(0) && !pattern.is_excluded() {
147 return true;
148 }
149 let max_usable_pattern_len = mapping.pattern.first_wildcard_pos.unwrap_or_else(|| pattern.path.len());
150 let common_len = max_usable_pattern_len.min(relative_path.len());
151
152 let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
153 let mut is_match = pattern.always_matches();
154 if !is_match && common_len != 0 {
155 let pattern_path = pattern.path[..common_len].as_bstr();
156 let longest_possible_relative_path = &relative_path[..common_len];
157 is_match = if ignore_case {
158 pattern_path.eq_ignore_ascii_case(longest_possible_relative_path)
159 } else {
160 pattern_path == longest_possible_relative_path
161 };
162
163 if is_match {
164 is_match = if common_len < max_usable_pattern_len {
165 pattern.path.get(common_len) == Some(&b'/')
166 } else if relative_path.len() > max_usable_pattern_len
167 && mapping.pattern.first_wildcard_pos.is_none()
168 {
169 relative_path.get(common_len) == Some(&b'/')
170 } else {
171 is_match
172 };
173 if let Some(is_dir) = is_dir.filter(|_| pattern.signature.contains(MagicSignature::MUST_BE_DIR)) {
174 is_match = if is_dir {
175 matches!(pattern.path.get(common_len), None | Some(&b'/'))
176 } else {
177 relative_path.get(common_len) == Some(&b'/')
178 };
179 }
180 }
181 }
182 if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
183 return !pattern.is_excluded();
184 }
185 }
186
187 self.all_patterns_are_excluded
188 }
189
190 pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {
196 if self.patterns.is_empty() || relative_path.is_empty() {
197 return true;
198 }
199 let common_prefix_len = self.common_prefix_len.min(relative_path.len());
200 if relative_path
201 .get(..common_prefix_len)
202 .is_none_or(|rela_path_prefix| rela_path_prefix != self.common_prefix()[..common_prefix_len])
203 {
204 return false;
205 }
206 for mapping in &self.patterns {
207 let pattern = &mapping.value.pattern;
208 if mapping.pattern.first_wildcard_pos.is_some() && pattern.is_excluded() {
209 return true;
210 }
211 let mut rightmost_idx = mapping.pattern.first_wildcard_pos.map_or_else(
212 || pattern.path.len(),
213 |idx| pattern.path[..idx].rfind_byte(b'/').unwrap_or(idx),
214 );
215 let ignore_case = pattern.signature.contains(MagicSignature::ICASE);
216 let mut is_match = pattern.always_matches();
217 if !is_match {
218 let plen = relative_path.len();
219 if leading && rightmost_idx > plen {
220 if let Some(idx) = pattern.path[..plen]
221 .rfind_byte(b'/')
222 .or_else(|| pattern.path[plen..].find_byte(b'/').map(|idx| idx + plen))
223 {
224 rightmost_idx = idx;
225 }
226 }
227 if let Some(relative_path) = relative_path.get(..rightmost_idx) {
228 let pattern_path = pattern.path[..rightmost_idx].as_bstr();
229 is_match = if ignore_case {
230 pattern_path.eq_ignore_ascii_case(relative_path)
231 } else {
232 pattern_path == relative_path
233 };
234 }
235 }
236 if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
237 return !pattern.is_excluded();
238 }
239 }
240
241 self.all_patterns_are_excluded
242 }
243}
244
245fn match_verbatim(
246 mapping: &gix_glob::search::pattern::Mapping<Spec>,
247 relative_path: &BStr,
248 is_dir: bool,
249 case: Case,
250 how: &mut MatchKind,
251) -> bool {
252 let pattern_len = mapping.value.pattern.path.len();
253 let mut relative_path_ends_with_slash_at_pattern_len = false;
254 let (match_is_allowed, probably_how) = relative_path.get(pattern_len).map_or_else(
255 || (relative_path.len() == pattern_len, Verbatim),
256 |b| {
257 relative_path_ends_with_slash_at_pattern_len = *b == b'/';
258 (relative_path_ends_with_slash_at_pattern_len, Prefix)
259 },
260 );
261 *how = probably_how;
262 let pattern_requirement_is_met = !mapping.pattern.mode.contains(gix_glob::pattern::Mode::MUST_BE_DIR)
263 || (relative_path_ends_with_slash_at_pattern_len || is_dir);
264
265 if match_is_allowed && pattern_requirement_is_met {
266 let dir_or_file = &relative_path[..mapping.value.pattern.path.len()];
267 match case {
268 Case::Sensitive => mapping.value.pattern.path == dir_or_file,
269 Case::Fold => mapping.value.pattern.path.eq_ignore_ascii_case(dir_or_file),
270 }
271 } else {
272 false
273 }
274}