From b3ee398bfbc365c99e6089e20bf78b1e9604033e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:25:41 +0000 Subject: [PATCH 1/2] Initial plan From 877a18df6d2e41e10fc5526e178e36a34d64e774 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:31:04 +0000 Subject: [PATCH 2/2] Fix search_slots panic with zero-repetition capture groups Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com> --- regex-automata/src/dfa/onepass.rs | 17 ++++--- regex-automata/tests/dfa/onepass/mod.rs | 1 + .../tests/dfa/onepass/regression.rs | 51 +++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 regex-automata/tests/dfa/onepass/regression.rs diff --git a/regex-automata/src/dfa/onepass.rs b/regex-automata/src/dfa/onepass.rs index 85f820ef54..a56a462511 100644 --- a/regex-automata/src/dfa/onepass.rs +++ b/regex-automata/src/dfa/onepass.rs @@ -2215,11 +2215,16 @@ impl DFA { // We *also* need to set any explicit slots that are active as part of // the path to the match state. if self.explicit_slot_start < slots.len() { - // NOTE: The 'cache.explicit_slots()' slice is setup at the - // beginning of every search such that it is guaranteed to return a - // slice of length equivalent to 'slots[explicit_slot_start..]'. - slots[self.explicit_slot_start..] - .copy_from_slice(cache.explicit_slots()); + // Copy only the slots that exist in the cache. When a regex has + // capture groups with zero repetition (e.g., (abc){0}), the cache + // may have fewer explicit slots than what the caller provided. + let cache_slots = cache.explicit_slots(); + let available = core::cmp::min( + cache_slots.len(), + slots.len().saturating_sub(self.explicit_slot_start), + ); + slots[self.explicit_slot_start..self.explicit_slot_start + available] + .copy_from_slice(&cache_slots[..available]); epsilons.slots().apply(at, &mut slots[self.explicit_slot_start..]); } *matched_pid = Some(pid); @@ -2577,7 +2582,7 @@ impl Cache { } fn setup_search(&mut self, explicit_slot_len: usize) { - self.explicit_slot_len = explicit_slot_len; + self.explicit_slot_len = core::cmp::min(explicit_slot_len, self.explicit_slots.len()); } } diff --git a/regex-automata/tests/dfa/onepass/mod.rs b/regex-automata/tests/dfa/onepass/mod.rs index 9d6ab475ef..aa656435d4 100644 --- a/regex-automata/tests/dfa/onepass/mod.rs +++ b/regex-automata/tests/dfa/onepass/mod.rs @@ -1,2 +1,3 @@ #[cfg(not(miri))] mod suite; +mod regression; diff --git a/regex-automata/tests/dfa/onepass/regression.rs b/regex-automata/tests/dfa/onepass/regression.rs new file mode 100644 index 0000000000..1968e26ea8 --- /dev/null +++ b/regex-automata/tests/dfa/onepass/regression.rs @@ -0,0 +1,51 @@ +// Regression test for zero-repetition capture groups causing panic. +// See: https://github.com/rust-lang/regex/issues/XXX +#[test] +fn zero_repetition_capture_group() { + use regex_automata::{ + dfa::onepass::DFA, + util::primitives::NonMaxUsize, + Anchored, Input, + }; + + let expr = DFA::new(r"(abc)(ABC){0}").unwrap(); + let s = "abcABC"; + let input = Input::new(s).span(0..s.len()).anchored(Anchored::Yes); + + // Test with slot array sized for the pattern + let mut cache = expr.create_cache(); + let mut slots: Vec> = vec![None; 4]; + let pid = expr.try_search_slots(&mut cache, &input, &mut slots).unwrap(); + assert_eq!(pid, Some(regex_automata::PatternID::must(0))); + assert_eq!(slots[0], Some(NonMaxUsize::new(0).unwrap())); + assert_eq!(slots[1], Some(NonMaxUsize::new(3).unwrap())); + assert_eq!(slots[2], Some(NonMaxUsize::new(0).unwrap())); + assert_eq!(slots[3], Some(NonMaxUsize::new(3).unwrap())); + + // Test with larger slot array (simulating reuse after a different regex) + let mut slots2: Vec> = vec![None; 6]; + let pid2 = expr.try_search_slots(&mut cache, &input, &mut slots2).unwrap(); + assert_eq!(pid2, Some(regex_automata::PatternID::must(0))); + // First capture group should match + assert_eq!(slots2[2], Some(NonMaxUsize::new(0).unwrap())); + assert_eq!(slots2[3], Some(NonMaxUsize::new(3).unwrap())); + // Second capture group with {0} should be None + assert_eq!(slots2[4], None); + assert_eq!(slots2[5], None); + + // Test switching between different regexes with different capture group counts + let expr2 = DFA::new(r"(abc)(ABC)").unwrap(); + let mut cache2 = expr2.create_cache(); + let mut slots3: Vec> = vec![None; 6]; + let pid3 = expr2.try_search_slots(&mut cache2, &input, &mut slots3).unwrap(); + assert_eq!(pid3, Some(regex_automata::PatternID::must(0))); + assert_eq!(slots3[4], Some(NonMaxUsize::new(3).unwrap())); + assert_eq!(slots3[5], Some(NonMaxUsize::new(6).unwrap())); + + // Switch back to the first regex - this previously caused a panic + let mut slots4: Vec> = vec![None; 6]; + let pid4 = expr.try_search_slots(&mut cache, &input, &mut slots4).unwrap(); + assert_eq!(pid4, Some(regex_automata::PatternID::must(0))); + assert_eq!(slots4[4], None); + assert_eq!(slots4[5], None); +}