1use 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 #[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
182fn 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 #[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#[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
260fn 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
344fn 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
392pub 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
402pub(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
434pub 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 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 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 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 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 let units = class_intersection_probe_units(r"\d{3}\d{3}", Flags::default(), None, true)
565 .expect("builders");
566 assert!(!units.is_empty());
567 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 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 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 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 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 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 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 assert_eq!(
732 class_intersection_probe_units("a+(?=a)ab+", flags, None, false).expect("units"),
733 Vec::<(String, String)>::new()
734 );
735 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 assert_eq!(
747 class_intersection_probe_units("a+a+", flags, None, false).expect("units"),
748 vec![("a".to_owned(), "\0".to_owned())]
749 );
750 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 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 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 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 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 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 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 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 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 assert!(!should_stop);
874 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 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 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 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 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 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 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}