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 fn glob_matches_str(pattern: &str, path: &str) -> bool {
40 let p = pattern.replace('\\', "/");
41 glob_match_tokens(&split_glob(&p), &split_glob(path))
42}
43
44pub fn source_tree_path_matches_filter(item_path: &str, filter_path: &str) -> bool {
45 let item_path = normalize_path(item_path);
46 let filter_path = normalize_path(filter_path);
47 let filter_path = filter_path.trim_end_matches('/');
48 if filter_path.is_empty() || filter_path == "." {
49 return true;
50 }
51 item_path == filter_path
52 || item_path
53 .strip_prefix(filter_path)
54 .map(|suffix| suffix.starts_with('/'))
55 .unwrap_or(false)
56 || (source_tree_scope_has_wildcard(&item_path) && glob_matches_str(&item_path, filter_path))
57}
58
59pub fn source_tree_path_is_ignored(path: impl AsRef<Path>, patterns: &[String]) -> bool {
60 let path = path.as_ref();
61 let normalized = normalize_path(path);
62 patterns.iter().any(|pattern| {
63 glob_matches(pattern, path)
64 || pattern
65 .strip_suffix("/**")
66 .map(|prefix| {
67 let prefix = normalize_path(prefix);
68 normalized == prefix || normalized.starts_with(&format!("{prefix}/"))
69 })
70 .unwrap_or(false)
71 })
72}
73
74pub fn source_tree_scope_has_wildcard(scope: &str) -> bool {
75 scope.chars().any(|ch| matches!(ch, '*' | '?'))
76}
77
78pub fn allow_entry_broad_scope(entry: &AllowEntry) -> Option<String> {
79 entry
80 .path
81 .as_ref()
82 .map(normalize_path)
83 .filter(|scope| source_tree_scope_has_wildcard(scope))
84 .or_else(|| {
85 entry
86 .glob
87 .as_deref()
88 .map(normalize_source_tree_scope)
89 .filter(|scope| source_tree_scope_has_wildcard(scope))
90 })
91 .or_else(|| {
92 entry
93 .selector
94 .glob
95 .as_deref()
96 .map(normalize_source_tree_scope)
97 .filter(|scope| source_tree_scope_has_wildcard(scope))
98 })
99}
100
101fn split_glob(s: &str) -> Vec<&str> {
102 s.split('/').filter(|part| !part.is_empty()).collect()
103}
104
105fn glob_match_tokens(pattern: &[&str], path: &[&str]) -> bool {
106 let Some((pattern_head, pattern_tail)) = pattern.split_first() else {
107 return path.is_empty();
108 };
109 if *pattern_head == "**" {
110 if glob_match_tokens(pattern_tail, path) {
111 return true;
112 }
113 return path
114 .split_first()
115 .is_some_and(|(_, path_tail)| glob_match_tokens(pattern, path_tail));
116 }
117 path.split_first().is_some_and(|(path_head, path_tail)| {
118 segment_matches(pattern_head, path_head) && glob_match_tokens(pattern_tail, path_tail)
119 })
120}
121
122fn segment_matches(pattern: &str, text: &str) -> bool {
123 let pattern = pattern.chars().collect::<Vec<_>>();
124 let text = text.chars().collect::<Vec<_>>();
125 segment_match_chars(&pattern, &text)
126}
127
128fn segment_match_chars(pattern: &[char], text: &[char]) -> bool {
129 let Some((&pattern_head, pattern_tail)) = pattern.split_first() else {
130 return text.is_empty();
131 };
132 match pattern_head {
133 '*' => {
134 segment_match_chars(pattern_tail, text)
135 || text
136 .split_first()
137 .is_some_and(|(_, text_tail)| segment_match_chars(pattern, text_tail))
138 }
139 '?' => text
140 .split_first()
141 .is_some_and(|(_, text_tail)| segment_match_chars(pattern_tail, text_tail)),
142 ch => text.split_first().is_some_and(|(&text_head, text_tail)| {
143 ch == text_head && segment_match_chars(pattern_tail, text_tail)
144 }),
145 }
146}