Skip to main content

guard_core_engine/redos/
repeat_prefix.rs

1//! The prefix walk: consumes parsed ops from the empty state and yields
2//! the reachable prefix states, recording reaching prefixes for repeated
3//! bodies.
4//!
5//! Port of the reference `_redos_repeat_prefix.py` (`_PrefixWalk` and
6//! `_repeat_reaching_prefixes`).
7
8use std::collections::HashSet;
9use std::time::Instant;
10
11use super::ast::{self, Flags, Op};
12use super::parse_slots::node_intervals;
13use super::prefix_history::{contains_repeat, optional_states_are_equivalent, suffix_history};
14use super::repeat_alphabet::repeat_alphabet_fills;
15use super::repeat_lookbehind::walk_negative_behind;
16use super::repeat_prefix_state::{
17    PREFIX_WALK_STATE_LIMIT, PREFIX_WALK_TEXT_BUDGET, REPEAT_PREFIX_STATE_LIMIT, RepeatPrefixState,
18    capture_state, consume_atom, consume_piece, positive_assertion, state_text_size,
19};
20use super::timeout::BuilderTimeout;
21
22pub(crate) type RepeatCollector<'a> =
23    &'a mut dyn FnMut(&[Op], Flags, &[RepeatPrefixState]) -> Result<(), BuilderTimeout>;
24
25pub(crate) fn check_deadline(deadline: Option<Instant>) -> Result<(), BuilderTimeout> {
26    if let Some(deadline) = deadline
27        && Instant::now() >= deadline
28    {
29        return Err(BuilderTimeout(
30            "Pattern validation repeat-prefix construction exceeded its deadline".into(),
31        ));
32    }
33    Ok(())
34}
35
36pub(crate) fn unique_states(
37    states: Vec<RepeatPrefixState>,
38) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
39    let mut unique: Vec<RepeatPrefixState> = Vec::with_capacity(states.len());
40    let mut seen: HashSet<RepeatPrefixState> = HashSet::with_capacity(states.len());
41    for state in states {
42        if seen.insert(state.clone()) {
43            unique.push(state);
44        }
45    }
46    if unique.len() > PREFIX_WALK_STATE_LIMIT {
47        return Err(BuilderTimeout(
48            "Pattern validation repeat-prefix state budget exceeded".into(),
49        ));
50    }
51    if unique.iter().map(state_text_size).sum::<usize>() > PREFIX_WALK_TEXT_BUDGET {
52        return Err(BuilderTimeout(
53            "Pattern validation repeat-prefix text budget exceeded".into(),
54        ));
55    }
56    Ok(unique)
57}
58
59fn assertion_witnesses(
60    body: &[Op],
61    flags: Flags,
62    deadline: Option<Instant>,
63) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
64    let mut unused_prefixes = Vec::new();
65    let mut walker = PrefixWalk {
66        flags,
67        prefixes: &mut unused_prefixes,
68        deadline,
69        collect: false,
70        alphabet: Vec::new(),
71        repeat_collector: None,
72        canonical_optionals: false,
73        require_reachable: false,
74    };
75    let states = walker.walk(body, vec![RepeatPrefixState::default()], false)?;
76    if states.iter().any(|state| {
77        !state.pending.is_empty() || !state.forbidden.is_empty() || !state.excluded.is_empty()
78    }) {
79        return Err(BuilderTimeout(
80            "Pattern validation cannot resolve nested assertion constraints".into(),
81        ));
82    }
83    Ok(states)
84}
85
86/// The reference `_PrefixWalk` as a context struct.
87pub(crate) struct PrefixWalk<'a> {
88    pub flags: Flags,
89    pub prefixes: &'a mut Vec<String>,
90    pub deadline: Option<Instant>,
91    pub collect: bool,
92    pub alphabet: Vec<String>,
93    pub repeat_collector: Option<RepeatCollector<'a>>,
94    pub canonical_optionals: bool,
95    pub require_reachable: bool,
96}
97
98impl PrefixWalk<'_> {
99    fn check_remaining_repeats(&self, items: &[Op]) -> Result<(), BuilderTimeout> {
100        if self.require_reachable && contains_repeat(items) {
101            return Err(BuilderTimeout(
102                "Pattern validation cannot resolve repeat-site prefixes".into(),
103            ));
104        }
105        Ok(())
106    }
107
108    fn atoms(
109        &self,
110        op: &Op,
111        states: Vec<RepeatPrefixState>,
112    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
113        let intervals = node_intervals(op, self.flags);
114        let mut result = Vec::with_capacity(states.len());
115        for state in states {
116            if let Some(next_state) = consume_atom(&state, &intervals)? {
117                result.push(next_state);
118            }
119        }
120        Ok(result)
121    }
122
123    fn record_prefixes(
124        &mut self,
125        high: Option<u32>,
126        states: &[RepeatPrefixState],
127    ) -> Result<(), BuilderTimeout> {
128        if self.collect && high.is_none_or(|high| high > 1) {
129            for state in states {
130                self.prefixes
131                    .push(format!("{}{}", state.text, state.pending));
132            }
133            if self.prefixes.len() > REPEAT_PREFIX_STATE_LIMIT * 4 {
134                return Err(BuilderTimeout(
135                    "Pattern validation repeat-prefix candidate budget exceeded".into(),
136                ));
137            }
138        }
139        Ok(())
140    }
141
142    fn repeat(
143        &mut self,
144        low: u32,
145        high: Option<u32>,
146        body: &[Op],
147        states: Vec<RepeatPrefixState>,
148        history_observable: bool,
149    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
150        self.record_prefixes(high, &states)?;
151        if high.is_none_or(|high| high > 1)
152            && let Some(collector) = self.repeat_collector.as_mut()
153        {
154            collector(body, self.flags, &states)?;
155        }
156        if low == 0 && high.is_none_or(|high| high > 0) {
157            let expanded = self.walk(body, states.clone(), history_observable)?;
158            if self.canonical_optionals
159                && optional_states_are_equivalent(body, &states, history_observable)
160            {
161                return Ok(states);
162            }
163            let mut all = states.clone();
164            all.extend(expanded.clone());
165            let combined = unique_states(all)?;
166            let remaining = match high {
167                None => u64::MAX,
168                Some(high) => u64::from(high.max(1) - 1),
169            };
170            return self.pending_repeats(body, combined, expanded, remaining, history_observable);
171        }
172        let mut current = states;
173        for _ in 0..low {
174            check_deadline(self.deadline)?;
175            let previous = current.clone();
176            current = self.walk(body, current, history_observable)?;
177            if current == previous || current.is_empty() {
178                break;
179            }
180        }
181        let remaining = match high {
182            None => u64::MAX,
183            Some(high) => u64::from(high.saturating_sub(low)),
184        };
185        let snapshot = current.clone();
186        self.pending_repeats(body, current, snapshot, remaining, history_observable)
187    }
188
189    fn pending_repeats(
190        &mut self,
191        body: &[Op],
192        result: Vec<RepeatPrefixState>,
193        frontier: Vec<RepeatPrefixState>,
194        remaining: u64,
195        history_observable: bool,
196    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
197        let mut result = result;
198        let mut frontier = frontier;
199        let mut remaining = remaining;
200        while remaining > 0 {
201            let pending: Vec<RepeatPrefixState> = frontier
202                .iter()
203                .filter(|state| !state.pending.is_empty())
204                .cloned()
205                .collect();
206            if pending.is_empty() {
207                break;
208            }
209            check_deadline(self.deadline)?;
210            frontier = self.walk(body, pending.clone(), history_observable)?;
211            let mut all = result.clone();
212            all.extend(frontier.clone());
213            result = unique_states(all)?;
214            if frontier == pending {
215                break;
216            }
217            remaining -= 1;
218        }
219        Ok(result)
220    }
221
222    fn branch(
223        &mut self,
224        alternatives: &[Vec<Op>],
225        states: Vec<RepeatPrefixState>,
226        history_observable: bool,
227    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
228        let mut result: Vec<RepeatPrefixState> = Vec::new();
229        for alternative in alternatives {
230            let walked = self.walk(alternative, states.clone(), history_observable)?;
231            let mut all = result;
232            all.extend(walked);
233            result = unique_states(all)?;
234        }
235        Ok(result)
236    }
237
238    fn group(
239        &mut self,
240        group: Option<u32>,
241        add: Flags,
242        del: Flags,
243        body: &[Op],
244        states: Vec<RepeatPrefixState>,
245        history_observable: bool,
246    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
247        // The reference builds a child walker that differs only in flags;
248        // every other field (prefixes, deadline, collector) is shared, so
249        // mutating in place and restoring is behaviorally identical.
250        let saved_flags = self.flags;
251        self.flags.apply_delta(&add, &del);
252        let mut result: Vec<RepeatPrefixState> = Vec::new();
253        for state in states {
254            let start = state.text.len();
255            let walked = self.walk(body, vec![state], history_observable)?;
256            for next_state in walked {
257                result.push(capture_state(&next_state, group, start));
258            }
259        }
260        self.flags = saved_flags;
261        Ok(result)
262    }
263
264    fn assertion(
265        &mut self,
266        behind: bool,
267        negated: bool,
268        body: &[Op],
269        states: Vec<RepeatPrefixState>,
270    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
271        let witnesses = assertion_witnesses(body, self.flags, self.deadline)?;
272        if !negated {
273            let mut result = Vec::new();
274            for state in &states {
275                result.extend(positive_assertion(behind, &witnesses, state));
276            }
277            return Ok(result);
278        }
279        if behind {
280            return walk_negative_behind(body, self.flags, &witnesses, &states, &self.alphabet);
281        }
282        let mut result = Vec::new();
283        for state in &states {
284            result.extend(super::repeat_prefix_state::negative_assertion(
285                body, self.flags, &witnesses, state,
286            ));
287        }
288        Ok(result)
289    }
290
291    fn backreference(
292        &self,
293        group: u32,
294        states: Vec<RepeatPrefixState>,
295    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
296        let mut result = Vec::new();
297        for state in states {
298            let captured = state
299                .captures
300                .iter()
301                .find(|(existing, _)| *existing == group)
302                .map(|(_, value)| value.clone());
303            if let Some(captured) = captured
304                && let Some(next_state) = consume_piece(&state, &captured)?
305            {
306                result.push(next_state);
307            }
308        }
309        Ok(result)
310    }
311
312    fn node(
313        &mut self,
314        op: &Op,
315        states: Vec<RepeatPrefixState>,
316        history_observable: bool,
317    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
318        match op {
319            Op::Literal(_) | Op::NotLiteral(_) | Op::In(_) | Op::Any | Op::Category(_) => {
320                self.atoms(op, states)
321            }
322            Op::Repeat {
323                kind: _,
324                low,
325                high,
326                body,
327            } => self.repeat(*low, *high, body, states, history_observable),
328            Op::GroupRef(group) => self.backreference(*group, states),
329            Op::Branch(alternatives) => self.branch(alternatives, states, history_observable),
330            Op::SubPattern {
331                group,
332                add,
333                del,
334                body,
335            } => self.group(*group, *add, *del, body, states, history_observable),
336            Op::GroupRefExists { group, yes, no } => {
337                self.conditional(*group, yes, no.as_deref(), states, history_observable)
338            }
339            Op::Assert {
340                behind,
341                negated,
342                body,
343            } => self.assertion(*behind, *negated, body, states),
344            Op::At(_) => Ok(states),
345            Op::Failure => Ok(Vec::new()),
346        }
347    }
348
349    fn conditional(
350        &mut self,
351        group: u32,
352        yes: &[Op],
353        no: Option<&[Op]>,
354        states: Vec<RepeatPrefixState>,
355        history_observable: bool,
356    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
357        let mut result = Vec::new();
358        for state in states {
359            let taken = if state.captures.iter().any(|(g, _)| *g == group) {
360                yes
361            } else {
362                no.unwrap_or(&[])
363            };
364            let walked = self.walk(taken, vec![state], history_observable)?;
365            result.extend(walked);
366        }
367        Ok(result)
368    }
369
370    /// Reference `_PrefixWalk.walk`.
371    pub fn walk(
372        &mut self,
373        items: &[Op],
374        states: Vec<RepeatPrefixState>,
375        history_observable: bool,
376    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
377        let history = suffix_history(items, history_observable);
378        let mut states = states;
379        for (index, op) in items.iter().enumerate() {
380            check_deadline(self.deadline)?;
381            states = unique_states(self.node(op, states, history[index])?)?;
382            if states.is_empty() {
383                self.check_remaining_repeats(&items[index + 1..])?;
384                break;
385            }
386        }
387        Ok(states)
388    }
389}
390
391/// Reference `_repeat_reaching_prefixes`.
392pub fn repeat_reaching_prefixes(
393    pattern: &str,
394    flags: Flags,
395    deadline: Option<Instant>,
396    require_reachable: bool,
397) -> Result<Vec<String>, BuilderTimeout> {
398    let Ok((ops, final_flags)) = ast::parse(pattern, flags) else {
399        return Ok(Vec::new());
400    };
401    let mut prefixes: Vec<String> = Vec::new();
402    let alphabet = repeat_alphabet_fills(pattern, flags, deadline, true)?;
403    let mut walker = PrefixWalk {
404        flags: final_flags,
405        prefixes: &mut prefixes,
406        deadline,
407        collect: true,
408        alphabet,
409        repeat_collector: None,
410        canonical_optionals: true,
411        require_reachable,
412    };
413    walker.walk(&ops, vec![RepeatPrefixState::default()], false)?;
414    let mut unique: Vec<String> = Vec::new();
415    for prefix in prefixes {
416        if !prefix.is_empty() && !unique.contains(&prefix) {
417            unique.push(prefix);
418        }
419    }
420    Ok(unique)
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    fn walk(
428        pattern: &str,
429        collect: bool,
430        require_reachable: bool,
431    ) -> Result<(Vec<String>, Vec<RepeatPrefixState>), BuilderTimeout> {
432        let (ops, flags) = crate::redos::ast::parse(pattern, Flags::default())
433            .map_err(|_| BuilderTimeout("parse error".into()))?;
434        let mut prefixes = Vec::new();
435        let mut walker = PrefixWalk {
436            flags,
437            prefixes: &mut prefixes,
438            deadline: None,
439            collect,
440            alphabet: vec!["x".to_owned(), "y".to_owned()],
441            repeat_collector: None,
442            canonical_optionals: true,
443            require_reachable,
444        };
445        let states = walker.walk(&ops, vec![RepeatPrefixState::default()], false)?;
446        Ok((prefixes, states))
447    }
448
449    #[test]
450    fn an_expired_deadline_surfaces_from_every_public_entry() {
451        let expired = std::time::Instant::now() - std::time::Duration::from_secs(1);
452        // the prefix walk itself
453        let error = walk_with_deadline("ab", expired).expect_err("deadline");
454        assert!(error.0.contains("deadline"), "{error}");
455        // the mandatory-repeat unroll and the pending-repeat ladder
456        let error = repeat_reaching_prefixes("a{40}b", Flags::default(), Some(expired), false)
457            .expect_err("deadline");
458        assert!(error.0.contains("deadline"), "{error}");
459        // the assertion-witness walker under a quantified assertion body
460        let (ops, _) = crate::redos::ast::parse("(?=a{40})", Flags::default()).expect("parses");
461        let error =
462            assertion_witnesses(&ops, Flags::default(), Some(expired)).expect_err("deadline");
463        assert!(error.0.contains("deadline"), "{error}");
464    }
465
466    #[test]
467    fn a_capturing_lookahead_witness_replaces_the_group_capture() {
468        // The positive lookahead's witness carries its own capture, which
469        // replaces the live capture of the same group before the literal.
470        let (_prefixes, states) = walk(r"(a)(?=(b))b", false, false).expect("walk");
471        assert_eq!(states.len(), 1);
472        assert_eq!(states[0].text, "ab");
473        // The lookahead's witness capture replaces its own group number.
474        assert_eq!(
475            states[0].captures,
476            vec![(1, "a".to_owned()), (2, "b".to_owned())]
477        );
478    }
479
480    #[test]
481    fn positive_lookbehind_witnesses_prepend_their_text() {
482        // The positive lookbehind rewrites the state text with the
483        // witness before the following literal is consumed.
484        let (_prefixes, states) = walk(r"(?<=xy)z", false, false).expect("walk");
485        assert_eq!(states.len(), 1);
486        assert_eq!(states[0].text, "xyz");
487    }
488
489    #[test]
490    fn single_bound_repeats_never_record_or_collect() {
491        // `a?` has high == Some(1): no prefix candidates, no repeat collect.
492        let (prefixes, states) = walk("xa?", true, false).expect("walk");
493        assert!(
494            prefixes.is_empty(),
495            "high == 1 records nothing: {prefixes:?}"
496        );
497        assert_eq!(states.len(), 1);
498    }
499
500    #[test]
501    fn a_fully_excluded_class_consumes_nothing() {
502        // `(?!\S)` excludes every non-space char, so the following `\S`
503        // cannot consume and the walk dies before the terminator.
504        let (_prefixes, states) = walk(r"(?!\S)\S", false, false).expect("walk");
505        assert!(states.is_empty(), "no state survives: {states:?}");
506    }
507
508    #[test]
509    fn an_uncaptured_conditional_takes_the_no_branch() {
510        // No group 1 exists, so the conditional walks its `no` branch.
511        let (_prefixes, states) = walk("(?(1)y|z)", false, false).expect("walk");
512        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
513        assert_eq!(texts, vec!["z".to_owned()]);
514        // The captured branch walks when group 1 is live.
515        let (_prefixes, states) = walk("(x)(?(1)y|z)", false, false).expect("walk");
516        assert_eq!(states[0].text, "xy");
517    }
518
519    #[test]
520    fn a_parse_failure_flows_through_the_harness() {
521        assert!(walk("[unterminated", false, false).is_err());
522    }
523
524    /// The walk harness with an injected deadline, for the arms only an
525    /// expired clock can reach.
526    fn walk_with_deadline(
527        pattern: &str,
528        deadline: std::time::Instant,
529    ) -> Result<Vec<RepeatPrefixState>, BuilderTimeout> {
530        let (ops, flags) = crate::redos::ast::parse(pattern, Flags::default()).expect("parses");
531        let mut prefixes = Vec::new();
532        let mut walker = PrefixWalk {
533            flags,
534            prefixes: &mut prefixes,
535            deadline: Some(deadline),
536            collect: false,
537            alphabet: Vec::new(),
538            repeat_collector: None,
539            canonical_optionals: true,
540            require_reachable: false,
541        };
542        walker.walk(&ops, vec![RepeatPrefixState::default()], false)
543    }
544
545    #[test]
546    fn literals_consume_their_first_member() {
547        let (_prefixes, states) = walk("ab", false, false).expect("walk");
548        assert_eq!(states.len(), 1);
549        assert_eq!(states[0].text, "ab");
550    }
551
552    #[test]
553    fn classes_consume_the_interval_start() {
554        let (_prefixes, states) = walk(r"\d", false, false).expect("walk");
555        assert_eq!(states[0].text, "0");
556    }
557
558    #[test]
559    fn optional_atoms_branch_the_states() {
560        // Canonical optional collapse: `a?` followed by `b` has the same
561        // prefix language as `b`, so the states merge.
562        let (_prefixes, states) = walk("a?b", false, false).expect("walk");
563        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
564        assert_eq!(texts, vec!["b".to_owned()]);
565    }
566
567    #[test]
568    fn unbounded_repeats_record_prefixes_when_collecting() {
569        // Prefixes record the states before the repeat site.
570        let (prefixes, _states) = walk(r"xa*", true, false).expect("walk");
571        assert!(prefixes.contains(&"x".to_owned()));
572        let (prefixes, _states) = walk(r"a", false, false).expect("walk");
573        assert!(prefixes.is_empty());
574    }
575
576    #[test]
577    fn mandatory_repeats_unroll_into_the_text() {
578        let (_prefixes, states) = walk("a{2}", false, false).expect("walk");
579        assert_eq!(states[0].text, "aa");
580    }
581
582    #[test]
583    fn failure_nodes_kill_the_walk() {
584        let (_prefixes, states) = walk(r"(?!)x", false, false).expect("walk");
585        assert!(states.is_empty());
586    }
587
588    #[test]
589    fn branches_union_the_alternative_states() {
590        let (_prefixes, states) = walk("x|y", false, false).expect("walk");
591        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
592        assert!(texts.contains(&"x".to_owned()));
593        assert!(texts.contains(&"y".to_owned()));
594    }
595
596    #[test]
597    fn negative_lookahead_excludes_the_member() {
598        let (_prefixes, states) = walk(r"(?!x)\S", false, false).expect("walk");
599        // The single-char forbidden word drops 'x' from the available set.
600        for state in &states {
601            assert_ne!(state.text, "x");
602        }
603    }
604
605    #[test]
606    fn positive_lookahead_pends_the_witness() {
607        let (_prefixes, states) = walk(r"(?=xy)\S", false, false).expect("walk");
608        // The witness pends, the anchor consumes its first char.
609        assert_eq!(states.len(), 1);
610        assert_eq!(states[0].text, "x");
611        assert_eq!(states[0].pending, "y");
612    }
613
614    #[test]
615    fn capturing_groups_record_captures() {
616        let (_prefixes, states) = walk("(x)", false, false).expect("walk");
617        assert_eq!(states[0].text, "x");
618        assert_eq!(states[0].captures, vec![(1, "x".to_owned())]);
619    }
620
621    #[test]
622    fn backreferences_consume_the_captured_text() {
623        let (_prefixes, states) = walk(r"(xy)\1", false, false).expect("walk");
624        assert_eq!(states[0].text, "xyxy");
625    }
626
627    #[test]
628    fn a_backreference_without_a_capture_consumes_nothing() {
629        // The second alternative's `\1` never sees a captured group 1
630        // (the branch runs on states where the first branch never ran),
631        // so the backreference contributes no state.
632        let (_prefixes, states) = walk(r"(a)|\1", false, false).expect("walk");
633        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
634        assert_eq!(texts, vec!["a".to_owned()]);
635    }
636
637    #[test]
638    fn a_single_char_forbidden_word_narrows_the_available_set() {
639        // The multi-atom lookahead body pends the single-char witness as
640        // a forbidden word; consuming the next atom must subtract it from
641        // the available set, which kills the walk here.
642        let (_prefixes, states) = walk(r"(?!x$)x", false, false).expect("walk");
643        assert!(states.is_empty(), "{states:?}");
644    }
645
646    #[test]
647    fn a_multi_char_forbidden_word_stays_out_of_the_narrowing() {
648        // The multi-char witness is forbidden as a whole word, so the
649        // per-character narrowing does not subtract it from the available
650        // set and the following literal consumes normally.
651        let (_prefixes, states) = walk(r"(?!xy)z", false, false).expect("walk");
652        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
653        assert_eq!(texts, vec!["z".to_owned()]);
654    }
655
656    #[test]
657    fn conditional_groups_take_the_live_branch() {
658        let (_prefixes, states) = walk("(x)(?(1)y|z)", false, false).expect("walk");
659        assert_eq!(states[0].text, "xy");
660    }
661
662    #[test]
663    fn require_reachable_rejects_unresolved_repeat_sites() {
664        // "a*" dies before the unstarted `b*` site when the walk cannot
665        // produce states.
666        let error = walk(r"(?!)b*", false, true).expect_err("unresolved");
667        assert_eq!(
668            error.0,
669            "Pattern validation cannot resolve repeat-site prefixes"
670        );
671    }
672
673    #[test]
674    fn assertion_witness_budget_is_enforced() {
675        // A negative lookbehind whose prefix ends on the forbidden char
676        // cannot be resolved once captures are in play.
677        let error = walk(r"(x)(?<!x)a", false, false).expect_err("capture lookbehind");
678        assert_eq!(
679            error.0,
680            "Pattern validation cannot resolve capture-dependent lookbehind"
681        );
682    }
683
684    #[test]
685    fn unique_states_budget_is_enforced() {
686        // The state limit and text budget guards live in unique_states.
687        let many: Vec<RepeatPrefixState> = (0..=PREFIX_WALK_STATE_LIMIT)
688            .map(|index| RepeatPrefixState {
689                text: format!("s{index}"),
690                ..RepeatPrefixState::default()
691            })
692            .collect();
693        let error = unique_states(many).expect_err("state budget exceeded");
694        assert_eq!(
695            error.0,
696            "Pattern validation repeat-prefix state budget exceeded"
697        );
698        let long: Vec<RepeatPrefixState> = vec![RepeatPrefixState {
699            text: "a".repeat(PREFIX_WALK_TEXT_BUDGET + 1),
700            ..RepeatPrefixState::default()
701        }];
702        let error = unique_states(long).expect_err("text budget exceeded");
703        assert_eq!(
704            error.0,
705            "Pattern validation repeat-prefix text budget exceeded"
706        );
707    }
708
709    #[test]
710    fn repeat_reaching_prefixes_returns_deduped_prefixes() {
711        // Prefixes are the states recorded before each repeated site.
712        let prefixes =
713            repeat_reaching_prefixes("xa*b", Flags::default(), None, false).expect("prefixes");
714        assert!(prefixes.contains(&"x".to_owned()));
715        assert!(prefixes.iter().all(|prefix| !prefix.is_empty()));
716        let mut sorted = prefixes.clone();
717        sorted.sort();
718        sorted.dedup();
719        assert_eq!(prefixes.len(), sorted.len(), "deduped");
720    }
721
722    #[test]
723    fn repeat_reaching_prefixes_parse_failures_are_empty() {
724        let prefixes =
725            repeat_reaching_prefixes("[oops", Flags::default(), None, false).expect("empty");
726        assert!(prefixes.is_empty());
727    }
728
729    #[test]
730    fn check_deadline_maps_all_three_states() {
731        assert!(check_deadline(None).is_ok());
732        assert!(check_deadline(Some(Instant::now() + std::time::Duration::from_secs(60))).is_ok());
733        let error = check_deadline(Some(Instant::now() - std::time::Duration::from_secs(1)))
734            .expect_err("expired");
735        assert_eq!(
736            error.0,
737            "Pattern validation repeat-prefix construction exceeded its deadline"
738        );
739    }
740
741    #[test]
742    fn assertion_witnesses_reject_unresolved_nested_assertions() {
743        let (ops, _) = crate::redos::ast::parse("a(?=x)", Flags::default()).expect("parses");
744        let error = assertion_witnesses(&ops, Flags::default(), None).expect_err("pending");
745        assert_eq!(
746            error.0,
747            "Pattern validation cannot resolve nested assertion constraints"
748        );
749    }
750
751    #[test]
752    fn collected_prefixes_honor_the_candidate_budget() {
753        // Seed the collector past its budget; the very next collected
754        // repeat must trip the guard.
755        let (ops, flags) = crate::redos::ast::parse("x+", Flags::default()).expect("parses");
756        let mut prefixes = vec![String::new(); REPEAT_PREFIX_STATE_LIMIT * 4 + 1];
757        let mut walker = PrefixWalk {
758            flags,
759            prefixes: &mut prefixes,
760            deadline: None,
761            collect: true,
762            alphabet: Vec::new(),
763            repeat_collector: None,
764            canonical_optionals: true,
765            require_reachable: false,
766        };
767        let error = walker
768            .walk(&ops, vec![RepeatPrefixState::default()], false)
769            .expect_err("budget");
770        assert_eq!(
771            error.0,
772            "Pattern validation repeat-prefix candidate budget exceeded"
773        );
774    }
775
776    #[test]
777    fn a_fixed_repeat_of_a_state_unchanging_body_stops_early() {
778        // Anchors leave the walk state untouched, so the fixed-repeat loop
779        // breaks out instead of spinning to its bound.
780        let (_prefixes, states) = walk(r"(?:\b){2}x", false, false).expect("walk");
781        assert_eq!(states.len(), 1);
782        assert_eq!(states[0].text, "x");
783    }
784
785    #[test]
786    fn pending_assertion_states_are_replayed_after_a_repeat() {
787        // The lookahead defers pending states that the repeat loop must
788        // replay to keep the prefix language complete.
789        let (_prefixes, states) = walk(r"(?:x(?=y))+", true, false).expect("walk");
790        assert_eq!(states.len(), 1);
791        assert_eq!(states[0].text, "x");
792    }
793
794    #[test]
795    fn a_conditional_without_else_walks_an_empty_branch() {
796        // The group was never captured, so the conditional takes the
797        // missing else branch, which contributes nothing.
798        let (_prefixes, states) = walk(r"(?(1)b)x", false, false).expect("walk");
799        let texts: Vec<String> = states.iter().map(|s| s.text.clone()).collect();
800        assert_eq!(texts, vec!["x".to_owned()]);
801    }
802
803    #[test]
804    fn pending_replay_stops_at_a_fixed_point() {
805        // The lookahead's witness merges back into the pending state, so
806        // the replay loop detects no progress and stops.
807        let (_prefixes, states) = walk(r"(?:(?=x))+", true, false).expect("walk");
808        assert_eq!(states.len(), 1);
809    }
810
811    #[test]
812    fn an_unbounded_optional_repeat_replays_its_pending_states() {
813        // `(?=x)*y`: the low-0 repeat walks its body, the pending witness
814        // survives the canonical check (the lookahead pends), and the
815        // replay ladder resolves it before the following literal.
816        let (_prefixes, states) = walk(r"(?=x)*y", true, false).expect("walk");
817        assert_eq!(states.len(), 1);
818        assert_eq!(states[0].text, "y");
819    }
820
821    #[test]
822    fn a_bounded_optional_repeat_keeps_a_finite_replay_budget() {
823        // `{0,3}`: the low-0 walk pends the lookahead witness and the
824        // remaining-repeats budget for the replay ladder is the bounded
825        // high minus the already-taken zero passes.
826        let (_prefixes, states) = walk(r"(?:x(?=y)){0,3}z", true, false).expect("walk");
827        assert_eq!(states.len(), 1);
828        assert_eq!(states[0].text, "z");
829    }
830}