2424import org .rumbledb .exceptions .InvalidRegexFlagException ;
2525import org .rumbledb .exceptions .InvalidRegexPatternException ;
2626
27+ import java .util .ArrayDeque ;
2728import java .util .ArrayList ;
29+ import java .util .Deque ;
2830import java .util .List ;
2931import java .util .regex .Matcher ;
3032import 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}
0 commit comments