allow_core/
source_tree_path.rs1use std::path::Path;
2
3use crate::AllowEntry;
4
5pub fn normalize_path(path: impl AsRef<Path>) -> String {
6 let text = path.as_ref().to_string_lossy().replace('\\', "/");
7 let absolute = text.starts_with('/');
8 let mut parts = Vec::new();
9 for part in text.split('/') {
10 match part {
11 "" | "." => {}
12 ".." => {
13 if parts.last().is_some_and(|part| *part != "..") {
14 parts.pop();
15 } else if !absolute {
16 parts.push(part);
17 }
18 }
19 other => parts.push(other),
20 }
21 }
22 let normalized = parts.join("/");
23 if absolute {
24 format!("/{normalized}")
25 } else {
26 normalized
27 }
28}
29
30pub(crate) fn normalize_source_tree_scope(scope: &str) -> String {
31 scope.replace('\\', "/")
32}
33
34pub fn glob_matches(pattern: &str, path: &Path) -> bool {
35 let path = normalize_path(path);
36 glob_matches_str(pattern, &path)
37}
38
39pub const GLOB_MATCH_MAX_STEPS: u32 = 10_000;
45
46pub fn glob_matches_str(pattern: &str, path: &str) -> bool {
47 let p = pattern.replace('\\', "/");
48 let mut steps = 0;
49 glob_match_tokens(&split_glob(&p), &split_glob(path), &mut steps)
50}
51
52pub fn source_tree_path_matches_filter(item_path: &str, filter_path: &str) -> bool {
53 let item_path = normalize_path(item_path);
54 let filter_path = normalize_path(filter_path);
55 let filter_path = filter_path.trim_end_matches('/');
56 if filter_path.is_empty() || filter_path == "." {
57 return true;
58 }
59 item_path == filter_path
60 || item_path
61 .strip_prefix(filter_path)
62 .map(|suffix| suffix.starts_with('/'))
63 .unwrap_or(false)
64 || (source_tree_scope_has_wildcard(&item_path) && glob_matches_str(&item_path, filter_path))
65}
66
67pub fn source_tree_path_is_ignored(path: impl AsRef<Path>, patterns: &[String]) -> bool {
68 let path = path.as_ref();
69 let normalized = normalize_path(path);
70 patterns.iter().any(|pattern| {
71 glob_matches(pattern, path)
72 || pattern
73 .strip_suffix("/**")
74 .map(|prefix| {
75 let prefix = normalize_path(prefix);
76 normalized == prefix || normalized.starts_with(&format!("{prefix}/"))
77 })
78 .unwrap_or(false)
79 })
80}
81
82pub fn source_tree_scope_has_wildcard(scope: &str) -> bool {
83 scope.chars().any(|ch| matches!(ch, '*' | '?'))
84}
85
86pub fn allow_entry_broad_scope(entry: &AllowEntry) -> Option<String> {
87 entry
88 .path
89 .as_ref()
90 .map(normalize_path)
91 .filter(|scope| source_tree_scope_has_wildcard(scope))
92 .or_else(|| {
93 entry
94 .glob
95 .as_deref()
96 .map(normalize_source_tree_scope)
97 .filter(|scope| source_tree_scope_has_wildcard(scope))
98 })
99 .or_else(|| {
100 entry
101 .selector
102 .glob
103 .as_deref()
104 .map(normalize_source_tree_scope)
105 .filter(|scope| source_tree_scope_has_wildcard(scope))
106 })
107}
108
109fn split_glob(s: &str) -> Vec<&str> {
110 s.split('/').filter(|part| !part.is_empty()).collect()
111}
112
113fn take_glob_step(steps: &mut u32) -> bool {
114 if *steps >= GLOB_MATCH_MAX_STEPS {
115 return false;
116 }
117 *steps = steps.saturating_add(1);
118 true
119}
120
121fn glob_match_tokens(pattern: &[&str], path: &[&str], steps: &mut u32) -> bool {
122 if !take_glob_step(steps) {
123 return false;
124 }
125 let Some((pattern_head, pattern_tail)) = pattern.split_first() else {
126 return path.is_empty();
127 };
128 if *pattern_head == "**" {
129 if glob_match_tokens(pattern_tail, path, steps) {
130 return true;
131 }
132 return path
133 .split_first()
134 .is_some_and(|(_, path_tail)| glob_match_tokens(pattern, path_tail, steps));
135 }
136 path.split_first().is_some_and(|(path_head, path_tail)| {
137 segment_matches(pattern_head, path_head, steps)
138 && glob_match_tokens(pattern_tail, path_tail, steps)
139 })
140}
141
142fn segment_matches(pattern: &str, text: &str, steps: &mut u32) -> bool {
143 let pattern = pattern.chars().collect::<Vec<_>>();
144 let text = text.chars().collect::<Vec<_>>();
145 segment_match_chars(&pattern, &text, steps)
146}
147
148fn segment_match_chars(pattern: &[char], text: &[char], steps: &mut u32) -> bool {
149 if !take_glob_step(steps) {
150 return false;
151 }
152 let Some((&pattern_head, pattern_tail)) = pattern.split_first() else {
153 return text.is_empty();
154 };
155 match pattern_head {
156 '*' => {
157 segment_match_chars(pattern_tail, text, steps)
158 || text
159 .split_first()
160 .is_some_and(|(_, text_tail)| segment_match_chars(pattern, text_tail, steps))
161 }
162 '?' => text
163 .split_first()
164 .is_some_and(|(_, text_tail)| segment_match_chars(pattern_tail, text_tail, steps)),
165 ch => text.split_first().is_some_and(|(&text_head, text_tail)| {
166 ch == text_head && segment_match_chars(pattern_tail, text_tail, steps)
167 }),
168 }
169}