From 1b89a39f1f0a094028f7ab130295012089c4ce7f Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Fri, 28 Aug 2026 08:38:00 +0530 Subject: [PATCH] Don't drop dot-entries for patterns with a literal leading dot `fill_todo` removes every directory entry whose name starts with a `.` when `require_literal_leading_dot` is set, before the entry is ever matched against the pattern for that component. That also removes entries a pattern is supposed to match: `require_literal_leading_dot` only asks that the `.` appear literally in the pattern, so `.git*` should still match `.gitignore`. `Pattern::matches_with` gets this right (it is covered by `test_pattern_matches_require_literal_leading_dot`), so `glob_with` disagreed with matching the same component by hand: // .gitignore exists in the current directory let opts = MatchOptions { require_literal_leading_dot: true, ..MatchOptions::new() }; Pattern::new(".git*").unwrap().matches_with(".gitignore", opts); // true glob_with(".git*", opts).unwrap().count(); // 0 The filtering is still needed for a recursive `**` component, which matches directory entries without consulting the pattern at all, so keep it for every pattern that does not start with a literal `.`. Reuse the same "starts with a literal dot" test that already guards the `.` and `..` special entries a few lines below. --- src/lib.rs | 16 ++++++++++++++-- tests/glob-std.rs | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3ef5091..d683595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -905,6 +905,10 @@ fn fill_todo( }; let pattern = &patterns[idx]; + // Whether this component's pattern starts with a literal `.`, in which case + // it is allowed to match entries whose name starts with a `.` even when + // `require_literal_leading_dot` is set. + let has_literal_leading_dot = matches!(pattern.tokens.first(), Some(Char('.'))); let is_dir = path.is_directory; let curdir = path.as_ref() == Path::new("."); match (pattern.has_metachars, is_dir) { @@ -960,7 +964,15 @@ fn fill_todo( }); match dirs { Ok(mut children) => { - if options.require_literal_leading_dot { + // Entries whose name starts with a `.` are skipped here so + // that a recursive `**` component, which matches directory + // entries without ever consulting the pattern, does not + // descend into them. Patterns that do start with a literal + // `.` must not be filtered: `Pattern::matches_with` already + // implements `require_literal_leading_dot` faithfully for + // them, and dropping the entries here would make e.g. + // `.git*` match nothing at all. + if options.require_literal_leading_dot && !has_literal_leading_dot { children.retain(|x| !x.1.to_str().unwrap().starts_with('.')); } children.sort_by(|p1, p2| p2.1.cmp(&p1.1)); @@ -971,7 +983,7 @@ fn fill_todo( // requires that the pattern has a leading dot, even if the // `MatchOptions` field `require_literal_leading_dot` is not // set. - if !pattern.tokens.is_empty() && pattern.tokens[0] == Char('.') { + if has_literal_leading_dot { for &special in &[".", ".."] { if pattern.matches_with(special, options) { add(todo, PathWrapper::from_path(path.join(special))); diff --git a/tests/glob-std.rs b/tests/glob-std.rs index f8c5379..510cf18 100644 --- a/tests/glob-std.rs +++ b/tests/glob-std.rs @@ -384,6 +384,31 @@ fn main() { vec!(PathBuf::from("i/qwe"), PathBuf::from("i/qwe/eee")) ); + // `require_literal_leading_dot` only requires that the leading `.` appears + // literally in the pattern -- it must not stop such a pattern from matching. + // This mirrors what `Pattern::matches_with` does for the same options. + mk_file("j", true); + mk_file("j/.aaa", false); + mk_file("j/.abb", false); + mk_file("j/bbb", false); + + assert_eq!( + glob_with_vec("j/.a*", options), + vec!(PathBuf::from("j/.aaa"), PathBuf::from("j/.abb")) + ); + assert_eq!( + glob_with_vec("j/.?aa", options), + vec!(PathBuf::from("j/.aaa")) + ); + assert_eq!( + glob_with_vec("j/.[ab]bb", options), + vec!(PathBuf::from("j/.abb")) + ); + // ... while patterns that do not start with a literal `.` keep skipping + // those entries. + assert_eq!(glob_with_vec("j/*", options), vec!(PathBuf::from("j/bbb"))); + assert_eq!(glob_with_vec("j/?aa", options), Vec::::new()); + if env::consts::FAMILY != "windows" { assert_eq!( glob_vec("bbb/specials/[*]"),