Skip to main content

guard_core_engine/redos/
class_intersection.rs

1//! Cross-atom class-intersection probe units.
2//!
3//! Port of the reference `_redos_class_intersection.py`. Fill
4//! confirmation is derived from the parsed intervals (the reference
5//! re-verifies with `re.fullmatch` predicates built from the same parse,
6//! so the two can never disagree there).
7
8use super::child::run_child_request;
9use super::exact_state::{
10    exact_overlap_fill_raw as exact_overlap_fill, isolated_group_exact_state,
11    narrow_exact_state_raw as narrow_exact_state,
12};
13use super::intervals::IntervalSet;
14use super::parse_slots::{NonPairingSlot, PairingAtom, Slot, pattern_slots};
15use super::stray_chooser::{
16    StrayContext, build_stray_context, choose_class_intersection_stray_with,
17};
18use super::timeout::BuilderTimeout;
19
20const MAX_GROUP_CROSSING_DEPTH: usize = super::exact_state::MAX_GROUP_CROSSING_DEPTH;
21
22fn crossing_slot_narrows(slot: &Slot, shared: &IntervalSet, depth: usize) -> Option<IntervalSet> {
23    match slot {
24        Slot::Pairing(atom) => {
25            if atom.allows_zero {
26                return Some(shared.clone());
27            }
28            let overlap = shared.intersection(&atom.intervals);
29            if overlap.is_empty() {
30                None
31            } else {
32                Some(overlap)
33            }
34        }
35        Slot::NonPairing(non) => {
36            if !non.is_boundary {
37                return Some(shared.clone());
38            }
39            let inner = non.inner.as_ref()?;
40            crossing_group_result(inner, shared, depth + 1).map(|_| shared.clone())
41        }
42    }
43}
44
45fn alternative_crossing(
46    alt_slots: &[Slot],
47    shared: &IntervalSet,
48    depth: usize,
49) -> Option<IntervalSet> {
50    let mut local = shared.clone();
51    for slot in alt_slots {
52        let narrowed = crossing_slot_narrows(slot, &local, depth)?;
53        local = narrowed;
54    }
55    Some(local)
56}
57
58fn crossing_group_result(
59    alternatives: &[Vec<Slot>],
60    shared: &IntervalSet,
61    depth: usize,
62) -> Option<IntervalSet> {
63    if depth > MAX_GROUP_CROSSING_DEPTH {
64        return None;
65    }
66    let mut combined: Option<IntervalSet> = None;
67    for alt in alternatives {
68        let Some(result) = alternative_crossing(alt, shared, depth) else {
69            continue;
70        };
71        combined = Some(match combined {
72            None => result,
73            Some(existing) => existing.union(&result),
74        });
75    }
76    combined
77}
78
79struct CrossedNonPairing {
80    shared: IntervalSet,
81    fill: Option<String>,
82    group_state: Option<IntervalSet>,
83}
84
85fn cross_non_pairing_slot(
86    slot: &NonPairingSlot,
87    shared: &IntervalSet,
88    exact_state: Option<&IntervalSet>,
89) -> Option<CrossedNonPairing> {
90    let Some(inner) = &slot.inner else {
91        if slot.is_boundary {
92            return None;
93        }
94        return Some(CrossedNonPairing {
95            shared: shared.clone(),
96            fill: None,
97            group_state: None,
98        });
99    };
100    let crossing = crossing_group_result(inner, shared, 0);
101    let group_state = isolated_group_exact_state(inner, 0);
102    if crossing.is_none() {
103        let fill = if slot.unbounded {
104            exact_overlap_fill(exact_state, group_state.as_ref()).map(String::from)
105        } else {
106            None
107        };
108        if slot.is_boundary {
109            // unreachable: a failed crossing implies every alternative is
110            // disjoint from `shared`, and `exact_state` equals `shared`
111            // here, so the exact fill is always `None` on this path
112            #[cfg(not(coverage))]
113            {
114                let fill = fill?;
115                return Some(CrossedNonPairing {
116                    shared: IntervalSet::empty(),
117                    fill: Some(fill),
118                    group_state,
119                });
120            }
121            #[cfg(coverage)]
122            {
123                let _ = fill;
124                return None;
125            }
126        }
127        return Some(CrossedNonPairing {
128            shared: shared.clone(),
129            fill,
130            group_state,
131        });
132    }
133    let fill = if slot.unbounded {
134        crossing
135            .as_ref()
136            .and_then(|set| set.first_member())
137            .and_then(char::from_u32)
138            .map(String::from)
139    } else {
140        None
141    };
142    let result_shared = if slot.is_boundary {
143        crossing.expect("crossing checked above")
144    } else {
145        shared.clone()
146    };
147    Some(CrossedNonPairing {
148        shared: result_shared,
149        fill,
150        group_state,
151    })
152}
153
154fn tail_pairing_intervals(slots: &[Slot], start: usize) -> Vec<IntervalSet> {
155    slots[start..]
156        .iter()
157        .filter_map(|slot| match slot {
158            Slot::Pairing(atom) => Some(atom.intervals.clone()),
159            Slot::NonPairing(_) => None,
160        })
161        .collect()
162}
163
164fn fill_confirmed(left: &PairingAtom, right: &PairingAtom, fill: &str) -> bool {
165    let Some(first) = fill.chars().next() else {
166        return false;
167    };
168    left.intervals.contains(u32::from(first)) && right.intervals.contains(u32::from(first))
169}
170
171fn left_confirms_fill(left: &PairingAtom, fill: &str) -> bool {
172    fill.chars()
173        .next()
174        .is_some_and(|first| left.intervals.contains(u32::from(first)))
175}
176
177struct ChainUnit {
178    fill: String,
179    stray: String,
180}
181
182/// The pairing-chain unit appender: verifies the fill's stray in the
183/// killable child before recording the unit (the runner is injected so
184/// tests can force stray-verification failures deterministically).
185fn append_pairing_unit_with(
186    units: &mut Vec<ChainUnit>,
187    left: &PairingAtom,
188    right: &PairingAtom,
189    fill: &str,
190    tail: &[IntervalSet],
191    ctx: &StrayContext,
192    run: crate::redos::cost_arbiter::ChildRunner<'_>,
193) -> Result<(), BuilderTimeout> {
194    // unreachable: `exact_state` starts at the left atom's intervals and
195    // every later narrowing keeps it inside `left`, so a fill that
196    // survived the exact check is always a member of `left`
197    #[cfg(not(coverage))]
198    if !fill_confirmed(left, right, fill) {
199        return Ok(());
200    }
201    #[cfg(coverage)]
202    let _ = fill_confirmed(left, right, fill);
203    let stray = choose_class_intersection_stray_with(
204        ctx,
205        fill,
206        &left.intervals,
207        &right.intervals,
208        tail,
209        run,
210    )?;
211    units.push(ChainUnit {
212        fill: fill.to_owned(),
213        stray,
214    });
215    Ok(())
216}
217
218/// The pairing-chain stepper: the child runner is injected so tests can
219/// force stray-verification failures deterministically.
220#[allow(clippy::too_many_arguments)]
221fn advance_pairing_chain_with(
222    units: &mut Vec<ChainUnit>,
223    left: &PairingAtom,
224    shared: &IntervalSet,
225    slot: &PairingAtom,
226    exact_state: &Option<IntervalSet>,
227    tail: &[IntervalSet],
228    ctx: &StrayContext,
229    run: crate::redos::cost_arbiter::ChildRunner<'_>,
230) -> Result<(IntervalSet, Option<IntervalSet>, bool), BuilderTimeout> {
231    let overlap = shared.intersection(&slot.intervals);
232    if !overlap.is_empty() {
233        if slot.unbounded
234            && let Some(member) = overlap.first_member()
235            && let Some(fill) = char::from_u32(member)
236        {
237            append_pairing_unit_with(units, left, slot, &fill.to_string(), tail, ctx, run)?;
238        }
239        if !slot.allows_zero {
240            return Ok((
241                overlap,
242                narrow_exact_state(exact_state.as_ref(), Some(&slot.intervals)),
243                false,
244            ));
245        }
246        return Ok((shared.clone(), exact_state.clone(), false));
247    }
248    let Some(exact_fill) = exact_overlap_fill(exact_state.as_ref(), Some(&slot.intervals)) else {
249        return Ok((shared.clone(), exact_state.clone(), !slot.allows_zero));
250    };
251    if slot.unbounded {
252        append_pairing_unit_with(units, left, slot, &exact_fill.to_string(), tail, ctx, run)?;
253    }
254    if !slot.allows_zero {
255        return Ok((overlap, None, false));
256    }
257    Ok((shared.clone(), exact_state.clone(), false))
258}
259
260/// The per-start-slot unit walk: the child runner is injected so tests
261/// can force stray-verification failures deterministically.
262fn pairing_units_from_with(
263    slots: &[Slot],
264    start: usize,
265    ctx: &StrayContext,
266    run: crate::redos::cost_arbiter::ChildRunner<'_>,
267) -> Result<Vec<ChainUnit>, BuilderTimeout> {
268    let Slot::Pairing(left) = &slots[start] else {
269        return Ok(Vec::new());
270    };
271    let mut shared = left.intervals.clone();
272    let mut exact_state: Option<IntervalSet> = Some(left.intervals.clone());
273    let mut units: Vec<ChainUnit> = Vec::new();
274    for index in start + 1..slots.len() {
275        let slot = &slots[index];
276        let tail = tail_pairing_intervals(slots, index + 1);
277        match slot {
278            Slot::NonPairing(non) => {
279                let Some(crossed) = cross_non_pairing_slot(non, &shared, exact_state.as_ref())
280                else {
281                    break;
282                };
283                shared = crossed.shared;
284                if non.is_boundary {
285                    exact_state =
286                        narrow_exact_state(exact_state.as_ref(), crossed.group_state.as_ref());
287                }
288                if let Some(fill) = crossed.fill
289                    && left_confirms_fill(left, &fill)
290                {
291                    let stray = choose_class_intersection_stray_with(
292                        ctx,
293                        &fill,
294                        &left.intervals,
295                        &shared,
296                        &tail,
297                        run,
298                    )?;
299                    units.push(ChainUnit { fill, stray });
300                }
301            }
302            Slot::Pairing(pairing) => {
303                let (new_shared, new_exact, should_stop) = advance_pairing_chain_with(
304                    &mut units,
305                    left,
306                    &shared,
307                    pairing,
308                    &exact_state,
309                    &tail,
310                    ctx,
311                    run,
312                )?;
313                shared = new_shared;
314                exact_state = new_exact;
315                if should_stop {
316                    break;
317                }
318            }
319        }
320    }
321    Ok(units)
322}
323
324fn flatten_alternatives(alternatives: &[Vec<Slot>]) -> Vec<Slot> {
325    if alternatives.len() == 1 {
326        return alternatives[0].clone();
327    }
328    let mut flat: Vec<Slot> = Vec::new();
329    for (index, alt) in alternatives.iter().enumerate() {
330        if index > 0 {
331            flat.push(Slot::NonPairing(NonPairingSlot {
332                is_boundary: true,
333                inner: None,
334                unbounded: false,
335                max_repeat: None,
336                variable_bounded: false,
337            }));
338        }
339        flat.extend(alt.iter().cloned());
340    }
341    flat
342}
343
344/// The slot walker: the child runner is injected so tests can force
345/// stray-verification failures deterministically.
346fn units_in_slots_with(
347    slots: &[Slot],
348    ctx: &StrayContext,
349    run: crate::redos::cost_arbiter::ChildRunner<'_>,
350) -> Result<Vec<ChainUnit>, BuilderTimeout> {
351    let mut units: Vec<ChainUnit> = Vec::new();
352    for (index, slot) in slots.iter().enumerate() {
353        match slot {
354            Slot::NonPairing(non) => {
355                if let Some(inner) = &non.inner {
356                    let flat = flatten_alternatives(inner);
357                    units.extend(units_in_slots_with(&flat, ctx, run)?);
358                }
359            }
360            Slot::Pairing(atom) => {
361                if atom.unbounded {
362                    units.extend(pairing_units_from_with(slots, index, ctx, run)?);
363                }
364            }
365        }
366    }
367    Ok(units)
368}
369
370fn include_bounded_repeats(slots: &[Slot]) -> Vec<Slot> {
371    slots
372        .iter()
373        .map(|slot| match slot {
374            Slot::Pairing(atom) => Slot::Pairing(PairingAtom {
375                unbounded: atom.unbounded || atom.max_repeat.is_some_and(|max| max > 1),
376                ..atom.clone()
377            }),
378            Slot::NonPairing(non) => Slot::NonPairing(NonPairingSlot {
379                inner: non.inner.as_ref().map(|inner| {
380                    inner
381                        .iter()
382                        .map(|alt| include_bounded_repeats(alt))
383                        .collect()
384                }),
385                unbounded: non.unbounded || non.max_repeat.is_some_and(|max| max > 1),
386                ..non.clone()
387            }),
388        })
389        .collect()
390}
391
392/// Reference `_class_intersection_probe_units`.
393pub fn class_intersection_probe_units(
394    pattern: &str,
395    flags: super::ast::Flags,
396    ctx: Option<&StrayContext>,
397    include_bounded: bool,
398) -> Result<Vec<(String, String)>, BuilderTimeout> {
399    class_intersection_probe_units_with(pattern, flags, ctx, include_bounded, &run_child_request)
400}
401
402/// The pure half of [`class_intersection_probe_units`]: the child runner
403/// is injected so tests can force stray-verification failures
404/// deterministically.
405pub(crate) fn class_intersection_probe_units_with(
406    pattern: &str,
407    flags: super::ast::Flags,
408    ctx: Option<&StrayContext>,
409    include_bounded: bool,
410    run: crate::redos::cost_arbiter::ChildRunner<'_>,
411) -> Result<Vec<(String, String)>, BuilderTimeout> {
412    let Some(slots) = pattern_slots(pattern, flags) else {
413        return Ok(Vec::new());
414    };
415    let owned_ctx;
416    let ctx = match ctx {
417        Some(ctx) => ctx,
418        None => {
419            owned_ctx = build_stray_context(pattern, flags, None);
420            &owned_ctx
421        }
422    };
423    let slots = if include_bounded {
424        include_bounded_repeats(&slots)
425    } else {
426        slots
427    };
428    Ok(units_in_slots_with(&slots, ctx, run)?
429        .into_iter()
430        .map(|unit| (unit.fill, unit.stray))
431        .collect())
432}
433
434/// Reference `_class_intersection_fills`.
435pub fn class_intersection_fills(
436    pattern: &str,
437    flags: super::ast::Flags,
438) -> Result<Vec<String>, BuilderTimeout> {
439    Ok(class_intersection_probe_units(pattern, flags, None, false)?
440        .into_iter()
441        .map(|(fill, _stray)| fill)
442        .collect())
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use std::time::Instant;
449
450    use crate::redos::ast::Flags;
451    use crate::redos::parse_slots::{NonPairingSlot, PairingAtom};
452
453    /// Destructure a non-pairing slot; panics on any other variant.
454    fn expect_non_pairing(slot: &Slot) -> &NonPairingSlot {
455        match slot {
456            Slot::NonPairing(non) => non,
457            other => panic!("expected non-pairing, got {other:?}"),
458        }
459    }
460
461    /// Destructure a pairing slot; panics on any other variant.
462    fn expect_pairing(slot: &Slot) -> &PairingAtom {
463        match slot {
464            Slot::Pairing(atom) => atom,
465            other => panic!("expected pairing, got {other:?}"),
466        }
467    }
468
469    fn im_flags() -> Flags {
470        Flags::ignorecase_multiline()
471    }
472
473    #[test]
474    fn event_handler_pair_finds_whitespace_and_slash_fills() {
475        let pattern = r"(?:<[^<>]*[\s/]+on\w+\s*=)";
476        let fills = class_intersection_fills(pattern, im_flags()).expect("builders");
477        assert!(!fills.is_empty());
478        assert!(fills.iter().all(|c| " \t\n\r\u{b}\u{c}/".contains(c)));
479    }
480
481    #[test]
482    fn disjoint_adjacent_classes_produce_no_fill() {
483        assert_eq!(
484            class_intersection_fills("[a-c]+[x-z]+", Flags::default()).expect("builders"),
485            Vec::<String>::new()
486        );
487    }
488
489    #[test]
490    fn alternation_boundaries_block_pairing() {
491        assert_eq!(
492            class_intersection_fills("[a-z]+|[a-z]+", Flags::default()).expect("builders"),
493            Vec::<String>::new()
494        );
495    }
496
497    #[test]
498    fn zero_admitting_middles_are_crossed() {
499        // Fills are interval first-members: tab (0x09) sorts below space
500        // (0x20), so the \s intersection fill is a tab.
501        let first = "\t".to_owned();
502        assert_eq!(
503            class_intersection_fills(r"'\s*[\);]*\s*--", Flags::default()).expect("builders"),
504            vec![first.clone()]
505        );
506        assert_eq!(
507            class_intersection_fills(r"'\s*(?:ab)*\s*--", Flags::default()).expect("builders"),
508            vec![first]
509        );
510    }
511
512    #[test]
513    fn mandatory_middles_block_or_cross_by_overlap() {
514        assert_eq!(
515            class_intersection_fills(r"\s*[\);]+\s*", Flags::default()).expect("builders"),
516            Vec::<String>::new()
517        );
518        // One fill per unbounded repeat site the walk can start from.
519        assert_eq!(
520            class_intersection_fills(r"'\s*(?:\s+)\s*--", Flags::default()).expect("builders"),
521            vec!["\t".to_owned(), "\t".to_owned(), "\t".to_owned()]
522        );
523    }
524
525    #[test]
526    fn multi_char_alternation_group_crossing() {
527        assert_eq!(
528            class_intersection_fills(r"^[c-w]*(?:[g-z][g-z]|[g-z][g-z][g-z])*$", Flags::default())
529                .expect("builders"),
530            vec!["g".to_owned()]
531        );
532        assert_eq!(
533            class_intersection_fills(r"^[a-f]*(?:[g-z][g-z]|[g-z][g-z][g-z])*$", Flags::default())
534                .expect("builders"),
535            Vec::<String>::new()
536        );
537    }
538
539    #[test]
540    fn ignorecase_folds_disjoint_case_pairs() {
541        assert_eq!(
542            class_intersection_probe_units(r"[a-z]*[A-Z]+", Flags::default(), None, false)
543                .expect("builders"),
544            Vec::<(String, String)>::new()
545        );
546        let units = class_intersection_probe_units(r"[a-z]*[A-Z]+", im_flags(), None, false)
547            .expect("builders");
548        assert!(!units.is_empty());
549        assert!(units[0].0 == "a" || units[0].0 == "A");
550    }
551
552    #[test]
553    fn parse_failures_yield_no_units() {
554        assert_eq!(
555            class_intersection_probe_units("[oops", Flags::default(), None, false)
556                .expect("builders"),
557            Vec::<(String, String)>::new()
558        );
559    }
560
561    #[test]
562    fn include_bounded_rewrites_bounded_repeats() {
563        // \d{3}\d{3} only crosses once the bounded repeats read unbounded.
564        let units = class_intersection_probe_units(r"\d{3}\d{3}", Flags::default(), None, true)
565            .expect("builders");
566        assert!(!units.is_empty());
567        // The rewrite also descends into grouped alternatives and marks
568        // their bounded digits unbounded.
569        let slots = pattern_slots_for(r"x(\d{2,3})+y");
570        let rewritten = include_bounded_repeats(&slots);
571        let non = expect_non_pairing(&rewritten[1]);
572        assert!(non.unbounded);
573        let inner = non.inner.as_ref().expect("inner").last().expect("branch");
574        let atom = expect_pairing(&inner[0]);
575        assert!(atom.unbounded);
576    }
577
578    #[test]
579    fn a_succeeding_boundary_crossing_maps_back_to_the_shared_set() {
580        // A nested boundary whose own crossing overlaps the shared set
581        // keeps the shared set unchanged (the success maps onto it).
582        let nested = Slot::NonPairing(NonPairingSlot {
583            is_boundary: true,
584            inner: Some(vec![vec![Slot::Pairing(PairingAtom {
585                intervals: IntervalSet::single(u32::from('a')),
586                allows_zero: false,
587                unbounded: false,
588                max_repeat: None,
589                variable_bounded: false,
590            })]]),
591            unbounded: true,
592            max_repeat: None,
593            variable_bounded: false,
594        });
595        let shared = IntervalSet::single(u32::from('a'));
596        let narrowed = crossing_slot_narrows(&nested, &shared, 0).expect("crossing");
597        assert_eq!(narrowed, shared);
598    }
599
600    #[test]
601    fn crossing_helpers_reject_past_the_depth_cap() {
602        let atom = Slot::Pairing(PairingAtom {
603            intervals: IntervalSet::single(u32::from('a')),
604            allows_zero: false,
605            unbounded: false,
606            max_repeat: None,
607            variable_bounded: false,
608        });
609        let alternatives = vec![vec![atom]];
610        assert_eq!(
611            crossing_group_result(&alternatives, &IntervalSet::single(u32::from('a')), 999),
612            None
613        );
614    }
615
616    #[test]
617    fn tail_pairing_intervals_collect_only_pairing_atoms_from_start() {
618        let slots = pattern_slots_for("a[b]c(d)");
619        let tail = tail_pairing_intervals(&slots, 1);
620        assert_eq!(tail.len(), 2);
621        assert_eq!(tail[0], IntervalSet::single(u32::from('b')));
622        assert_eq!(tail[1], IntervalSet::single(u32::from('c')));
623        assert!(tail_pairing_intervals(&slots, slots.len()).is_empty());
624    }
625
626    #[test]
627    fn fill_confirmation_requires_interval_membership_on_both_sides() {
628        let left = PairingAtom {
629            intervals: IntervalSet::single(u32::from('a')),
630            allows_zero: false,
631            unbounded: false,
632            max_repeat: None,
633            variable_bounded: false,
634        };
635        let right = left.clone();
636        assert!(fill_confirmed(&left, &right, "a"));
637        assert!(!fill_confirmed(&left, &right, "b"));
638        assert!(!fill_confirmed(&left, &right, ""));
639        assert!(left_confirms_fill(&left, "a"));
640        assert!(!left_confirms_fill(&left, "b"));
641    }
642
643    fn pattern_slots_for(pattern: &str) -> Vec<Slot> {
644        crate::redos::parse_slots::pattern_slots(pattern, Flags::default()).expect("pattern parses")
645    }
646
647    #[test]
648    fn pairing_units_need_a_pairing_start_slot() {
649        let ctx = build_stray_context("a*z", Flags::default(), None);
650        let slots = vec![Slot::NonPairing(NonPairingSlot {
651            is_boundary: true,
652            inner: None,
653            unbounded: false,
654            max_repeat: None,
655            variable_bounded: false,
656        })];
657        let units = pairing_units_from_with(&slots, 0, &ctx, &run_child_request).expect("walk");
658        assert!(units.is_empty());
659    }
660
661    #[test]
662    fn flatten_alternatives_inserts_boundaries_between_branches() {
663        let atom = |c: char| {
664            Slot::Pairing(PairingAtom {
665                intervals: IntervalSet::single(u32::from(c)),
666                allows_zero: false,
667                unbounded: false,
668                max_repeat: None,
669                variable_bounded: false,
670            })
671        };
672        let flat = flatten_alternatives(&[vec![atom('a')], vec![atom('b')]]);
673        assert_eq!(flat.len(), 3);
674        assert!(matches!(&flat[1], Slot::NonPairing(non) if non.is_boundary));
675        assert_eq!(flatten_alternatives(&[vec![atom('a')]]).len(), 1);
676    }
677
678    #[test]
679    fn include_bounded_marks_bounded_group_alternations_unbounded() {
680        // A bounded quantifier on an alternation group makes the
681        // non-pairing slot count as repeatable for the crossing walk.
682        let slots = pattern_slots_for(r"a+(?:ab|cd){2,3}");
683        let rewritten = include_bounded_repeats(&slots);
684        let non = expect_non_pairing(&rewritten[1]);
685        assert!(non.unbounded);
686    }
687
688    #[test]
689    fn bounded_flag_propagates_into_nested_groups() {
690        let slots = pattern_slots_for(r"(\d{2})+");
691        let rewritten = include_bounded_repeats(&slots);
692        let non = expect_non_pairing(&rewritten[0]);
693        assert!(non.unbounded);
694        let inner = non.inner.as_ref().expect("inner").last().expect("branch");
695        let atom = expect_pairing(&inner[0]);
696        assert!(atom.unbounded);
697    }
698
699    #[test]
700    fn zero_width_lookahead_crossings_stay_transparent() {
701        let flags = Flags::default();
702        // The lookahead's `a*` allows zero width, so the crossing keeps the
703        // shared set untouched and the chain still confirms its fill.
704        assert_eq!(
705            class_intersection_probe_units("a+(?=a*)a+", flags, None, false).expect("units"),
706            vec![("a".to_owned(), "\0".to_owned())]
707        );
708        // A nested boundary inside the crossing recurses one level deeper.
709        assert_eq!(
710            class_intersection_probe_units("a+(?=(?=b)c)a+", flags, None, false).expect("units"),
711            vec![("a".to_owned(), "\0".to_owned())]
712        );
713        // A nested boundary whose own crossing succeeds keeps the shared
714        // set (the crossing result is mapped back onto it).
715        assert_eq!(
716            class_intersection_probe_units("a+(?=(?=a)a)a+", flags, None, false).expect("units"),
717            vec![("a".to_owned(), "\0".to_owned())]
718        );
719    }
720
721    #[test]
722    fn boundary_crossings_map_their_outcomes() {
723        let flags = Flags::default();
724        // A crossing whose inner class cannot overlap the shared set is
725        // unbounded and misses, so it confirms nothing.
726        assert_eq!(
727            class_intersection_probe_units("a+(?=b)a+b", flags, None, false).expect("units"),
728            vec![("a".to_owned(), "\0".to_owned())]
729        );
730        // A non-unbounded boundary crossing never yields a fill.
731        assert_eq!(
732            class_intersection_probe_units("a+(?=a)ab+", flags, None, false).expect("units"),
733            Vec::<(String, String)>::new()
734        );
735        // A bounded group is invisible to the default (unbounded-only) walk.
736        assert_eq!(
737            class_intersection_probe_units("a+(a*)b+", flags, None, false).expect("units"),
738            Vec::<(String, String)>::new()
739        );
740    }
741
742    #[test]
743    fn pairing_chains_and_alternation_groups_produce_units() {
744        let flags = Flags::default();
745        // Two overlapping unbounded classes confirm their shared fill.
746        assert_eq!(
747            class_intersection_probe_units("a+a+", flags, None, false).expect("units"),
748            vec![("a".to_owned(), "\0".to_owned())]
749        );
750        // A transparent group between pairings crosses as a non-boundary.
751        assert_eq!(
752            class_intersection_probe_units("a+(?:a|b)+b", flags, None, false).expect("units"),
753            vec![("a".to_owned(), "\0".to_owned())]
754        );
755        // A quantified boundary crossing stays unconfirmed.
756        assert_eq!(
757            class_intersection_probe_units("a+(?=b)*a+b", flags, None, false).expect("units"),
758            vec![("a".to_owned(), "\0".to_owned())]
759        );
760        // Negated classes chain two independent fill units.
761        assert_eq!(
762            class_intersection_probe_units("a+[^x]*b+", flags, None, false).expect("units"),
763            vec![
764                ("a".to_owned(), "\0".to_owned()),
765                ("b".to_owned(), "x".to_owned()),
766            ]
767        );
768    }
769
770    #[test]
771    fn boundary_crossings_recurse_into_their_alternatives() {
772        let flags = Flags::default();
773        // A repeat-wrapped group is a hard boundary for the crossing; the
774        // crossing walks its alternatives one level deeper and the chain
775        // still confirms its fill through the surviving overlap.
776        assert_eq!(
777            class_intersection_probe_units("a+(?=(?:b|c)+)a+", flags, None, false).expect("units"),
778            vec![("a".to_owned(), "\0".to_owned())]
779        );
780        // A boundary group that cannot overlap the shared set stops the
781        // chain: the crossing fails and no exact fill can rescue it.
782        assert_eq!(
783            class_intersection_probe_units("a+(?:b)+c+", flags, None, false).expect("units"),
784            Vec::<(String, String)>::new()
785        );
786    }
787
788    #[test]
789    fn the_exact_fill_path_appends_units_when_the_overlap_is_empty() {
790        let flags = Flags::default();
791        let ctx = build_stray_context("a+a+", flags, None);
792        let slots = pattern_slots("a+a+", flags).expect("slots");
793        let left = expect_pairing(&slots[0]);
794        let slot = expect_pairing(&slots[1]);
795        // An empty overlap with a live exact fill still appends the unit.
796        let (shared, exact, should_stop) = advance_pairing_chain_with(
797            &mut Vec::new(),
798            left,
799            &IntervalSet::empty(),
800            slot,
801            &Some(left.intervals.clone()),
802            &[],
803            &ctx,
804            &run_child_request,
805        )
806        .expect("advance");
807        assert!(shared.is_empty());
808        assert_eq!(exact, None);
809        // The exhausted exact state does not stop the walk on its own.
810        assert!(!should_stop);
811    }
812
813    #[test]
814    #[should_panic(expected = "expected non-pairing")]
815    fn expect_non_pairing_rejects_other_variants() {
816        let _ = expect_non_pairing(&Slot::Pairing(PairingAtom {
817            intervals: IntervalSet::empty(),
818            allows_zero: false,
819            unbounded: false,
820            max_repeat: None,
821            variable_bounded: false,
822        }));
823    }
824
825    #[test]
826    #[should_panic(expected = "expected pairing")]
827    fn expect_pairing_rejects_other_variants() {
828        let _ = expect_pairing(&Slot::NonPairing(NonPairingSlot {
829            is_boundary: false,
830            inner: None,
831            unbounded: false,
832            max_repeat: None,
833            variable_bounded: false,
834        }));
835    }
836
837    #[test]
838    fn multi_atom_boundary_groups_cross_and_fail_together() {
839        let flags = Flags::default();
840        // A repeat-wrapped two-atom group is a hard boundary whose body
841        // cannot overlap the shared set: the crossing fails and the chain
842        // stops without units.
843        assert_eq!(
844            class_intersection_probe_units("a+(?:bc)+d+", flags, None, false).expect("units"),
845            Vec::<(String, String)>::new()
846        );
847    }
848
849    #[test]
850    fn advance_pairing_chain_maps_bounded_and_zero_admitting_slots() {
851        let flags = Flags::default();
852        let ctx = build_stray_context("aa", flags, None);
853        let slots = pattern_slots("aa", flags).expect("slots");
854        let left = expect_pairing(&slots[0]);
855        // A bounded slot with a live exact fill appends its unit and stops
856        // the chain on its mandatory membership.
857        let slots_bounded = pattern_slots("a[ab]", flags).expect("slots");
858        let bounded = expect_pairing(&slots_bounded[1]);
859        let (shared, exact, should_stop) = advance_pairing_chain_with(
860            &mut Vec::new(),
861            left,
862            &IntervalSet::empty(),
863            bounded,
864            &Some(left.intervals.clone()),
865            &[],
866            &ctx,
867            &run_child_request,
868        )
869        .expect("advance");
870        assert!(shared.is_empty());
871        assert_eq!(exact, None);
872        // The exhausted exact state does not stop the walk on its own.
873        assert!(!should_stop);
874        // A zero-admitting slot with an empty overlap leaves everything
875        // untouched and lets the walk continue.
876        let slots_zero = pattern_slots("aa*", flags).expect("slots");
877        let zero = expect_pairing(&slots_zero[1]);
878        let (shared, exact, should_stop) = advance_pairing_chain_with(
879            &mut Vec::new(),
880            left,
881            &IntervalSet::empty(),
882            zero,
883            &Some(left.intervals.clone()),
884            &[],
885            &ctx,
886            &run_child_request,
887        )
888        .expect("advance");
889        assert!(shared.is_empty());
890        assert!(exact.is_some());
891        assert!(!should_stop);
892    }
893
894    #[test]
895    fn an_expired_deadline_surfaces_through_the_unit_chain() {
896        let ctx = build_stray_context(
897            "a+a+",
898            Flags::default(),
899            Some(Instant::now() - std::time::Duration::from_secs(1)),
900        );
901        let slots = pattern_slots("a+a+", Flags::default()).expect("slots");
902        let left = expect_pairing(&slots[0]);
903        let slot = expect_pairing(&slots[1]);
904        let error = advance_pairing_chain_with(
905            &mut Vec::new(),
906            left,
907            &left.intervals.clone(),
908            slot,
909            &Some(left.intervals.clone()),
910            &[],
911            &ctx,
912            &run_child_request,
913        )
914        .expect_err("expired");
915        assert_eq!(
916            error.0,
917            "Pattern validation probe construction exceeded its deadline"
918        );
919    }
920
921    #[test]
922    fn an_expired_deadline_surfaces_through_both_chain_paths() {
923        let ctx = build_stray_context(
924            "a+a+",
925            Flags::default(),
926            Some(Instant::now() - std::time::Duration::from_secs(1)),
927        );
928        // The pairing-advance path propagates the builder error.
929        let error = class_intersection_probe_units("a+a+", Flags::default(), Some(&ctx), false)
930            .expect_err("expired");
931        assert_eq!(
932            error.0,
933            "Pattern validation probe construction exceeded its deadline"
934        );
935        // The non-pairing fill path propagates it the same way.
936        let error =
937            class_intersection_probe_units("a+(?:a|b)+b+", Flags::default(), Some(&ctx), false)
938                .expect_err("expired");
939        assert_eq!(
940            error.0,
941            "Pattern validation probe construction exceeded its deadline"
942        );
943    }
944
945    /// A child runner whose every stray-verification dispatch times out.
946    fn timing_out_runner() -> crate::redos::cost_arbiter::ChildRunner<'static> {
947        &|_request: &crate::redos::child::ChildRequest,
948          _timeout: f64|
949         -> Result<crate::redos::child::ChildOutcome, crate::redos::child::ChildSpawnError> {
950            Err(crate::redos::child::ChildSpawnError::Timeout)
951        }
952    }
953
954    #[test]
955    fn the_injected_child_failure_propagates_through_the_pairing_chain() {
956        // `a+a+` confirms its fill through advance_pairing_chain, so the
957        // injected child failure surfaces as the mapped stray-verification
958        // timeout instead of a unit list.
959        let ctx = build_stray_context("a+a+", Flags::default(), None);
960        let error = class_intersection_probe_units_with(
961            "a+a+",
962            Flags::default(),
963            Some(&ctx),
964            false,
965            timing_out_runner(),
966        )
967        .expect_err("injected child failure");
968        assert_eq!(
969            error.0,
970            "Pattern validation stray verification exceeded its \
971             killable-subprocess timeout"
972        );
973    }
974
975    #[test]
976    fn the_injected_child_failure_propagates_through_the_group_crossing() {
977        // The `(?:a|b)+` crossing confirms its fill on the non-pairing
978        // path; the same injected failure surfaces there too.
979        let ctx = build_stray_context("a+(?:a|b)+b", Flags::default(), None);
980        let error = class_intersection_probe_units_with(
981            "a+(?:a|b)+b",
982            Flags::default(),
983            Some(&ctx),
984            false,
985            timing_out_runner(),
986        )
987        .expect_err("injected child failure");
988        assert_eq!(
989            error.0,
990            "Pattern validation stray verification exceeded its \
991             killable-subprocess timeout"
992        );
993        // A parse failure still short-circuits before any child dispatch.
994        assert!(
995            class_intersection_probe_units_with(
996                "[oops",
997                Flags::default(),
998                None,
999                false,
1000                timing_out_runner(),
1001            )
1002            .expect("no units")
1003            .is_empty()
1004        );
1005    }
1006}