##// END OF EJS Templates
rust-regex: prevent nonsensical `.*.*` pattern from happening...
Raphaël Gomès -
r45348:1e9bfeae default
parent child Browse files
Show More
@@ -1,657 +1,660 b''
1 1 // filepatterns.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Handling of Mercurial-specific patterns.
9 9
10 10 use crate::{
11 11 utils::{
12 12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
13 13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
14 14 SliceExt,
15 15 },
16 16 FastHashMap, PatternError,
17 17 };
18 18 use lazy_static::lazy_static;
19 19 use regex::bytes::{NoExpand, Regex};
20 20 use std::fs::File;
21 21 use std::io::Read;
22 22 use std::ops::Deref;
23 23 use std::path::{Path, PathBuf};
24 24 use std::vec::Vec;
25 25
26 26 lazy_static! {
27 27 static ref RE_ESCAPE: Vec<Vec<u8>> = {
28 28 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
29 29 let to_escape = b"()[]{}?*+-|^$\\.&~# \t\n\r\x0b\x0c";
30 30 for byte in to_escape {
31 31 v[*byte as usize].insert(0, b'\\');
32 32 }
33 33 v
34 34 };
35 35 }
36 36
37 37 /// These are matched in order
38 38 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
39 39 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
40 40
41 41 /// Appended to the regexp of globs
42 42 const GLOB_SUFFIX: &[u8; 7] = b"(?:/|$)";
43 43
44 44 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
45 45 pub enum PatternSyntax {
46 46 /// A regular expression
47 47 Regexp,
48 48 /// Glob that matches at the front of the path
49 49 RootGlob,
50 50 /// Glob that matches at any suffix of the path (still anchored at
51 51 /// slashes)
52 52 Glob,
53 53 /// a path relative to repository root, which is matched recursively
54 54 Path,
55 55 /// A path relative to cwd
56 56 RelPath,
57 57 /// an unrooted glob (*.rs matches Rust files in all dirs)
58 58 RelGlob,
59 59 /// A regexp that needn't match the start of a name
60 60 RelRegexp,
61 61 /// A path relative to repository root, which is matched non-recursively
62 62 /// (will not match subdirectories)
63 63 RootFiles,
64 64 /// A file of patterns to read and include
65 65 Include,
66 66 /// A file of patterns to match against files under the same directory
67 67 SubInclude,
68 68 }
69 69
70 70 /// Transforms a glob pattern into a regex
71 71 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
72 72 let mut input = pat;
73 73 let mut res: Vec<u8> = vec![];
74 74 let mut group_depth = 0;
75 75
76 76 while let Some((c, rest)) = input.split_first() {
77 77 input = rest;
78 78
79 79 match c {
80 80 b'*' => {
81 81 for (source, repl) in GLOB_REPLACEMENTS {
82 82 if let Some(rest) = input.drop_prefix(source) {
83 83 input = rest;
84 84 res.extend(*repl);
85 85 break;
86 86 }
87 87 }
88 88 }
89 89 b'?' => res.extend(b"."),
90 90 b'[' => {
91 91 match input.iter().skip(1).position(|b| *b == b']') {
92 92 None => res.extend(b"\\["),
93 93 Some(end) => {
94 94 // Account for the one we skipped
95 95 let end = end + 1;
96 96
97 97 res.extend(b"[");
98 98
99 99 for (i, b) in input[..end].iter().enumerate() {
100 100 if *b == b'!' && i == 0 {
101 101 res.extend(b"^")
102 102 } else if *b == b'^' && i == 0 {
103 103 res.extend(b"\\^")
104 104 } else if *b == b'\\' {
105 105 res.extend(b"\\\\")
106 106 } else {
107 107 res.push(*b)
108 108 }
109 109 }
110 110 res.extend(b"]");
111 111 input = &input[end + 1..];
112 112 }
113 113 }
114 114 }
115 115 b'{' => {
116 116 group_depth += 1;
117 117 res.extend(b"(?:")
118 118 }
119 119 b'}' if group_depth > 0 => {
120 120 group_depth -= 1;
121 121 res.extend(b")");
122 122 }
123 123 b',' if group_depth > 0 => res.extend(b"|"),
124 124 b'\\' => {
125 125 let c = {
126 126 if let Some((c, rest)) = input.split_first() {
127 127 input = rest;
128 128 c
129 129 } else {
130 130 c
131 131 }
132 132 };
133 133 res.extend(&RE_ESCAPE[*c as usize])
134 134 }
135 135 _ => res.extend(&RE_ESCAPE[*c as usize]),
136 136 }
137 137 }
138 138 res
139 139 }
140 140
141 141 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
142 142 pattern
143 143 .iter()
144 144 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
145 145 .collect()
146 146 }
147 147
148 148 pub fn parse_pattern_syntax(
149 149 kind: &[u8],
150 150 ) -> Result<PatternSyntax, PatternError> {
151 151 match kind {
152 152 b"re:" => Ok(PatternSyntax::Regexp),
153 153 b"path:" => Ok(PatternSyntax::Path),
154 154 b"relpath:" => Ok(PatternSyntax::RelPath),
155 155 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
156 156 b"relglob:" => Ok(PatternSyntax::RelGlob),
157 157 b"relre:" => Ok(PatternSyntax::RelRegexp),
158 158 b"glob:" => Ok(PatternSyntax::Glob),
159 159 b"rootglob:" => Ok(PatternSyntax::RootGlob),
160 160 b"include:" => Ok(PatternSyntax::Include),
161 161 b"subinclude:" => Ok(PatternSyntax::SubInclude),
162 162 _ => Err(PatternError::UnsupportedSyntax(
163 163 String::from_utf8_lossy(kind).to_string(),
164 164 )),
165 165 }
166 166 }
167 167
168 168 /// Builds the regex that corresponds to the given pattern.
169 169 /// If within a `syntax: regexp` context, returns the pattern,
170 170 /// otherwise, returns the corresponding regex.
171 171 fn _build_single_regex(entry: &IgnorePattern) -> Vec<u8> {
172 172 let IgnorePattern {
173 173 syntax, pattern, ..
174 174 } = entry;
175 175 if pattern.is_empty() {
176 176 return vec![];
177 177 }
178 178 match syntax {
179 179 PatternSyntax::Regexp => pattern.to_owned(),
180 180 PatternSyntax::RelRegexp => {
181 181 // The `regex` crate accepts `**` while `re2` and Python's `re`
182 182 // do not. Checking for `*` correctly triggers the same error all
183 183 // engines.
184 if pattern[0] == b'^' || pattern[0] == b'*' {
184 if pattern[0] == b'^'
185 || pattern[0] == b'*'
186 || pattern.starts_with(b".*")
187 {
185 188 return pattern.to_owned();
186 189 }
187 190 [&b".*"[..], pattern].concat()
188 191 }
189 192 PatternSyntax::Path | PatternSyntax::RelPath => {
190 193 if pattern == b"." {
191 194 return vec![];
192 195 }
193 196 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
194 197 }
195 198 PatternSyntax::RootFiles => {
196 199 let mut res = if pattern == b"." {
197 200 vec![]
198 201 } else {
199 202 // Pattern is a directory name.
200 203 [escape_pattern(pattern).as_slice(), b"/"].concat()
201 204 };
202 205
203 206 // Anything after the pattern must be a non-directory.
204 207 res.extend(b"[^/]+$");
205 208 res
206 209 }
207 210 PatternSyntax::RelGlob => {
208 211 let glob_re = glob_to_re(pattern);
209 212 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
210 213 [b".*", rest, GLOB_SUFFIX].concat()
211 214 } else {
212 215 [b"(?:.*/)?", glob_re.as_slice(), GLOB_SUFFIX].concat()
213 216 }
214 217 }
215 218 PatternSyntax::Glob | PatternSyntax::RootGlob => {
216 219 [glob_to_re(pattern).as_slice(), GLOB_SUFFIX].concat()
217 220 }
218 221 PatternSyntax::Include | PatternSyntax::SubInclude => unreachable!(),
219 222 }
220 223 }
221 224
222 225 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
223 226 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
224 227
225 228 /// TODO support other platforms
226 229 #[cfg(unix)]
227 230 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
228 231 if bytes.is_empty() {
229 232 return b".".to_vec();
230 233 }
231 234 let sep = b'/';
232 235
233 236 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
234 237 if initial_slashes > 2 {
235 238 // POSIX allows one or two initial slashes, but treats three or more
236 239 // as single slash.
237 240 initial_slashes = 1;
238 241 }
239 242 let components = bytes
240 243 .split(|b| *b == sep)
241 244 .filter(|c| !(c.is_empty() || c == b"."))
242 245 .fold(vec![], |mut acc, component| {
243 246 if component != b".."
244 247 || (initial_slashes == 0 && acc.is_empty())
245 248 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
246 249 {
247 250 acc.push(component)
248 251 } else if !acc.is_empty() {
249 252 acc.pop();
250 253 }
251 254 acc
252 255 });
253 256 let mut new_bytes = components.join(&sep);
254 257
255 258 if initial_slashes > 0 {
256 259 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
257 260 buf.extend(new_bytes);
258 261 new_bytes = buf;
259 262 }
260 263 if new_bytes.is_empty() {
261 264 b".".to_vec()
262 265 } else {
263 266 new_bytes
264 267 }
265 268 }
266 269
267 270 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
268 271 /// that don't need to be transformed into a regex.
269 272 pub fn build_single_regex(
270 273 entry: &IgnorePattern,
271 274 ) -> Result<Option<Vec<u8>>, PatternError> {
272 275 let IgnorePattern {
273 276 pattern, syntax, ..
274 277 } = entry;
275 278 let pattern = match syntax {
276 279 PatternSyntax::RootGlob
277 280 | PatternSyntax::Path
278 281 | PatternSyntax::RelGlob
279 282 | PatternSyntax::RootFiles => normalize_path_bytes(&pattern),
280 283 PatternSyntax::Include | PatternSyntax::SubInclude => {
281 284 return Err(PatternError::NonRegexPattern(entry.clone()))
282 285 }
283 286 _ => pattern.to_owned(),
284 287 };
285 288 if *syntax == PatternSyntax::RootGlob
286 289 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b))
287 290 {
288 291 Ok(None)
289 292 } else {
290 293 let mut entry = entry.clone();
291 294 entry.pattern = pattern;
292 295 Ok(Some(_build_single_regex(&entry)))
293 296 }
294 297 }
295 298
296 299 lazy_static! {
297 300 static ref SYNTAXES: FastHashMap<&'static [u8], &'static [u8]> = {
298 301 let mut m = FastHashMap::default();
299 302
300 303 m.insert(b"re".as_ref(), b"relre:".as_ref());
301 304 m.insert(b"regexp".as_ref(), b"relre:".as_ref());
302 305 m.insert(b"glob".as_ref(), b"relglob:".as_ref());
303 306 m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref());
304 307 m.insert(b"include".as_ref(), b"include:".as_ref());
305 308 m.insert(b"subinclude".as_ref(), b"subinclude:".as_ref());
306 309 m
307 310 };
308 311 }
309 312
310 313 #[derive(Debug)]
311 314 pub enum PatternFileWarning {
312 315 /// (file path, syntax bytes)
313 316 InvalidSyntax(PathBuf, Vec<u8>),
314 317 /// File path
315 318 NoSuchFile(PathBuf),
316 319 }
317 320
318 321 pub fn parse_pattern_file_contents<P: AsRef<Path>>(
319 322 lines: &[u8],
320 323 file_path: P,
321 324 warn: bool,
322 325 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
323 326 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
324 327 let comment_escape_regex = Regex::new(r"\\#").unwrap();
325 328 let mut inputs: Vec<IgnorePattern> = vec![];
326 329 let mut warnings: Vec<PatternFileWarning> = vec![];
327 330
328 331 let mut current_syntax = b"relre:".as_ref();
329 332
330 333 for (line_number, mut line) in lines.split(|c| *c == b'\n').enumerate() {
331 334 let line_number = line_number + 1;
332 335
333 336 let line_buf;
334 337 if line.contains(&b'#') {
335 338 if let Some(cap) = comment_regex.captures(line) {
336 339 line = &line[..cap.get(1).unwrap().end()]
337 340 }
338 341 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
339 342 line = &line_buf;
340 343 }
341 344
342 345 let mut line = line.trim_end();
343 346
344 347 if line.is_empty() {
345 348 continue;
346 349 }
347 350
348 351 if let Some(syntax) = line.drop_prefix(b"syntax:") {
349 352 let syntax = syntax.trim();
350 353
351 354 if let Some(rel_syntax) = SYNTAXES.get(syntax) {
352 355 current_syntax = rel_syntax;
353 356 } else if warn {
354 357 warnings.push(PatternFileWarning::InvalidSyntax(
355 358 file_path.as_ref().to_owned(),
356 359 syntax.to_owned(),
357 360 ));
358 361 }
359 362 continue;
360 363 }
361 364
362 365 let mut line_syntax: &[u8] = &current_syntax;
363 366
364 367 for (s, rels) in SYNTAXES.iter() {
365 368 if let Some(rest) = line.drop_prefix(rels) {
366 369 line_syntax = rels;
367 370 line = rest;
368 371 break;
369 372 }
370 373 if let Some(rest) = line.drop_prefix(&[s, &b":"[..]].concat()) {
371 374 line_syntax = rels;
372 375 line = rest;
373 376 break;
374 377 }
375 378 }
376 379
377 380 inputs.push(IgnorePattern::new(
378 381 parse_pattern_syntax(&line_syntax).map_err(|e| match e {
379 382 PatternError::UnsupportedSyntax(syntax) => {
380 383 PatternError::UnsupportedSyntaxInFile(
381 384 syntax,
382 385 file_path.as_ref().to_string_lossy().into(),
383 386 line_number,
384 387 )
385 388 }
386 389 _ => e,
387 390 })?,
388 391 &line,
389 392 &file_path,
390 393 ));
391 394 }
392 395 Ok((inputs, warnings))
393 396 }
394 397
395 398 pub fn read_pattern_file<P: AsRef<Path>>(
396 399 file_path: P,
397 400 warn: bool,
398 401 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
399 402 let mut f = match File::open(file_path.as_ref()) {
400 403 Ok(f) => Ok(f),
401 404 Err(e) => match e.kind() {
402 405 std::io::ErrorKind::NotFound => {
403 406 return Ok((
404 407 vec![],
405 408 vec![PatternFileWarning::NoSuchFile(
406 409 file_path.as_ref().to_owned(),
407 410 )],
408 411 ))
409 412 }
410 413 _ => Err(e),
411 414 },
412 415 }?;
413 416 let mut contents = Vec::new();
414 417
415 418 f.read_to_end(&mut contents)?;
416 419
417 420 Ok(parse_pattern_file_contents(&contents, file_path, warn)?)
418 421 }
419 422
420 423 /// Represents an entry in an "ignore" file.
421 424 #[derive(Debug, Eq, PartialEq, Clone)]
422 425 pub struct IgnorePattern {
423 426 pub syntax: PatternSyntax,
424 427 pub pattern: Vec<u8>,
425 428 pub source: PathBuf,
426 429 }
427 430
428 431 impl IgnorePattern {
429 432 pub fn new(
430 433 syntax: PatternSyntax,
431 434 pattern: &[u8],
432 435 source: impl AsRef<Path>,
433 436 ) -> Self {
434 437 Self {
435 438 syntax,
436 439 pattern: pattern.to_owned(),
437 440 source: source.as_ref().to_owned(),
438 441 }
439 442 }
440 443 }
441 444
442 445 pub type PatternResult<T> = Result<T, PatternError>;
443 446
444 447 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
445 448 /// patterns.
446 449 ///
447 450 /// `subinclude:` is not treated as a special pattern here: unraveling them
448 451 /// needs to occur in the "ignore" phase.
449 452 pub fn get_patterns_from_file(
450 453 pattern_file: impl AsRef<Path>,
451 454 root_dir: impl AsRef<Path>,
452 455 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
453 456 let (patterns, mut warnings) = read_pattern_file(&pattern_file, true)?;
454 457 let patterns = patterns
455 458 .into_iter()
456 459 .flat_map(|entry| -> PatternResult<_> {
457 460 let IgnorePattern {
458 461 syntax,
459 462 pattern,
460 463 source: _,
461 464 } = &entry;
462 465 Ok(match syntax {
463 466 PatternSyntax::Include => {
464 467 let inner_include =
465 468 root_dir.as_ref().join(get_path_from_bytes(&pattern));
466 469 let (inner_pats, inner_warnings) = get_patterns_from_file(
467 470 &inner_include,
468 471 root_dir.as_ref(),
469 472 )?;
470 473 warnings.extend(inner_warnings);
471 474 inner_pats
472 475 }
473 476 _ => vec![entry],
474 477 })
475 478 })
476 479 .flatten()
477 480 .collect();
478 481
479 482 Ok((patterns, warnings))
480 483 }
481 484
482 485 /// Holds all the information needed to handle a `subinclude:` pattern.
483 486 pub struct SubInclude {
484 487 /// Will be used for repository (hg) paths that start with this prefix.
485 488 /// It is relative to the current working directory, so comparing against
486 489 /// repository paths is painless.
487 490 pub prefix: HgPathBuf,
488 491 /// The file itself, containing the patterns
489 492 pub path: PathBuf,
490 493 /// Folder in the filesystem where this it applies
491 494 pub root: PathBuf,
492 495 }
493 496
494 497 impl SubInclude {
495 498 pub fn new(
496 499 root_dir: impl AsRef<Path>,
497 500 pattern: &[u8],
498 501 source: impl AsRef<Path>,
499 502 ) -> Result<SubInclude, HgPathError> {
500 503 let normalized_source =
501 504 normalize_path_bytes(&get_bytes_from_path(source));
502 505
503 506 let source_root = get_path_from_bytes(&normalized_source);
504 507 let source_root = source_root.parent().unwrap_or(source_root.deref());
505 508
506 509 let path = source_root.join(get_path_from_bytes(pattern));
507 510 let new_root = path.parent().unwrap_or(path.deref());
508 511
509 512 let prefix = canonical_path(&root_dir, &root_dir, new_root)?;
510 513
511 514 Ok(Self {
512 515 prefix: path_to_hg_path_buf(prefix).and_then(|mut p| {
513 516 if !p.is_empty() {
514 517 p.push(b'/');
515 518 }
516 519 Ok(p)
517 520 })?,
518 521 path: path.to_owned(),
519 522 root: new_root.to_owned(),
520 523 })
521 524 }
522 525 }
523 526
524 527 /// Separate and pre-process subincludes from other patterns for the "ignore"
525 528 /// phase.
526 529 pub fn filter_subincludes(
527 530 ignore_patterns: &[IgnorePattern],
528 531 root_dir: impl AsRef<Path>,
529 532 ) -> Result<(Vec<SubInclude>, Vec<&IgnorePattern>), HgPathError> {
530 533 let mut subincludes = vec![];
531 534 let mut others = vec![];
532 535
533 536 for ignore_pattern in ignore_patterns.iter() {
534 537 let IgnorePattern {
535 538 syntax,
536 539 pattern,
537 540 source,
538 541 } = ignore_pattern;
539 542 if *syntax == PatternSyntax::SubInclude {
540 543 subincludes.push(SubInclude::new(&root_dir, pattern, &source)?);
541 544 } else {
542 545 others.push(ignore_pattern)
543 546 }
544 547 }
545 548 Ok((subincludes, others))
546 549 }
547 550
548 551 #[cfg(test)]
549 552 mod tests {
550 553 use super::*;
551 554 use pretty_assertions::assert_eq;
552 555
553 556 #[test]
554 557 fn escape_pattern_test() {
555 558 let untouched =
556 559 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
557 560 assert_eq!(escape_pattern(untouched), untouched.to_vec());
558 561 // All escape codes
559 562 assert_eq!(
560 563 escape_pattern(br#"()[]{}?*+-|^$\\.&~# \t\n\r\v\f"#),
561 564 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\ \\t\\n\\r\\v\\f"#
562 565 .to_vec()
563 566 );
564 567 }
565 568
566 569 #[test]
567 570 fn glob_test() {
568 571 assert_eq!(glob_to_re(br#"?"#), br#"."#);
569 572 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
570 573 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
571 574 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
572 575 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
573 576 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
574 577 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
575 578 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
576 579 }
577 580
578 581 #[test]
579 582 fn test_parse_pattern_file_contents() {
580 583 let lines = b"syntax: glob\n*.elc";
581 584
582 585 assert_eq!(
583 586 parse_pattern_file_contents(lines, Path::new("file_path"), false)
584 587 .unwrap()
585 588 .0,
586 589 vec![IgnorePattern::new(
587 590 PatternSyntax::RelGlob,
588 591 b"*.elc",
589 592 Path::new("file_path")
590 593 )],
591 594 );
592 595
593 596 let lines = b"syntax: include\nsyntax: glob";
594 597
595 598 assert_eq!(
596 599 parse_pattern_file_contents(lines, Path::new("file_path"), false)
597 600 .unwrap()
598 601 .0,
599 602 vec![]
600 603 );
601 604 let lines = b"glob:**.o";
602 605 assert_eq!(
603 606 parse_pattern_file_contents(lines, Path::new("file_path"), false)
604 607 .unwrap()
605 608 .0,
606 609 vec![IgnorePattern::new(
607 610 PatternSyntax::RelGlob,
608 611 b"**.o",
609 612 Path::new("file_path")
610 613 )]
611 614 );
612 615 }
613 616
614 617 #[test]
615 618 fn test_build_single_regex() {
616 619 assert_eq!(
617 620 build_single_regex(&IgnorePattern::new(
618 621 PatternSyntax::RelGlob,
619 622 b"rust/target/",
620 623 Path::new("")
621 624 ))
622 625 .unwrap(),
623 626 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
624 627 );
625 628 }
626 629
627 630 #[test]
628 631 fn test_build_single_regex_shortcut() {
629 632 assert_eq!(
630 633 build_single_regex(&IgnorePattern::new(
631 634 PatternSyntax::RootGlob,
632 635 b"",
633 636 Path::new("")
634 637 ))
635 638 .unwrap(),
636 639 None,
637 640 );
638 641 assert_eq!(
639 642 build_single_regex(&IgnorePattern::new(
640 643 PatternSyntax::RootGlob,
641 644 b"whatever",
642 645 Path::new("")
643 646 ))
644 647 .unwrap(),
645 648 None,
646 649 );
647 650 assert_eq!(
648 651 build_single_regex(&IgnorePattern::new(
649 652 PatternSyntax::RootGlob,
650 653 b"*.o",
651 654 Path::new("")
652 655 ))
653 656 .unwrap(),
654 657 Some(br"[^/]*\.o(?:/|$)".to_vec()),
655 658 );
656 659 }
657 660 }
General Comments 0
You need to be logged in to leave comments. Login now