1use std::str::FromStr;
4
5pub use fast_glob::Error as GlobError;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct GlobPattern(Vec<u8>);
11
12impl FromStr for GlobPattern {
13 type Err = GlobError;
14
15 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
16 Self::from_bytes(pattern.as_bytes().to_vec())
17 }
18}
19
20impl GlobPattern {
21 fn from_bytes(pattern: Vec<u8>) -> Result<Self, GlobError> {
22 fast_glob::validate(&pattern)?;
23 Ok(Self(pattern))
24 }
25
26 pub fn matches(&self, candidate: impl AsRef<[u8]>) -> bool {
28 fast_glob::glob_match(&self.0, candidate)
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34pub enum KeyGlobError {
35 #[error("empty key-path glob; use '*' to select top-level keys")]
37 Empty,
38 #[error("unclosed quote at byte {index}")]
40 UnclosedQuote {
41 index: usize,
43 },
44 #[error("{0}")]
46 Glob(GlobError),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct KeyGlobPattern(GlobPattern);
58
59const LITERAL_SLASH: u8 = 0xff;
62const LITERAL_BACKSLASH: u8 = 0xfe;
63
64fn encode(byte: u8) -> u8 {
65 match byte {
66 b'/' => LITERAL_SLASH,
67 b'\\' => LITERAL_BACKSLASH,
68 byte => byte,
69 }
70}
71
72impl KeyGlobPattern {
73 pub fn matches_keys(&self, keys: &[String]) -> bool {
76 if keys.is_empty() {
77 return false;
78 }
79 let mut candidate = Vec::new();
80 for (index, key) in keys.iter().enumerate() {
81 if index != 0 {
82 candidate.push(b'/');
83 }
84 candidate.extend(key.bytes().map(encode));
85 }
86 self.0.matches(candidate)
87 }
88}
89
90struct Normalizer<'a> {
91 source: &'a [u8],
92 bytes: Vec<u8>,
93 offsets: Vec<usize>,
94}
95
96impl Normalizer<'_> {
97 fn push(&mut self, byte: u8, offset: usize) {
98 self.bytes.push(byte);
99 self.offsets.push(offset);
100 }
101
102 fn literal(&mut self, byte: u8, offset: usize) {
103 let byte = encode(byte);
104 if matches!(byte, b'*' | b'?' | b'[' | b']' | b'{' | b'}' | b',' | b'!') {
105 self.push(b'\\', offset);
106 }
107 self.push(byte, offset);
108 }
109
110 fn glob_error(&self, kind: fast_glob::ErrorKind, index: usize) -> KeyGlobError {
111 KeyGlobError::Glob(GlobError { kind, index })
112 }
113
114 fn escaped(&self, index: &mut usize) -> Result<u8, KeyGlobError> {
116 let offset = *index;
117 let mut byte = self.source[*index];
118 if byte == b'\\' {
119 *index += 1;
120 byte = *self
121 .source
122 .get(*index)
123 .ok_or_else(|| self.glob_error(fast_glob::ErrorKind::TrailingBackslash, offset))?;
124 byte = match byte {
125 b'b' => 8,
126 b'n' => b'\n',
127 b'r' => b'\r',
128 b't' => b'\t',
129 byte => byte,
130 };
131 }
132 *index += 1;
133 Ok(byte)
134 }
135
136 fn class(&mut self, index: &mut usize) -> Result<(), KeyGlobError> {
137 let start = *index;
138 *index += 1;
139 let negated = matches!(self.source.get(*index), Some(b'!' | b'^'));
140 if negated {
141 *index += 1;
142 }
143 let mut members = [false; 256];
144 let mut first = true;
145 loop {
146 let Some(&byte) = self.source.get(*index) else {
147 return Err(self.glob_error(fast_glob::ErrorKind::UnclosedBracket, start));
148 };
149 if byte == b']' && !first {
150 *index += 1;
151 break;
152 }
153 let low = self.escaped(index)?;
154 let high = if self.source.get(*index) == Some(&b'-')
155 && self.source.get(*index + 1).is_some_and(|b| *b != b']')
156 {
157 *index += 1;
158 self.escaped(index)?
159 } else {
160 low
161 };
162 if low <= high {
163 members[usize::from(low)..=usize::from(high)].fill(true);
164 }
165 first = false;
166 }
167
168 self.push(b'[', start);
169 let mut emitted = false;
170 for original in 0..=0xfd_u8 {
173 if members[usize::from(original)] != negated {
174 let byte = encode(original);
175 if matches!(byte, b']' | b'-' | b'^' | b'!' | b'[') {
176 self.push(b'\\', start);
177 }
178 self.push(byte, start);
179 emitted = true;
180 }
181 }
182 if !emitted {
183 self.push(b'/', start);
185 }
186 self.push(b']', start);
187 Ok(())
188 }
189}
190
191impl FromStr for KeyGlobPattern {
192 type Err = KeyGlobError;
193
194 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
195 if pattern.is_empty() {
196 return Err(KeyGlobError::Empty);
197 }
198 let mut norm = Normalizer {
199 source: pattern.as_bytes(),
200 bytes: Vec::new(),
201 offsets: Vec::new(),
202 };
203 let mut index = 0;
204 while index < norm.source.len() {
205 let offset = index;
206 match norm.source[index] {
207 b'\'' => {
208 index += 1;
209 loop {
210 let Some(&byte) = norm.source.get(index) else {
211 return Err(KeyGlobError::UnclosedQuote { index: offset });
212 };
213 if byte == b'\'' {
214 index += 1;
215 break;
216 }
217 let literal_offset = index;
218 if byte == b'\\' {
219 index += 1;
220 if index == norm.source.len() {
221 return Err(norm.glob_error(
222 fast_glob::ErrorKind::TrailingBackslash,
223 literal_offset,
224 ));
225 }
226 }
227 norm.literal(norm.source[index], literal_offset);
228 index += 1;
229 }
230 }
231 b'[' => norm.class(&mut index)?,
232 b'\\' => {
233 let byte = norm.escaped(&mut index)?;
234 norm.literal(byte, offset);
235 }
236 b'.' => {
237 norm.push(b'/', offset);
238 index += 1;
239 }
240 byte => {
241 norm.push(encode(byte), offset);
242 index += 1;
243 }
244 }
245 }
246 GlobPattern::from_bytes(norm.bytes)
247 .map(Self)
248 .map_err(|mut err| {
249 err.index = norm
250 .offsets
251 .get(err.index)
252 .copied()
253 .unwrap_or(pattern.len());
254 KeyGlobError::Glob(err)
255 })
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn matches(pattern: &str, keys: &[&str]) -> bool {
264 pattern
265 .parse::<KeyGlobPattern>()
266 .unwrap()
267 .matches_keys(&keys.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>())
268 }
269
270 #[test]
271 fn selectors_match_full_paths_and_respect_depth() {
272 for (pattern, keys, expected) in [
273 ("*", vec!["foo"], true),
274 ("*", vec!["foo.bar/baz\\qux"], true),
275 ("*", vec!["foo", "bar"], false),
276 ("foo", vec!["foo"], true),
277 ("foo", vec!["parent", "foo"], false),
278 ("foo.*", vec!["foo", "bar"], true),
279 ("foo.*", vec!["foo", "bar", "baz"], false),
280 ("foo.*", vec!["foo"], false),
281 ("foo.**", vec!["foo", "bar", "baz"], true),
282 ("**.cache", vec!["cache"], true),
283 ("**.cache", vec!["foo", "bar", "cache"], true),
284 ("**.cache", vec!["foo", "cache", "bar"], false),
285 ("{foo,bar}.*", vec!["bar", "baz"], true),
286 ("{foo.bar,baz}.*", vec!["foo", "bar", "qux"], true),
287 ("{foo,bar}.*", vec!["baz", "qux"], false),
288 ("!foo", vec!["foo"], false),
289 ("!foo", vec!["bar"], true),
290 ("!!foo", vec!["foo"], true),
291 ("!foo.*", vec!["foo"], true),
292 ("*", vec![], false),
293 ("''", vec![""], true),
294 ("''", vec![], false),
295 ("foo.''", vec!["foo", ""], true),
296 ] {
297 assert_eq!(matches(pattern, &keys), expected, "{pattern:?}: {keys:?}");
298 }
299 }
300
301 #[test]
302 fn literals_do_not_collide_with_nested_paths_or_glob_syntax() {
303 for (pattern, keys, expected) in [
304 ("'foo.bar'", vec!["foo.bar"], true),
305 ("'foo.bar'", vec!["foo", "bar"], false),
306 ("foo.bar", vec!["foo.bar"], false),
307 ("'foo.bar'.*", vec!["foo.bar", "pool"], true),
308 (r"foo\.bar", vec!["foo.bar"], true),
309 ("foo/bar", vec!["foo/bar"], true),
310 ("foo/bar", vec!["foo", "bar"], false),
311 ("'foo/bar'", vec!["foo/bar"], true),
312 (r"'foo\\bar'", vec![r"foo\bar"], true),
313 (r"foo\\bar", vec![r"foo\bar"], true),
314 (r"'it\'s'", vec!["it's"], true),
315 ("'*'", vec!["*"], true),
316 ("'*'", vec!["anything"], false),
317 ("'!foo'", vec!["!foo"], true),
318 ("'!foo'", vec!["foo"], false),
319 ("'{a,b}[0]?'", vec!["{a,b}[0]?"], true),
320 ("{'foo.bar',baz}.*", vec!["foo.bar", "x"], true),
321 ("foo'bar'.*", vec!["foobar", "x"], true),
322 ("'µ'", vec!["µ"], true),
323 ("?", vec!["µ"], false),
324 ("??", vec!["µ"], true),
325 ("?", vec!["/"], true),
326 ("?", vec!["\\"], true),
327 ("?", vec!["", ""], false),
328 (r"\n", vec!["\n"], true),
329 (r"'\n'", vec!["n"], true),
330 ] {
331 assert_eq!(matches(pattern, &keys), expected, "{pattern:?}: {keys:?}");
332 }
333 }
334
335 #[test]
336 fn classes_preserve_original_byte_membership() {
337 for pattern in [
338 "[a-z]", "[!a-z]", "[^a-z]", "[.]", "[/]", r"[\\]", "[.-0]", r"[Z-\^]", "[]]", "[[]",
339 "[-]", "[z-a]", "[!z-a]", "['.]", r"[\n]",
340 ] {
341 let selector: KeyGlobPattern = pattern.parse().unwrap();
342 for byte in 0..=127_u8 {
345 let reference = pattern.as_bytes().to_vec();
346 if matches!(byte, b'/' | b'\\') {
349 continue;
350 }
351 assert_eq!(
352 selector.matches_keys(&[String::from_utf8(vec![byte]).unwrap()]),
353 fast_glob::glob_match(&reference, [byte]),
354 "{pattern:?} byte {byte}"
355 );
356 }
357 }
358 for (pattern, key, expected) in [
359 ("[/]", "/", true),
360 (r"[\\]", "\\", true),
361 ("[.-0]", "/", true),
362 ("[!.-0]", "/", false),
363 (r"[Z-\^]", "\\", true),
364 ("[!a-z]", "/", true),
365 ("[.]", ".", true),
366 ("[.]", "/", false),
367 ("[z-a]", "z", false),
368 ] {
369 assert_eq!(matches(pattern, &[key]), expected, "{pattern:?}: {key:?}");
370 }
371 assert!(!matches("[!a-z]", &["", ""]));
372 }
373
374 #[test]
375 fn errors_name_original_source_offsets() {
376 assert_eq!("".parse::<KeyGlobPattern>(), Err(KeyGlobError::Empty));
377 assert_eq!(
378 "µ.'unclosed".parse::<KeyGlobPattern>(),
379 Err(KeyGlobError::UnclosedQuote { index: 3 })
380 );
381 for (pattern, kind, index) in [
382 (
383 "'long literal'.{foo",
384 fast_glob::ErrorKind::UnclosedBrace,
385 15,
386 ),
387 ("[!a-z].{foo", fast_glob::ErrorKind::UnclosedBrace, 7),
388 ("foo.[bar", fast_glob::ErrorKind::UnclosedBracket, 4),
389 ("foo.\\", fast_glob::ErrorKind::TrailingBackslash, 4),
390 ("'foo\\", fast_glob::ErrorKind::TrailingBackslash, 4),
391 ] {
392 let err = pattern.parse::<KeyGlobPattern>().unwrap_err();
393 assert_eq!(err, KeyGlobError::Glob(GlobError { kind, index }));
394 assert!(!err.to_string().ends_with('\n'));
395 assert!(!err.to_string().contains("--shallow"));
396 }
397 }
398
399 proptest::proptest! {
400 #[test]
401 fn quoted_keys_round_trip_without_separator_collisions(key in ".*") {
402 let quoted = format!("'{}'", key.replace('\\', "\\\\").replace('\'', "\\'"));
403 let selector: KeyGlobPattern = quoted.parse().unwrap();
404 proptest::prop_assert!(selector.matches_keys(std::slice::from_ref(&key)));
405 if key.contains('/') {
406 let nested = key.split('/').map(String::from).collect::<Vec<_>>();
407 proptest::prop_assert!(!selector.matches_keys(&nested));
408 }
409 }
410 }
411}