Skip to main content

redos_detector/
downgrade_pattern.rs

1//! Rewrites unsupported patterns into supported ones.
2//!
3//! Three transforms run until the pattern stabilizes. A reference to a group in
4//! a positive lookahead is replaced by a non-capturing group holding that
5//! group's contents. A reference to a non-finite-size group is inlined the same
6//! way. A pattern with no start anchor gets `[^]*?` prepended, wrapping the rest
7//! in a non-capturing group when the root is a disjunction. A downgraded pattern
8//! may introduce false positives.
9
10use crate::arrays::drop_common;
11use crate::ast::{AnchorKind, GroupBehavior, NodeKind, RcNode};
12use crate::parse::parse;
13use std::collections::HashSet;
14
15/// A downgraded pattern and the offsets of any atomic groups it introduced.
16#[derive(Clone, Debug)]
17pub struct DowngradedRegexPattern {
18    /// Offsets of groups to treat as atomic.
19    pub atomic_group_offsets: HashSet<usize>,
20    /// The downgraded pattern.
21    pub pattern: String,
22}
23
24/// Returns whether each `range[0]` token a reference points to is atomic or optional.
25#[derive(Clone, Copy, PartialEq, Eq)]
26enum AtomicOrOptional {
27    Atomic,
28    Optional,
29    None,
30}
31
32struct Action {
33    atomic_or_optional: AtomicOrOptional,
34    group: RcNode,
35    reference: RcNode,
36    reference_start: usize,
37    reference_end: usize,
38}
39
40fn utf16_slice_replace(input: &str, replacement: &str, start: usize, end: usize) -> String {
41    let units: Vec<u16> = input.encode_utf16().collect();
42    let mut out: Vec<u16> = Vec::new();
43    out.extend_from_slice(&units[..start]);
44    out.extend(replacement.encode_utf16());
45    out.extend_from_slice(&units[end..]);
46    String::from_utf16_lossy(&out)
47}
48
49fn shift_offsets(offsets: &HashSet<usize>, after: i64, shift_amount: i64) -> HashSet<usize> {
50    offsets
51        .iter()
52        .map(|&offset| {
53            if (offset as i64) > after {
54                (offset as i64 + shift_amount) as usize
55            } else {
56                offset
57            }
58        })
59        .collect()
60}
61
62fn quantifier_iterations_to_string(node: &RcNode) -> String {
63    if let NodeKind::Quantifier {
64        min,
65        max,
66        greedy,
67        symbol,
68        ..
69    } = &node.kind
70    {
71        if let Some(sym) = symbol {
72            return format!("{}{}", sym, if *greedy { "" } else { "?" });
73        }
74        if max.map(|m| m == *min).unwrap_or(false) {
75            return format!("{{{}}}", min);
76        }
77        let max_str = max.map(|m| m.to_string()).unwrap_or_default();
78        return format!("{{{},{}}}{}", min, max_str, if *greedy { "" } else { "?" });
79    }
80    String::new()
81}
82
83/// The result of stripping capturing groups and lookarounds from a node.
84pub(crate) struct RawWithoutGroups {
85    pub(crate) references_with_offset: Vec<(RcNode, usize)>,
86    pub(crate) result: String,
87}
88
89/// Reconstructs `root`'s source with capturing and non-capturing groups turned
90/// into `(?:...)` and lookarounds removed, tracking reference offsets.
91pub(crate) fn get_raw_without_capturing_groups_or_lookaheads(root: &RcNode) -> RawWithoutGroups {
92    let mut references_with_offset: Vec<(RcNode, usize)> = Vec::new();
93    let result = walk(root, 0, &mut references_with_offset);
94    RawWithoutGroups {
95        references_with_offset,
96        result,
97    }
98}
99
100fn walk(node: &RcNode, offset: usize, refs: &mut Vec<(RcNode, usize)>) -> String {
101    let walk_all = |nodes: &[RcNode], start_offset: usize, refs: &mut Vec<(RcNode, usize)>| {
102        let mut result = String::new();
103        for n in nodes {
104            let len = result.encode_utf16().count();
105            result.push_str(&walk(n, start_offset + len, refs));
106        }
107        result
108    };
109
110    match &node.kind {
111        NodeKind::Anchor { .. }
112        | NodeKind::CharacterClass { .. }
113        | NodeKind::CharacterClassEscape { .. }
114        | NodeKind::UnicodePropertyEscape { .. }
115        | NodeKind::Value { .. }
116        | NodeKind::Dot => node.raw.clone(),
117        NodeKind::Reference { .. } => {
118            refs.push((node.clone(), offset));
119            node.raw.clone()
120        }
121        NodeKind::Group { behavior, body } => match behavior {
122            GroupBehavior::Normal | GroupBehavior::Ignore => {
123                format!("(?:{})", walk_all(body, offset + 3, refs))
124            }
125            _ => String::new(),
126        },
127        NodeKind::Disjunction { body } => {
128            let mut res = String::new();
129            for (i, child) in body.iter().enumerate() {
130                if i > 0 {
131                    res.push('|');
132                }
133                let len = res.encode_utf16().count();
134                res.push_str(&walk(child, offset + len, refs));
135            }
136            res
137        }
138        NodeKind::Alternative { body } => walk_all(body, offset, refs),
139        NodeKind::Quantifier { body, .. } => {
140            format!(
141                "{}{}",
142                walk(body, offset, refs),
143                quantifier_iterations_to_string(node)
144            )
145        }
146        NodeKind::CharacterClassRange { .. } => node.raw.clone(),
147    }
148}
149
150fn already_has_start_anchor_replacement(node: &RcNode) -> bool {
151    let body = match &node.kind {
152        NodeKind::Alternative { body } => body,
153        _ => return false,
154    };
155    if body.is_empty() {
156        return false;
157    }
158    let maybe_quantifier = &body[0];
159    let (min, max, greedy, q_body) = match &maybe_quantifier.kind {
160        NodeKind::Quantifier {
161            min,
162            max,
163            greedy,
164            body,
165            ..
166        } => (*min, *max, *greedy, body),
167        _ => return false,
168    };
169    if min != 0 || max.is_some() || greedy {
170        return false;
171    }
172    match &q_body.kind {
173        NodeKind::CharacterClass { negative, body } => body.is_empty() && *negative,
174        _ => false,
175    }
176}
177
178/// Returns whether the pattern is not bounded at the start.
179pub fn is_missing_start_anchor(root: &RcNode) -> bool {
180    if already_has_start_anchor_replacement(root) {
181        return false;
182    }
183    check_anchor(root) == AnchorCheck::ConsumingNode
184}
185
186#[derive(PartialEq, Eq, Clone, Copy)]
187enum AnchorCheck {
188    Anchor,
189    ConsumingNode,
190    Null,
191}
192
193fn check_anchor(node: &RcNode) -> AnchorCheck {
194    match &node.kind {
195        NodeKind::Anchor { kind } => {
196            if *kind == AnchorKind::Start {
197                AnchorCheck::Anchor
198            } else {
199                AnchorCheck::Null
200            }
201        }
202        NodeKind::Quantifier { max, body, min, .. } => {
203            if *max == Some(0) {
204                return AnchorCheck::Null;
205            }
206            let may_be_skipped = *min == 0;
207            check_children(std::slice::from_ref(body), may_be_skipped)
208        }
209        NodeKind::Alternative { body } | NodeKind::Group { body, .. } => {
210            check_children(body, false)
211        }
212        NodeKind::Disjunction { body } => {
213            let results: Vec<AnchorCheck> = body.iter().map(check_anchor).collect();
214            if results.contains(&AnchorCheck::ConsumingNode) {
215                AnchorCheck::ConsumingNode
216            } else if results.iter().all(|r| *r == AnchorCheck::Anchor) {
217                AnchorCheck::Anchor
218            } else {
219                AnchorCheck::Null
220            }
221        }
222        _ => AnchorCheck::ConsumingNode,
223    }
224}
225
226fn check_children(body: &[RcNode], may_be_skipped: bool) -> AnchorCheck {
227    for child in body {
228        let res = check_anchor(child);
229        if res == AnchorCheck::ConsumingNode {
230            return AnchorCheck::ConsumingNode;
231        }
232        if res == AnchorCheck::Anchor {
233            if !may_be_skipped {
234                return AnchorCheck::Anchor;
235            }
236            return AnchorCheck::Null;
237        }
238    }
239    AnchorCheck::Null
240}
241
242/// Downgrades `pattern` if needed.
243///
244/// Returns [`Error::Parse`](crate::Error::Parse) when the pattern fails to
245/// parse.
246pub fn downgrade_pattern(
247    pattern: &str,
248    unicode: bool,
249) -> Result<DowngradedRegexPattern, crate::Error> {
250    let mut last = DowngradedRegexPattern {
251        atomic_group_offsets: HashSet::new(),
252        pattern: pattern.to_string(),
253    };
254    loop {
255        let (need_rerun, result) = run(&last, unicode)?;
256        last = result;
257        if !need_rerun {
258            break;
259        }
260    }
261    Ok(last)
262}
263
264struct GroupEntry {
265    group: RcNode,
266    stack: Vec<RcNode>,
267}
268
269struct Walker {
270    actions: Vec<Action>,
271    needs_start_anchor_replacement: bool,
272    groups: std::collections::HashMap<u64, GroupEntry>,
273    infinite_groups: HashSet<usize>,
274    next_group_index: u64,
275}
276
277fn lookahead_only_contains_group(lookahead: &RcNode, group: &RcNode) -> bool {
278    if let NodeKind::Group { body, .. } = &lookahead.kind {
279        body.len() == 1 && body[0].id == group.id
280    } else {
281        false
282    }
283}
284
285fn run(
286    last: &DowngradedRegexPattern,
287    unicode: bool,
288) -> Result<(bool, DowngradedRegexPattern), crate::Error> {
289    let ast = parse(&last.pattern, unicode).map_err(|e| crate::Error::Parse(e.0))?;
290    let needs_wrapping_in_group = matches!(ast.kind, NodeKind::Disjunction { .. });
291
292    let mut walker = Walker {
293        actions: Vec::new(),
294        needs_start_anchor_replacement: false,
295        groups: std::collections::HashMap::new(),
296        infinite_groups: HashSet::new(),
297        next_group_index: 1,
298    };
299
300    let mut passed = false;
301    walk_downgrade(&ast, &[], &mut passed, None, &mut walker);
302
303    let mut new_pattern = last.pattern.clone();
304    let mut atomic_group_offsets = last.atomic_group_offsets.clone();
305    let mut need_to_rerun = false;
306
307    // Apply actions by descending reference start so earlier edits do not shift
308    // later offsets.
309    let mut order: Vec<usize> = (0..walker.actions.len()).collect();
310    order.sort_by(|&a, &b| {
311        walker.actions[b]
312            .reference_start
313            .cmp(&walker.actions[a].reference_start)
314    });
315
316    for &idx in &order {
317        let reference_start = walker.actions[idx].reference_start;
318        let reference_end = walker.actions[idx].reference_end;
319        let atomic_or_optional = walker.actions[idx].atomic_or_optional;
320        let raw = get_raw_without_capturing_groups_or_lookaheads(&walker.actions[idx].group);
321
322        if !raw.references_with_offset.is_empty() {
323            need_to_rerun = true;
324        }
325
326        let replacement = if atomic_or_optional == AtomicOrOptional::Optional {
327            format!("(?:{}?)", raw.result)
328        } else {
329            raw.result.clone()
330        };
331
332        new_pattern =
333            utf16_slice_replace(&new_pattern, &replacement, reference_start, reference_end);
334
335        let shift_amount =
336            replacement.encode_utf16().count() as i64 - (reference_end - reference_start) as i64;
337        atomic_group_offsets =
338            shift_offsets(&atomic_group_offsets, reference_start as i64, shift_amount);
339
340        if atomic_or_optional == AtomicOrOptional::Atomic {
341            atomic_group_offsets.insert(reference_start);
342        }
343
344        // For any other atomic action whose reference appears inside this
345        // result, add its inner offset.
346        for inner in &walker.actions {
347            if inner.atomic_or_optional == AtomicOrOptional::Atomic {
348                if let Some((_, offset)) = raw
349                    .references_with_offset
350                    .iter()
351                    .find(|(r, _)| r.id == inner.reference.id)
352                {
353                    atomic_group_offsets.insert(reference_start + offset);
354                }
355            }
356        }
357    }
358
359    if walker.needs_start_anchor_replacement && !already_has_start_anchor_replacement(&ast) {
360        if needs_wrapping_in_group {
361            new_pattern = format!("(?:{})", new_pattern);
362        }
363        new_pattern = format!("[^]*?{}", new_pattern);
364        let shift = if needs_wrapping_in_group { 8 } else { 5 };
365        atomic_group_offsets = shift_offsets(&atomic_group_offsets, -1, shift);
366    }
367
368    Ok((
369        need_to_rerun,
370        DowngradedRegexPattern {
371            atomic_group_offsets,
372            pattern: new_pattern,
373        },
374    ))
375}
376
377#[allow(clippy::too_many_arguments)]
378fn walk_downgrade(
379    node: &RcNode,
380    node_stack: &[RcNode],
381    passed_start_anchor: &mut bool,
382    immediately_preceding_lookahead: Option<&RcNode>,
383    walker: &mut Walker,
384) {
385    let on_consuming_node = |passed: &bool, walker: &mut Walker| {
386        if !passed {
387            walker.needs_start_anchor_replacement = true;
388        }
389    };
390
391    match &node.kind {
392        NodeKind::CharacterClass { .. }
393        | NodeKind::CharacterClassEscape { .. }
394        | NodeKind::UnicodePropertyEscape { .. }
395        | NodeKind::Value { .. }
396        | NodeKind::Dot => {
397            on_consuming_node(passed_start_anchor, walker);
398        }
399        NodeKind::Anchor { kind } => {
400            if *kind == AnchorKind::Start {
401                *passed_start_anchor = true;
402            }
403        }
404        NodeKind::Group { behavior, body } => {
405            let group_index = if *behavior == GroupBehavior::Normal {
406                let idx = walker.next_group_index;
407                walker.next_group_index += 1;
408                Some(idx)
409            } else {
410                None
411            };
412
413            let mut new_stack = node_stack.to_vec();
414            new_stack.push(node.clone());
415            walk_all_serial(body, &new_stack, passed_start_anchor, walker);
416
417            if let Some(idx) = group_index {
418                walker.groups.insert(
419                    idx,
420                    GroupEntry {
421                        group: node.clone(),
422                        stack: node_stack.to_vec(),
423                    },
424                );
425            }
426        }
427        NodeKind::Disjunction { body } => {
428            let mut new_stack = node_stack.to_vec();
429            new_stack.push(node.clone());
430            walk_all_parallel(body, &new_stack, passed_start_anchor, walker);
431        }
432        NodeKind::Alternative { body } => {
433            let mut new_stack = node_stack.to_vec();
434            new_stack.push(node.clone());
435            walk_all_serial(body, &new_stack, passed_start_anchor, walker);
436        }
437        NodeKind::Quantifier { min, max, body, .. } => {
438            if *max != Some(0) {
439                if max.is_none() {
440                    for stack_node in node_stack.iter().rev() {
441                        if let NodeKind::Group { behavior, .. } = &stack_node.kind {
442                            if behavior.is_lookaround() {
443                                break;
444                            }
445                            walker.infinite_groups.insert(stack_node.id);
446                        }
447                    }
448                }
449                let mut new_stack = node_stack.to_vec();
450                new_stack.push(node.clone());
451                if *min == 0 {
452                    let mut copy = *passed_start_anchor;
453                    walk_all_serial(std::slice::from_ref(body), &new_stack, &mut copy, walker);
454                } else {
455                    walk_all_serial(
456                        std::slice::from_ref(body),
457                        &new_stack,
458                        passed_start_anchor,
459                        walker,
460                    );
461                }
462            }
463        }
464        NodeKind::Reference { match_index } => {
465            on_consuming_node(passed_start_anchor, walker);
466            if let Some(entry) = walker.groups.get(match_index) {
467                let group = entry.group.clone();
468                let group_stack = entry.stack.clone();
469                let group_stack_ids: Vec<usize> = group_stack.iter().map(|n| n.id).collect();
470                let node_stack_ids: Vec<usize> = node_stack.iter().map(|n| n.id).collect();
471                let (local_ids, _) = drop_common(&group_stack_ids, &node_stack_ids);
472                let local_stack: Vec<RcNode> = group_stack
473                    .iter()
474                    .filter(|n| local_ids.contains(&n.id))
475                    .cloned()
476                    .collect();
477
478                let lookahead_stack: Vec<RcNode> = local_stack
479                    .iter()
480                    .filter(|n| {
481                        matches!(&n.kind, NodeKind::Group { behavior, .. } if behavior.is_lookaround())
482                    })
483                    .cloned()
484                    .collect();
485                let group_may_not_be_reached = local_stack.iter().any(|n| match &n.kind {
486                    NodeKind::Disjunction { .. } => true,
487                    NodeKind::Quantifier { min, .. } => *min == 0,
488                    _ => false,
489                });
490                let group_in_lookahead = !lookahead_stack.is_empty();
491                let group_could_be_set = lookahead_stack.iter().all(|n| {
492                    matches!(&n.kind, NodeKind::Group { behavior, .. } if !behavior.is_negative())
493                });
494
495                if group_could_be_set
496                    && (group_in_lookahead || walker.infinite_groups.contains(&group.id))
497                {
498                    let atomic = immediately_preceding_lookahead
499                        .map(|lh| lookahead_only_contains_group(lh, &group))
500                        .unwrap_or(false);
501                    let optional = group_in_lookahead && group_may_not_be_reached;
502                    let atomic_or_optional = if atomic {
503                        AtomicOrOptional::Atomic
504                    } else if optional {
505                        AtomicOrOptional::Optional
506                    } else {
507                        AtomicOrOptional::None
508                    };
509                    walker.actions.push(Action {
510                        atomic_or_optional,
511                        group,
512                        reference: node.clone(),
513                        reference_start: node.range.0,
514                        reference_end: node.range.1,
515                    });
516                }
517            }
518        }
519        NodeKind::CharacterClassRange { .. } => {}
520    }
521}
522
523fn walk_all_serial(
524    nodes: &[RcNode],
525    node_stack: &[RcNode],
526    passed_start_anchor: &mut bool,
527    walker: &mut Walker,
528) {
529    let mut just_had_lookahead: Option<RcNode> = None;
530    for expression in nodes {
531        walk_downgrade(
532            expression,
533            node_stack,
534            passed_start_anchor,
535            just_had_lookahead.as_ref(),
536            walker,
537        );
538        just_had_lookahead = match &expression.kind {
539            NodeKind::Group {
540                behavior: GroupBehavior::Lookahead,
541                ..
542            } => Some(expression.clone()),
543            _ => None,
544        };
545    }
546}
547
548fn walk_all_parallel(
549    nodes: &[RcNode],
550    node_stack: &[RcNode],
551    passed_start_anchor: &mut bool,
552    walker: &mut Walker,
553) {
554    for expression in nodes {
555        let mut copy = *passed_start_anchor;
556        walk_downgrade(expression, node_stack, &mut copy, None, walker);
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    fn missing(pattern: &str) -> bool {
565        let ast = parse(pattern, false).unwrap();
566        is_missing_start_anchor(&ast)
567    }
568
569    #[test]
570    fn is_missing_start_anchor_cases() {
571        assert!(!missing(""));
572        assert!(missing("a"));
573        assert!(!missing("^a"));
574        assert!(!missing("$^a"));
575        assert!(missing("$a"));
576        assert!(missing("a^a"));
577        assert!(!missing("a{0}^a"));
578        assert!(missing("a{0,1}^a"));
579        assert!(missing("a{1}^a"));
580        assert!(missing("^a|b"));
581        assert!(!missing("^a|^b"));
582        assert!(!missing("|"));
583        assert!(!missing("^(a|b)"));
584        assert!(!missing("^ab"));
585        assert!(missing("[ab]^c"));
586        assert!(missing("(^)?c"));
587        assert!(!missing("(^){1}c"));
588        assert!(missing("[^]*a"));
589        assert!(!missing("[^]*?a"));
590        assert!(missing("[^a]*?a"));
591        assert!(missing("[]*?a"));
592        assert!(missing("b*?a"));
593    }
594
595    fn raw(pattern: &str) -> (String, Vec<(String, usize)>) {
596        let ast = parse(pattern, false).unwrap();
597        let result = get_raw_without_capturing_groups_or_lookaheads(&ast);
598        let refs: Vec<(String, usize)> = result
599            .references_with_offset
600            .iter()
601            .map(|(node, offset)| (node.raw.clone(), *offset))
602            .collect();
603        (result.result, refs)
604    }
605
606    #[test]
607    fn get_raw_simple() {
608        let (result, refs) = raw("(a)");
609        assert_eq!(result, "(?:a)");
610        assert!(refs.is_empty());
611    }
612
613    #[test]
614    fn get_raw_every_node_type() {
615        let (result, refs) =
616            raw("^(a)b{1}c+d{1,2}e+(?:f)(?=g)(?!h)(?<=i)(?<!j)(k|l).(m(n))o+?[a\\d]\\1$");
617        assert_eq!(
618            result,
619            "^(?:a)b{1}c+d{1,2}e+(?:f)(?:k|l).(?:m(?:n))o+?[a\\d]\\1$"
620        );
621        assert_eq!(refs, vec![("\\1".to_string(), 51)]);
622    }
623
624    #[test]
625    fn get_raw_multi_reference() {
626        let (result, refs) = raw("()()()()a(\\1)c(d|\\2|f)(?:\\3)(?:\\4)+");
627        assert_eq!(
628            result,
629            "(?:)(?:)(?:)(?:)a(?:\\1)c(?:d|\\2|f)(?:\\3)(?:\\4)+"
630        );
631        assert_eq!(
632            refs,
633            vec![
634                ("\\1".to_string(), 20),
635                ("\\2".to_string(), 29),
636                ("\\3".to_string(), 37),
637                ("\\4".to_string(), 43),
638            ]
639        );
640    }
641}