1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Java's `glob:` `PathMatcher` (`sun.nio.fs.Globs.toUnixRegexPattern`), matched against a path string.
use regex::Regex;
const REGEX_META_CHARS: &str = ".^$+{[]|()";
const GLOB_META_CHARS: &str = "\\*?[{";
fn is_regex_meta(c: char) -> bool {
REGEX_META_CHARS.contains(c)
}
fn is_glob_meta(c: char) -> bool {
GLOB_META_CHARS.contains(c)
}
/// `Globs.toRegexPattern(glob, isDos = false)`; `Err` is the `PatternSyntaxException` message.
pub fn to_unix_regex_pattern(glob: &str) -> Result<String, String> {
let chars: Vec<char> = glob.chars().collect();
let next = |i: usize| chars.get(i).copied();
let mut in_group = false;
let mut regex = String::from("^");
let mut i = 0;
while i < chars.len() {
let c = chars[i];
i += 1;
match c {
'\\' => {
let Some(next) = next(i) else { return Err("No character to escape".to_owned()) };
if is_glob_meta(next) || is_regex_meta(next) {
regex.push('\\');
}
regex.push(next);
i += 1;
}
'/' => regex.push(c),
'[' => {
regex.push_str("[[^/]&&[");
if next(i) == Some('^') {
regex.push_str("\\^");
i += 1;
} else {
if next(i) == Some('!') {
regex.push('^');
i += 1;
}
if next(i) == Some('-') {
regex.push('-');
i += 1;
}
}
let mut has_range_start = false;
let mut last = '\0';
let mut c = '\0';
while i < chars.len() {
c = chars[i];
i += 1;
if c == ']' {
break;
}
if c == '/' {
return Err("Explicit 'name separator' in class".to_owned());
}
if c == '\\' || c == '[' || c == '&' && next(i) == Some('&') {
regex.push('\\');
}
regex.push(c);
if c == '-' {
if !has_range_start {
return Err("Invalid range".to_owned());
}
let end = next(i);
i += 1;
match end {
None | Some(']') => {
c = end.unwrap_or('\0');
break;
}
Some(end) if end < last => return Err("Invalid range".to_owned()),
Some(end) => {
c = end;
regex.push(end);
}
}
has_range_start = false;
} else {
has_range_start = true;
last = c;
}
}
if c != ']' {
return Err("Missing ']".to_owned());
}
regex.push_str("]]");
}
'{' => {
if in_group {
return Err("Cannot nest groups".to_owned());
}
regex.push_str("(?:(?:");
in_group = true;
}
'}' if in_group => {
regex.push_str("))");
in_group = false;
}
',' if in_group => regex.push_str(")|(?:"),
'*' if next(i) == Some('*') => {
regex.push_str(".*");
i += 1;
}
'*' => regex.push_str("[^/]*"),
'?' => regex.push_str("[^/]"),
c => {
if is_regex_meta(c) {
regex.push('\\');
}
regex.push(c);
}
}
}
if in_group {
return Err("Missing '}".to_owned());
}
regex.push('$');
Ok(regex)
}
/// A compiled `glob:` matcher. Java regexes run in DOTALL-less mode, so `.` does not match line breaks.
#[derive(Clone, Debug)]
pub struct PathMatcher {
glob: String,
regex: Regex,
}
impl PathMatcher {
pub fn new(glob: &str, case_insensitive: bool) -> Result<PathMatcher, String> {
let pattern = to_unix_regex_pattern(glob)?;
let pattern = if case_insensitive { format!("(?i){pattern}") } else { pattern };
let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
Ok(PathMatcher { glob: glob.to_owned(), regex })
}
pub fn matches(&self, path: &str) -> bool {
self.regex.is_match(path)
}
/// `toString()` of the JDK's matcher lambda is opaque; the glob is what a trace log can usefully show.
pub fn glob(&self) -> &str {
&self.glob
}
}