allow_core/
source_tree_path.rs1use std::path::Path;
2
3use crate::AllowEntry;
4
5pub fn normalize_path(path: impl AsRef<Path>) -> String {
44 let text = path.as_ref().to_string_lossy().replace('\\', "/");
45 use unicode_normalization::UnicodeNormalization;
49 let nfc: String = text.nfc().collect();
50 let (stripped, force_absolute) = strip_verbatim_prefix(&nfc);
53 let absolute = force_absolute || stripped.starts_with('/');
54 let mut parts = Vec::new();
55 for part in stripped.split('/') {
56 match part {
57 "" | "." => {}
58 ".." => {
59 if parts.last().is_some_and(|part| *part != "..") {
60 parts.pop();
61 } else if !absolute {
62 parts.push(part);
63 }
64 }
65 other => parts.push(other),
66 }
67 }
68 let normalized = parts.join("/");
69 if absolute {
70 format!("/{normalized}")
71 } else {
72 normalized
73 }
74}
75
76fn strip_verbatim_prefix(text: &str) -> (&str, bool) {
86 if let Some(rest) = text.strip_prefix("//?/UNC/") {
87 return (rest, true);
88 }
89 if let Some(rest) = text.strip_prefix("//?/") {
90 return (rest, false);
91 }
92 (text, false)
93}
94
95pub(crate) fn normalize_source_tree_scope(scope: &str) -> String {
96 scope.replace('\\', "/")
97}
98
99pub fn glob_matches(pattern: &str, path: &Path) -> bool {
100 let path = normalize_path(path);
101 glob_matches_str(pattern, &path)
102}
103
104pub const GLOB_MATCH_MAX_STEPS: u32 = 10_000;
110
111pub fn glob_matches_str(pattern: &str, path: &str) -> bool {
112 let p = pattern.replace('\\', "/");
113 let mut steps = 0;
114 glob_match_tokens(&split_glob(&p), &split_glob(path), &mut steps)
115}
116
117pub fn source_tree_path_matches_filter(item_path: &str, filter_path: &str) -> bool {
118 let item_path = normalize_path(item_path);
119 let filter_path = normalize_path(filter_path);
120 let filter_path = filter_path.trim_end_matches('/');
121 if filter_path.is_empty() || filter_path == "." {
122 return true;
123 }
124 item_path == filter_path
125 || item_path
126 .strip_prefix(filter_path)
127 .map(|suffix| suffix.starts_with('/'))
128 .unwrap_or(false)
129 || (source_tree_scope_has_wildcard(filter_path)
133 && glob_matches_str(filter_path, &item_path))
134 || (source_tree_scope_has_wildcard(&item_path)
135 && glob_matches_str(&item_path, filter_path))
136}
137
138pub fn source_tree_path_is_ignored(path: impl AsRef<Path>, patterns: &[String]) -> bool {
139 let path = path.as_ref();
140 let normalized = normalize_path(path);
141 patterns.iter().any(|pattern| {
142 glob_matches(pattern, path)
143 || pattern
144 .strip_suffix("/**")
145 .map(|prefix| {
146 let prefix = normalize_path(prefix);
147 normalized == prefix || normalized.starts_with(&format!("{prefix}/"))
148 })
149 .unwrap_or(false)
150 })
151}
152
153pub fn source_tree_scope_has_wildcard(scope: &str) -> bool {
154 scope.chars().any(|ch| matches!(ch, '*' | '?'))
155}
156
157pub fn allow_entry_broad_scope(entry: &AllowEntry) -> Option<String> {
158 entry
159 .path
160 .as_ref()
161 .map(normalize_path)
162 .filter(|scope| source_tree_scope_has_wildcard(scope))
163 .or_else(|| {
164 entry
165 .glob
166 .as_deref()
167 .map(normalize_source_tree_scope)
168 .filter(|scope| source_tree_scope_has_wildcard(scope))
169 })
170 .or_else(|| {
171 entry
172 .selector
173 .glob
174 .as_deref()
175 .map(normalize_source_tree_scope)
176 .filter(|scope| source_tree_scope_has_wildcard(scope))
177 })
178}
179
180pub fn strip_win32_verbatim_prefix(path: &str) -> String {
185 let normalized = path.replace('\\', "/");
187 if let Some(rest) = normalized.strip_prefix("//?/UNC/") {
188 format!("/{rest}")
189 } else if let Some(rest) = normalized.strip_prefix("//?/") {
190 rest.to_string()
191 } else {
192 path.to_string()
193 }
194}
195
196fn split_glob(s: &str) -> Vec<&str> {
197 s.split('/').filter(|part| !part.is_empty()).collect()
198}
199
200fn take_glob_step(steps: &mut u32) -> bool {
201 if *steps >= GLOB_MATCH_MAX_STEPS {
202 return false;
203 }
204 *steps = steps.saturating_add(1);
205 true
206}
207
208fn glob_match_tokens(pattern: &[&str], path: &[&str], steps: &mut u32) -> bool {
209 if !take_glob_step(steps) {
210 return false;
211 }
212 let Some((pattern_head, pattern_tail)) = pattern.split_first() else {
213 return path.is_empty();
214 };
215 if *pattern_head == "**" {
216 if glob_match_tokens(pattern_tail, path, steps) {
217 return true;
218 }
219 return path
220 .split_first()
221 .is_some_and(|(_, path_tail)| glob_match_tokens(pattern, path_tail, steps));
222 }
223 path.split_first().is_some_and(|(path_head, path_tail)| {
224 segment_matches(pattern_head, path_head, steps)
225 && glob_match_tokens(pattern_tail, path_tail, steps)
226 })
227}
228
229fn segment_matches(pattern: &str, text: &str, steps: &mut u32) -> bool {
230 let pattern = pattern.chars().collect::<Vec<_>>();
231 let text = text.chars().collect::<Vec<_>>();
232 segment_match_chars(&pattern, &text, steps)
233}
234
235fn segment_match_chars(pattern: &[char], text: &[char], steps: &mut u32) -> bool {
236 if !take_glob_step(steps) {
237 return false;
238 }
239 let Some((&pattern_head, pattern_tail)) = pattern.split_first() else {
240 return text.is_empty();
241 };
242 match pattern_head {
243 '*' => {
244 segment_match_chars(pattern_tail, text, steps)
245 || text
246 .split_first()
247 .is_some_and(|(_, text_tail)| segment_match_chars(pattern, text_tail, steps))
248 }
249 '?' => text
250 .split_first()
251 .is_some_and(|(_, text_tail)| segment_match_chars(pattern_tail, text_tail, steps)),
252 ch => text.split_first().is_some_and(|(&text_head, text_tail)| {
253 ch == text_head && segment_match_chars(pattern_tail, text_tail, steps)
254 }),
255 }
256}