Skip to content

Commit 9d93fb8

Browse files
committed
fix: PINNED_BACKREFERENCE bugs from PR #94 review
Require group/backref pair to span the whole pattern, enforce group/separator length bounds in generated bytecode, and fix matchBounded to report absolute (not substring-relative) spans. Patterns with anchors outside the span now correctly fall through to VARIABLE_CAPTURE_BACKREF.
1 parent 343e776 commit 9d93fb8

11 files changed

Lines changed: 318 additions & 212 deletions

File tree

‎reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PatternAnalyzer.java‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3575,6 +3575,14 @@ private PinnedBackreferenceInfo detectPinnedBackreference(RegexNode ast) {
35753575
return null;
35763576
}
35773577

3578+
// The generated matcher only ever scans the group, the optional separator, and the
3579+
// backreference echo - it has no code path for anything else in the concatenation. A
3580+
// non-empty prefix/suffix (e.g. the \b anchors in \b(\w+)\s+\1\b) would therefore be silently
3581+
// ignored rather than evaluated, so require the group/backref pair to span the whole pattern.
3582+
if (groupIndex != 0 || backrefIndex != children.size() - 1) {
3583+
return null;
3584+
}
3585+
35783586
// The codegen forward-scans the group's content as a single flat "one-or-more chars from
35793587
// groupCharSet" loop with no notion of an upper bound, so only an unbounded (max == -1),
35803588
// greedy, min >= 1 quantifier directly wrapping a charset-bearing child is safe - this
@@ -3605,6 +3613,8 @@ private PinnedBackreferenceInfo detectPinnedBackreference(RegexNode ast) {
36053613
// earlier occurrence of the same backreference) is rejected rather than approximated.
36063614
RegexNode separator = null;
36073615
CharSet separatorCharSet = null;
3616+
int separatorMinLength = 0;
3617+
int separatorMaxLength = -1;
36083618
if (backrefIndex > groupIndex + 1) {
36093619
List<RegexNode> sepNodes = children.subList(groupIndex + 1, backrefIndex);
36103620
if (sepNodes.size() != 1) {
@@ -3613,6 +3623,27 @@ private PinnedBackreferenceInfo detectPinnedBackreference(RegexNode ast) {
36133623
separator = sepNodes.get(0);
36143624
separatorCharSet = getFirstCharSet(separator);
36153625

3626+
// The scan can't backtrack, so it always consumes the longest run of separator-charset
3627+
// chars available; that's only correct if the separator's quantifier bounds either equal
3628+
// that run (min <= run) or don't cap it below that run (max == -1 or max >= run). Track the
3629+
// bounds here so codegen can reject a candidate whose actual run falls outside them, instead
3630+
// of accepting whatever length the greedy scan happens to consume.
3631+
if (separator instanceof QuantifierNode) {
3632+
QuantifierNode sepQuant = (QuantifierNode) separator;
3633+
if (!sepQuant.greedy) {
3634+
return null;
3635+
}
3636+
separatorMinLength = sepQuant.min;
3637+
separatorMaxLength = sepQuant.max;
3638+
} else {
3639+
// No quantifier wrapping the separator node means exactly one occurrence.
3640+
separatorMinLength = 1;
3641+
separatorMaxLength = 1;
3642+
}
3643+
if (separatorMinLength < 1) {
3644+
return null;
3645+
}
3646+
36163647
// Disjointness condition 2: separator vs. group content.
36173648
if (separatorCharSet == null || !groupCharSet.isDisjoint(separatorCharSet)) {
36183649
return null;
@@ -3623,8 +3654,11 @@ private PinnedBackreferenceInfo detectPinnedBackreference(RegexNode ast) {
36233654
capturingGroup.groupNumber,
36243655
capturingGroup.child,
36253656
groupCharSet,
3657+
groupQuant.min,
36263658
separator,
3627-
separatorCharSet);
3659+
separatorCharSet,
3660+
separatorMinLength,
3661+
separatorMaxLength);
36283662
}
36293663

36303664
/** Check if a node represents a single character or character class. */

‎reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PinnedBackreferenceInfo.java‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class PinnedBackreferenceInfo implements PatternInfo {
4242
/** Character set the group's content matches. */
4343
public final CharSet groupCharSet;
4444

45+
/** Minimum number of chars the group's quantifier requires (e.g. 2 for {@code \w{2,}}). */
46+
public final int groupMinLength;
47+
4548
/**
4649
* Node(s) between the group's close and the backreference site, or null if the backreference
4750
* directly follows the group.
@@ -51,17 +54,34 @@ public class PinnedBackreferenceInfo implements PatternInfo {
5154
/** Character set of the separator, or null if there is no separator. */
5255
public final CharSet separatorCharSet;
5356

57+
/**
58+
* Minimum number of chars the separator's quantifier requires. Unused if there's no separator.
59+
*/
60+
public final int separatorMinLength;
61+
62+
/**
63+
* Maximum number of chars the separator's quantifier allows, or -1 if unbounded. Unused if
64+
* there's no separator.
65+
*/
66+
public final int separatorMaxLength;
67+
5468
public PinnedBackreferenceInfo(
5569
int groupIndex,
5670
RegexNode groupBody,
5771
CharSet groupCharSet,
72+
int groupMinLength,
5873
RegexNode separator,
59-
CharSet separatorCharSet) {
74+
CharSet separatorCharSet,
75+
int separatorMinLength,
76+
int separatorMaxLength) {
6077
this.groupIndex = groupIndex;
6178
this.groupBody = Objects.requireNonNull(groupBody);
6279
this.groupCharSet = Objects.requireNonNull(groupCharSet);
80+
this.groupMinLength = groupMinLength;
6381
this.separator = separator;
6482
this.separatorCharSet = separatorCharSet;
83+
this.separatorMinLength = separatorMinLength;
84+
this.separatorMaxLength = separatorMaxLength;
6585
}
6686

6787
/** Whether a separator exists between the group's close and the backreference site. */
@@ -75,8 +95,11 @@ public int structuralHashCode() {
7595
hash = 31 * hash + groupIndex;
7696
hash = 31 * hash + groupBody.getClass().getName().hashCode();
7797
hash = 31 * hash + groupCharSet.hashCode();
98+
hash = 31 * hash + groupMinLength;
7899
hash = 31 * hash + (separator != null ? separator.getClass().getName().hashCode() : 0);
79100
hash = 31 * hash + (separatorCharSet != null ? separatorCharSet.hashCode() : 0);
101+
hash = 31 * hash + separatorMinLength;
102+
hash = 31 * hash + separatorMaxLength;
80103
return hash;
81104
}
82105

0 commit comments

Comments
 (0)