From 4df1298127695ed1e76937f16de1a0135677ccf5 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 17 Aug 2025 17:18:27 -0400 Subject: [PATCH] globset: fix bug where trailing `.` in file name was incorrectly handled I'm not sure why I did this, but I think I was trying to imitate the contract of [`std::path::Path::file_name`]: > Returns None if the path terminates in `..`. But the status quo clearly did not implement this. And as a result, if you have a glob that ends in a `.`, it was instead treated as the empty string (which only matches the empty string). We fix this by implementing the semantic from the standard library correctly. Fixes #2990 [`std::path::Path::file_name`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name --- CHANGELOG.md | 2 ++ crates/globset/src/pathutil.rs | 14 +++++++++----- tests/regression.rs | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0159bb0..e37735e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Bug fixes: Ignore a UTF-8 BOM marker at the start of `.gitignore` (and similar files). * [BUG #2944](https://github.com/BurntSushi/ripgrep/pull/2944): Fix a bug where the "bytes searched" in `--stats` output could be incorrect. +* [BUG #2990](https://github.com/BurntSushi/ripgrep/issues/2990): + Fix a bug where ripgrep would mishandle globs that ended with a `.`. Feature enhancements: diff --git a/crates/globset/src/pathutil.rs b/crates/globset/src/pathutil.rs index 8488e74f..861f61b8 100644 --- a/crates/globset/src/pathutil.rs +++ b/crates/globset/src/pathutil.rs @@ -4,21 +4,25 @@ use bstr::{ByteSlice, ByteVec}; /// The final component of the path, if it is a normal file. /// -/// If the path terminates in `.`, `..`, or consists solely of a root of -/// prefix, file_name will return None. +/// If the path terminates in `..`, or consists solely of a root of prefix, +/// file_name will return `None`. pub(crate) fn file_name<'a>(path: &Cow<'a, [u8]>) -> Option> { - if path.last_byte().map_or(true, |b| b == b'.') { + if path.is_empty() { return None; } let last_slash = path.rfind_byte(b'/').map(|i| i + 1).unwrap_or(0); - Some(match *path { + let got = match *path { Cow::Borrowed(path) => Cow::Borrowed(&path[last_slash..]), Cow::Owned(ref path) => { let mut path = path.clone(); path.drain_bytes(..last_slash); Cow::Owned(path) } - }) + }; + if got == &b".."[..] { + return None; + } + Some(got) } /// Return a file extension given a path's file name. diff --git a/tests/regression.rs b/tests/regression.rs index 4fd3c0f8..ecc1205f 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1461,3 +1461,19 @@ rgtest!(r2944_incorrect_bytes_searched, |dir: Dir, mut cmd: TestCommand| { let got = cmd.args(&["--stats", "-m2", "foo", "."]).stdout(); assert!(got.contains("10 bytes searched\n")); }); + +// See: https://github.com/BurntSushi/ripgrep/issues/2990 +#[cfg(unix)] +rgtest!(r2990_trip_over_trailing_dot, |dir: Dir, _cmd: TestCommand| { + dir.create_dir("asdf"); + dir.create_dir("asdf."); + dir.create("asdf/foo", ""); + dir.create("asdf./foo", ""); + + let got = dir.command().args(&["--files", "-g", "!asdf/"]).stdout(); + eqnice!("asdf./foo\n", got); + + // This used to ignore the glob given and included `asdf./foo` in output. + let got = dir.command().args(&["--files", "-g", "!asdf./"]).stdout(); + eqnice!("asdf/foo\n", got); +});