Skip to content

Commit 99c8a22

Browse files
Fix regex backrefs and string-join empty items
Fix regex backrefs and string-join empty items
2 parents c4eee0b + 476aa2f commit 99c8a22

4 files changed

Lines changed: 168 additions & 0 deletions

File tree

src/main/java/org/rumbledb/runtime/functions/strings/RegexPatternUtils.java

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.rumbledb.exceptions.InvalidRegexFlagException;
2525
import org.rumbledb.exceptions.InvalidRegexPatternException;
2626

27+
import java.util.ArrayDeque;
2728
import java.util.ArrayList;
29+
import java.util.Deque;
2830
import java.util.List;
2931
import java.util.regex.Matcher;
3032
import java.util.regex.Pattern;
@@ -66,6 +68,7 @@ public static CompiledRegex compileRegex(String pattern, String flagsString, Exc
6668
}
6769
}
6870
}
71+
validateXQueryRegex(pattern, quote, metadata);
6972
if (quote) {
7073
pattern = Pattern.quote(pattern);
7174
}
@@ -83,6 +86,105 @@ public static CompiledRegex compileRegex(String pattern, String flagsString, Exc
8386
}
8487
}
8588

89+
private static void validateXQueryRegex(String pattern, boolean quote, ExceptionMetadata metadata) {
90+
if (quote) {
91+
return;
92+
}
93+
94+
int nextCaptureGroupNumber = 1;
95+
Deque<GroupContext> openGroups = new ArrayDeque<>();
96+
for (int i = 0; i < pattern.length(); i++) {
97+
char current = pattern.charAt(i);
98+
if (current == '[') {
99+
i = skipCharacterClass(pattern, i);
100+
continue;
101+
}
102+
if (current == '\\') {
103+
if (i + 1 >= pattern.length()) {
104+
continue;
105+
}
106+
char next = pattern.charAt(i + 1);
107+
if (Character.isDigit(next)) {
108+
int end = i + 1;
109+
while (end < pattern.length() && Character.isDigit(pattern.charAt(end))) {
110+
end++;
111+
}
112+
validateBackReference(
113+
pattern.substring(i, end),
114+
pattern.substring(i + 1, end),
115+
openGroups,
116+
nextCaptureGroupNumber,
117+
metadata
118+
);
119+
i = end - 1;
120+
continue;
121+
}
122+
if ((next == 'p' || next == 'P') && i + 2 < pattern.length() && pattern.charAt(i + 2) == '{') {
123+
i = skipUnicodeEscape(pattern, i);
124+
continue;
125+
}
126+
i++;
127+
continue;
128+
}
129+
if (current == '(') {
130+
boolean capturing = !(i + 1 < pattern.length() && pattern.charAt(i + 1) == '?');
131+
if (capturing) {
132+
openGroups.push(new GroupContext(nextCaptureGroupNumber));
133+
nextCaptureGroupNumber++;
134+
} else {
135+
openGroups.push(GroupContext.nonCapturingGroup());
136+
}
137+
continue;
138+
}
139+
if (current == ')' && !openGroups.isEmpty()) {
140+
openGroups.pop();
141+
}
142+
}
143+
}
144+
145+
private static void validateBackReference(
146+
String token,
147+
String groupNumberText,
148+
Deque<GroupContext> openGroups,
149+
int nextCaptureGroupNumber,
150+
ExceptionMetadata metadata
151+
) {
152+
if (groupNumberText.isEmpty() || groupNumberText.charAt(0) == '0') {
153+
throw new InvalidRegexPatternException("Invalid back-reference " + token, metadata);
154+
}
155+
156+
int longestExistingPrefixLength = findLongestExistingBackReferencePrefixLength(
157+
groupNumberText,
158+
nextCaptureGroupNumber
159+
);
160+
if (longestExistingPrefixLength == 0) {
161+
throw new InvalidRegexPatternException("Invalid back-reference " + token, metadata);
162+
}
163+
int referencedGroupNumber = Integer.parseInt(groupNumberText.substring(0, longestExistingPrefixLength));
164+
165+
for (GroupContext groupContext : openGroups) {
166+
if (groupContext.isCapturing() && groupContext.getNumber() == referencedGroupNumber) {
167+
throw new InvalidRegexPatternException("Invalid back-reference " + token, metadata);
168+
}
169+
}
170+
}
171+
172+
private static int findLongestExistingBackReferencePrefixLength(
173+
String groupNumberText,
174+
int nextCaptureGroupNumber
175+
) {
176+
long referencedGroupNumber = 0;
177+
int longestExistingPrefixLength = 0;
178+
for (int i = 0; i < groupNumberText.length(); i++) {
179+
referencedGroupNumber = referencedGroupNumber * 10 + Character.digit(groupNumberText.charAt(i), 10);
180+
if (referencedGroupNumber >= nextCaptureGroupNumber) {
181+
break;
182+
}
183+
longestExistingPrefixLength = i + 1;
184+
}
185+
return longestExistingPrefixLength;
186+
}
187+
86188
public static boolean matchesEmptyString(Pattern pattern) {
87189
return hasZeroLengthMatch(pattern, "")
88190
|| hasZeroLengthMatch(pattern, "a")
@@ -425,6 +527,33 @@ private static int skipUnicodeEscape(String pattern, int startIndex) {
425527
return Math.min(index, pattern.length() - 1);
426528
}
427529

530+
private static int skipCharacterClass(String pattern, int startIndex) {
531+
int index = startIndex + 1;
532+
boolean firstToken = true;
533+
while (index < pattern.length()) {
534+
char current = pattern.charAt(index);
535+
if (current == '\\') {
536+
if (index + 1 >= pattern.length()) {
537+
return index;
538+
}
539+
if (
540+
index + 2 < pattern.length()
541+
&& (pattern.charAt(index + 1) == 'p' || pattern.charAt(index + 1) == 'P')
542+
&& pattern.charAt(index + 2) == '{'
543+
) {
544+
index = skipUnicodeEscape(pattern, index);
545+
} else {
546+
index++;
547+
}
548+
} else if (current == ']' && !firstToken) {
549+
return index;
550+
}
551+
firstToken = false;
552+
index++;
553+
}
554+
return pattern.length() - 1;
555+
}
556+
428557
private static boolean isAsciiUppercase(int codePoint) {
429558
return codePoint >= 'A' && codePoint <= 'Z';
430559
}
@@ -498,4 +627,30 @@ private EscapedToken(String text, int endIndex) {
498627
this.endIndex = endIndex;
499628
}
500629
}
630+
631+
private static final class GroupContext {
632+
private final boolean capturing;
633+
private final int number;
634+
635+
private GroupContext(boolean capturing, int number) {
636+
this.capturing = capturing;
637+
this.number = number;
638+
}
639+
640+
private GroupContext(int number) {
641+
this(true, number);
642+
}
643+
644+
private static GroupContext nonCapturingGroup() {
645+
return new GroupContext(false, -1);
646+
}
647+
648+
private boolean isCapturing() {
649+
return this.capturing;
650+
}
651+
652+
private int getNumber() {
653+
return this.number;
654+
}
655+
}
501656
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(:JIQS: ShouldRun; Output="(#a#, #r#c#d#r#)" :)
2+
string-join(("", "a", ""), "#"),
3+
string-join(tokenize("abracadabra", "(ab)|(a)"), "#")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(:JIQS: ShouldCrash; ErrorCode="FORX0002"; ErrorMetadata="LINE:2:COLUMN:0:" :)
2+
replace("abcd", "(a)\\2(b)", "")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(:JIQS: ShouldRun; Output="true" :)
2+
matches("$$9", "(.)\\19")
3+
and matches("#abc#1", "^(#)abc\\11$")
4+
and matches("$$9", "(((((((((((.)))))))))))\\119")
5+
and matches(
6+
"abcdefghiabcdefghia0a1",
7+
"(a)(b)(c)(d)(e)(f)(g)(h)(i)\\1\\2\\3\\4\\5\\6\\7\\8\\9\\10\\11"
8+
)

0 commit comments

Comments
 (0)