Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions regex-automata/src/meta/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,35 @@ impl ReverseSuffix {
}
let kind = core.info.config().get_match_kind();
let suffixes = crate::util::prefilter::suffixes(kind, hirs);
// An exact suffix literal that is also a proper suffix of another
// suffix literal means one alternation branch's complete match can be
// nested inside the tail of another branch's match. Candidate *ends*
// are then not ordered like match *starts*: the first confirmed
// candidate can yield a match that starts *later* than a still
// undiscovered overlapping match ending further right, violating
// leftmost-first. (Repro: `a|.aa` on `-#aa` reported [(2,3),(3,4)]
// instead of [(1,4)].) Decline the optimization in that case.
if let Some(lits) = suffixes.literals() {
for (i, a) in lits.iter().enumerate() {
if !a.is_exact() {
continue;
}
for (j, b) in lits.iter().enumerate() {
if i != j
&& b.as_bytes().len() > a.as_bytes().len()
&& b.as_bytes().ends_with(a.as_bytes())
{
debug!(
"skipping reverse suffix optimization because \
an exact suffix literal is a proper suffix of \
another suffix literal (nested-end overlaps \
defeat leftmost-first candidate ordering)"
);
return Err(core);
}
}
}
}
let lcs = match suffixes.longest_common_suffix() {
None => {
debug!(
Expand Down
17 changes: 17 additions & 0 deletions tests/regression_fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,20 @@ fn fail_branch_prevents_match() {
let re = Regex::new(pat).unwrap();
assert!(re.is_match(hay));
}

// This was caught by a differential fuzzer (REAL-regex vs this crate), and
// then minimized by hand.
//
// The reverse suffix optimization processes suffix-prefilter candidates in
// order of match *end*. When one alternation branch's entire match (an exact
// suffix literal) is a proper suffix of another branch's suffix, a match can
// be nested inside the tail of an overlapping match that ends later but
// starts *earlier*. Yielding the first confirmed candidate then skips the
// true leftmost match, and the anti-quadratic clamp makes the miss permanent.
#[test]
fn reverse_suffix_nested_overlap_misses_leftmost() {
let re = Regex::new("a|.aa").unwrap();
let spans: Vec<(usize, usize)> =
re.find_iter("-#aa").map(|m| (m.start(), m.end())).collect();
assert_eq!(spans, vec![(1, 4)]);
}
Loading