Skip to main content

allow_core/
source_tree_path.rs

1use 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
39/// Maximum recursive match steps for one glob evaluation.
40///
41/// Protects against exponential backtracking from pathological patterns such
42/// as many `*` / `**` tokens against long paths (#1924). When the budget is
43/// exhausted the match fails closed (returns `false`) instead of hanging.
44pub 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}