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
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}