Skip to content

Commit a2f6202

Browse files
Merge pull request #27 from cloudlinux/def-57621-rx-prefilter-fix
fix(operators): keep the @rx prefilter from rejecting what the regex matches
2 parents 9b5a14e + da267d3 commit a2f6202

3 files changed

Lines changed: 380 additions & 82 deletions

File tree

‎internal/operators/rxprefilter.go‎

Lines changed: 78 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -529,12 +529,13 @@ func extractLiterals(re *syntax.Regexp, ci bool) interface{} {
529529
// select|sleep|substr → s(?:elect|leep|ubstr)
530530
// AST: OpConcat([OpLiteral("s"), OpAlternate([...])])
531531
//
532-
// It prepends the raw prefix literal to every element returned by
533-
// rawExtractSuffixes, recovering the full literal words. The result is returned
534-
// as anyRequired so the caller knows "at least one of these must be present".
532+
// It prepends the raw prefix literal to every string that leadingLiterals says
533+
// the alternation must begin with, recovering the full literal words. The result
534+
// is returned as anyRequired so the caller knows "at least one of these must be
535+
// present".
535536
//
536-
// Handles arbitrary nesting depth: rawExtractSuffixes recurses back into
537-
// extractLiterals → trieReconstruct for inner OpConcat nodes, so patterns like
537+
// Handles arbitrary nesting depth: leadingLiterals recurses through nested
538+
// alternations and concatenations, so patterns like
538539
//
539540
// select|set|sleep → s(?:e(?:lect|t)|leep)
540541
//
@@ -555,100 +556,104 @@ func trieReconstruct(concat *syntax.Regexp, ci bool) anyRequired {
555556
return nil
556557
}
557558

558-
// Collect suffix candidates from remaining children using the permissive
559-
// extractor that allows short literals (they will be combined with prefix).
560-
var suffixes []string
561-
for _, sub := range concat.Sub[1:] {
562-
lits := rawExtractSuffixes(sub, ci)
563-
if lits == nil {
564-
continue
565-
}
566-
suffixes = append(suffixes, lits...)
567-
}
559+
// The second child directly follows the prefix, so every match of the
560+
// concat contains prefix+s for one of the strings s that the child must
561+
// begin with. Only text at the very start of the child can be glued to the
562+
// prefix: a literal further in (after a star, an optional group, a class...)
563+
// is not adjacent to it and would make a phantom string, e.g. the "60;" of
564+
// &#0*60; must not become "#60;" because the input "<" has no such
565+
// substring.
566+
suffixes := leadingLiterals(concat.Sub[1], ci)
568567
if len(suffixes) == 0 {
569568
return nil
570569
}
571570

572571
result := make(anyRequired, 0, len(suffixes))
573572
for _, s := range suffixes {
574-
if full := prefix + s; len(full) >= 2 {
575-
result = append(result, full)
573+
full := prefix + s
574+
if len(full) < 2 {
575+
// Cannot happen (both parts are non-empty), but never drop an
576+
// element of an anyRequired set: that would reject its matches.
577+
return nil
576578
}
577-
}
578-
if len(result) == 0 {
579-
return nil
579+
result = append(result, full)
580580
}
581581
return result
582582
}
583583

584-
// rawExtractSuffixes extracts literal candidates from a regex subtree for use
585-
// as trie-reconstruction suffixes. Unlike extractLiterals it permits short
586-
// literals (< 2 bytes) because they will be combined with a prefix by the
587-
// caller, producing a longer and more selective string.
584+
// maxLeadingLiterals bounds the number of strings leadingLiterals builds.
585+
const maxLeadingLiterals = anyRequiredMaxN
586+
587+
// leadingLiterals returns a set of non-empty strings such that every match of
588+
// re begins with at least one of them, or nil when it cannot say. Unlike
589+
// extractLiterals it permits short literals (< 2 bytes), because the caller
590+
// (trieReconstruct) combines them with a prefix into a longer string.
588591
//
589-
// Returns nil if any branch of an alternation has no extractable literal (same
590-
// semantics as extractLiterals for OpAlternate: we can't safely omit a branch).
591-
func rawExtractSuffixes(re *syntax.Regexp, ci bool) []string {
592+
// The strings are anchored at the start of the match. That is what makes it safe
593+
// to glue them to a literal that directly precedes re: nothing may be skipped
594+
// between the two. A node that can consume other text first (a star, an optional
595+
// or class, a zero-width assertion) stops the walk, and so does anything that
596+
// is not a fixed string, after contributing what it knows its match starts with.
597+
//
598+
// An alternation needs a leading set from every branch; one branch without it
599+
// makes the whole alternation nil, as in extractLiterals, because that branch
600+
// could match without any of the strings.
601+
func leadingLiterals(re *syntax.Regexp, ci bool) []string {
592602
switch re.Op {
593603
case syntax.OpLiteral:
594-
s := rawLiteral(re, ci)
595-
if s == "" {
596-
return nil
604+
if s := rawLiteral(re, ci); s != "" {
605+
return []string{s}
597606
}
598-
return []string{s}
607+
return nil
608+
609+
case syntax.OpCapture, syntax.OpPlus:
610+
return leadingLiterals(re.Sub[0], ci)
611+
612+
case syntax.OpRepeat:
613+
if re.Min >= 1 {
614+
return leadingLiterals(re.Sub[0], ci)
615+
}
616+
return nil
599617

600618
case syntax.OpAlternate:
601-
var result []string
619+
var out []string
602620
for _, sub := range re.Sub {
603-
lits := rawExtractSuffixes(sub, ci)
621+
lits := leadingLiterals(sub, ci)
604622
if lits == nil {
605-
return nil // one branch has no extractable suffix → bail
623+
return nil
624+
}
625+
out = append(out, lits...)
626+
if len(out) > maxLeadingLiterals {
627+
return nil
606628
}
607-
result = append(result, lits...)
608629
}
609-
return result
630+
return out
610631

611632
case syntax.OpConcat:
612-
// Try the full extractLiterals pipeline first (handles deeper nesting
613-
// through the trieReconstruct fallback it already calls).
614-
lits := extractLiterals(re, ci)
615-
if lits != nil {
616-
switch v := lits.(type) {
617-
case allRequired:
618-
// Only safe to return a single trie suffix when the concat
619-
// collapses to exactly one contiguous literal. Multiple
620-
// allRequired elements mean there are wildcards between them
621-
// (e.g. "elect.*from" → allRequired{"elect","from"}). Joining
622-
// them would produce "electfrom" — a phantom string that never
623-
// appears contiguously in a real input — causing false negatives
624-
// on valid matches like "select x from". Return nil here so the
625-
// caller falls back to the safer anyRequired propagation instead.
626-
if len(v) == 1 {
627-
return []string{v[0]}
628-
}
629-
return nil
630-
case anyRequired:
631-
return []string(v)
632-
case combinedRequired:
633-
// For trie-reconstruction we need a suffix that is *always* present
634-
// when this sub-concat fires. The .all elements are guaranteed;
635-
// .any elements are only conditionally present (one of them must be
636-
// present, but not a specific one). Returning a .any element would
637-
// let the outer prefix combine with a wrong suffix (e.g. "s"+"execute"
638-
// instead of "s"+"p_"+"execute" → "sp_execute"), producing a phantom
639-
// literal that never appears contiguously in real input.
640-
// Return the single longest .all element as the guaranteed suffix.
641-
rep := longest([]string(v.all))
642-
if rep == "" {
643-
return nil
633+
// Start from the empty string and extend it child by child. A fixed
634+
// literal lets the walk continue to the next child; any other child
635+
// ends it after contributing its own leading strings.
636+
acc := []string{""}
637+
for _, sub := range re.Sub {
638+
lits := leadingLiterals(sub, ci)
639+
if lits == nil || len(acc)*len(lits) > maxLeadingLiterals {
640+
break
641+
}
642+
next := make([]string, 0, len(acc)*len(lits))
643+
for _, a := range acc {
644+
for _, l := range lits {
645+
next = append(next, a+l)
644646
}
645-
return []string{rep}
647+
}
648+
acc = next
649+
if sub.Op != syntax.OpLiteral {
650+
break
646651
}
647652
}
648-
return nil
649-
650-
case syntax.OpCapture:
651-
return rawExtractSuffixes(re.Sub[0], ci)
653+
if len(acc) == 1 && acc[0] == "" {
654+
return nil
655+
}
656+
return acc
652657

653658
default:
654659
return nil

0 commit comments

Comments
 (0)