From de2567a4c76fa671005538d6cd841abc44932b9a Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 12 Oct 2025 16:29:10 -0400 Subject: [PATCH] printer: fix panic in replacements in look-around corner case The abstraction boundary fuck up is the gift that keeps on giving. It turns out that the invariant that the match would never exceed the range given is not always true. So we kludge around it. Also, update the CHANGELOG to include the fix for #2111. Fixes #3180 --- CHANGELOG.md | 4 ++++ crates/printer/src/util.rs | 14 ++++++++++---- tests/regression.rs | 13 +++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f0177d1..b6ece534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Unreleased changes. Release notes have not yet been written. Performance improvements: +* [PERF #2111](https://github.com/BurntSushi/ripgrep/issues/2111): + Don't resolve helper binaries on Windows when `-z/--search-zip` isn't used. * [PERF #2865](https://github.com/BurntSushi/ripgrep/pull/2865): Avoid using path canonicalization on Windows when emitting hyperlinks. @@ -46,6 +48,8 @@ Bug fixes: Statically compile PCRE2 into macOS release artifacts on `aarch64`. * [BUG #3173](https://github.com/BurntSushi/ripgrep/issues/3173): Fix ancestor ignore filter bug when searching whitelisted hidden files. +* [BUG #3180](https://github.com/BurntSushi/ripgrep/issues/3180): + Fix a panicking bug when using `-U/--multiline` and `-r/--replace`. Feature enhancements: diff --git a/crates/printer/src/util.rs b/crates/printer/src/util.rs index 4fd96cc2..491a0b7e 100644 --- a/crates/printer/src/util.rs +++ b/crates/printer/src/util.rs @@ -59,7 +59,8 @@ impl Replacer { // See the giant comment in 'find_iter_at_in_context' below for why we // do this dance. let is_multi_line = searcher.multi_line_with_matcher(&matcher); - // Get the line_terminator that was removed (if any) so we can add it back + // Get the line_terminator that was removed (if any) so we can add it + // back. let line_terminator = if is_multi_line { if haystack[range.end..].len() >= MAX_LOOK_AHEAD { haystack = &haystack[..range.end + MAX_LOOK_AHEAD]; @@ -513,7 +514,8 @@ where // Otherwise, it's possible for the regex (via look-around) to observe // the line terminator and not match because of it. let mut m = Match::new(0, range.end); - // No need to rember the line terminator as we aren't doing a replace here + // No need to rember the line terminator as we aren't doing a replace + // here. trim_line_terminator(searcher, bytes, &mut m); bytes = &bytes[..m.end()]; } @@ -575,9 +577,13 @@ where last_match = m.end(); append(caps, dst) })?; - let end = std::cmp::min(bytes.len(), range.end); + let end = if last_match > range.end { + bytes.len() + } else { + std::cmp::min(bytes.len(), range.end) + }; dst.extend(&bytes[last_match..end]); - // Add back any line terminator + // Add back any line terminator. dst.extend(line_terminator); Ok(()) } diff --git a/tests/regression.rs b/tests/regression.rs index f3ba63b0..93e4ba67 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1654,3 +1654,16 @@ rgtest!(r3173_hidden_whitelist_only_dot, |dir: Dir, _: TestCommand| { eqnice!(cmd().args(&["--files", "."]).stdout(), "./.foo.txt\n"); eqnice!(cmd().args(&["--files", "./"]).stdout(), "./.foo.txt\n"); }); + +// See: https://github.com/BurntSushi/ripgrep/issues/3180 +rgtest!(r3180_look_around_panic, |dir: Dir, mut cmd: TestCommand| { + dir.create("haystack", " b b b b b b b b\nc\n"); + + let got = cmd + .arg(r#"(^|[^a-z])((([a-z]+)?)\s)?b(\s([a-z]+)?)($|[^a-z])"#) + .arg("haystack") + .arg("-U") + .arg("-rx") + .stdout(); + eqnice!("xbxbx\n", got); +});