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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Deterministic glob matching without a regular-expression engine.
#[derive(Clone, PartialEq, Eq, Debug)]
enum Part {
Literal(char),
One,
Any,
Class {
negated: bool,
ranges: Vec<(u32, u32)>,
},
}
/// A compiled identifier glob.
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Glob {
parts: Vec<Part>,
}
impl Glob {
/// Compiles `pattern` into deterministic glob components.
///
/// Compilation is `O(P log R)` in the worst case due to normalization of
/// character-class ranges. Unlike the original implementation, Unicode
/// ranges such as `[a-z]` are stored as intervals rather than expanded into
/// one `char` per member.
pub(crate) fn new(pattern: &str) -> Self {
let mut parts = Vec::new();
let mut position = 0usize;
while let Some((character, next)) = character_at(pattern, position) {
match character {
'*' => {
if !matches!(parts.last(), Some(Part::Any)) {
parts.push(Part::Any);
}
position = next;
}
'?' => {
parts.push(Part::One);
position = next;
}
'[' => {
if let Some((part, next)) = class(pattern, position) {
parts.push(part);
position = next;
} else {
parts.push(Part::Literal('['));
position = next;
}
}
'\\' => {
if let Some((escaped, escaped_next)) = character_at(pattern, next) {
parts.push(Part::Literal(escaped));
position = escaped_next;
} else {
parts.push(Part::Literal('\\'));
position = next;
}
}
literal => {
parts.push(Part::Literal(literal));
position = next;
}
}
}
Self { parts }
}
/// Returns whether `text` satisfies this glob.
///
/// Matching uses the classic greedy wildcard algorithm with backtracking to
/// the latest `*`. It performs no heap allocation. Typical runtime is
/// linear in pattern plus text length; worst-case runtime remains
/// `O(P * T)` for adversarial wildcard patterns, while auxiliary space is
/// reduced from `O(T)` to `O(1)`.
pub(crate) fn matches(&self, text: &str) -> bool {
let mut part_position = 0usize;
let mut text_position = 0usize;
let mut star_part = None;
let mut star_text = 0usize;
while text_position < text.len() {
let Some((character, next_text)) = character_at(text, text_position) else {
return false;
};
match self.parts.get(part_position) {
Some(Part::Any) => {
star_part = Some(part_position);
star_text = text_position;
part_position += 1;
}
Some(part) if part_matches(part, character) => {
part_position += 1;
text_position = next_text;
}
_ => {
let Some(star) = star_part else {
return false;
};
let Some((_, next_star_text)) = character_at(text, star_text) else {
return false;
};
star_text = next_star_text;
text_position = next_star_text;
part_position = star + 1;
}
}
}
while matches!(self.parts.get(part_position), Some(Part::Any)) {
part_position += 1;
}
part_position == self.parts.len()
}
}
/// Parses a character class beginning at byte offset `start`.
///
/// Members and ranges are normalized into sorted non-overlapping Unicode scalar
/// intervals. Returns `None` for an unterminated class.
fn class(pattern: &str, start: usize) -> Option<(Part, usize)> {
let mut position = start.checked_add(1)?;
let mut negated = false;
if character_at(pattern, position).is_some_and(|(character, _)| character == '!') {
let (_, next) = character_at(pattern, position)?;
negated = true;
position = next;
}
let mut ranges = Vec::new();
while let Some((character, next)) = character_at(pattern, position) {
if character == ']' && !ranges.is_empty() {
normalize_ranges(&mut ranges);
return Some((Part::Class { negated, ranges }, next));
}
if let Some(('-', after_dash)) = character_at(pattern, next)
&& let Some((end, after_end)) = character_at(pattern, after_dash)
&& end != ']'
{
let start_code = u32::from(character);
let end_code = u32::from(end);
ranges.push((start_code.min(end_code), start_code.max(end_code)));
position = after_end;
continue;
}
let code = u32::from(character);
ranges.push((code, code));
position = next;
}
None
}
/// Sorts and merges overlapping or adjacent character-class intervals.
///
/// Runtime is `O(R log R)` for `R` input ranges and additional space is `O(1)`
/// beyond the existing vector.
fn normalize_ranges(ranges: &mut Vec<(u32, u32)>) {
if ranges.len() < 2 {
return;
}
ranges.sort_unstable();
let mut write = 0usize;
for read in 1..ranges.len() {
let (start, end) = ranges[read];
let current_end = ranges[write].1;
let adjacent_or_overlapping = match current_end.checked_add(1) {
Some(boundary) => start <= boundary,
None => true,
};
if adjacent_or_overlapping {
if end > current_end {
ranges[write].1 = end;
}
} else {
write += 1;
ranges[write] = (start, end);
}
}
ranges.truncate(write + 1);
}
/// Returns the Unicode scalar beginning at byte offset `position` and its end.
///
/// Invalid offsets return `None`; no slicing panic is possible.
#[inline]
fn character_at(text: &str, position: usize) -> Option<(char, usize)> {
let tail = text.get(position..)?;
let character = tail.chars().next()?;
Some((character, position.checked_add(character.len_utf8())?))
}
/// Tests one compiled glob part against a Unicode scalar.
///
/// Character classes use binary search over normalized intervals, so class
/// matching costs `O(log R)` rather than a linear scan over expanded members.
#[inline]
fn part_matches(part: &Part, character: char) -> bool {
match part {
Part::Literal(expected) => *expected == character,
Part::One | Part::Any => true,
Part::Class { negated, ranges } => {
let code = u32::from(character);
let position = ranges.partition_point(|(start, _)| *start <= code);
let contained = position
.checked_sub(1)
.and_then(|index| ranges.get(index))
.is_some_and(|(_, end)| code <= *end);
contained != *negated
}
}
}
#[cfg(test)]
#[path = "glob_tests.rs"]
mod tests;