Skip to content

Commit 8114779

Browse files
authored
Merge pull request #1564 from WebFuzzing/regex-charclass-intersection
Java regex char class intersection, union (nesting) and subtraction.
2 parents 4102985 + 4587390 commit 8114779

5 files changed

Lines changed: 167 additions & 9 deletions

File tree

core/src/main/antlr4/org/evomaster/core/parser/RegexJava.g4

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,23 @@ patternCharacter
189189
| BRACE_close
190190
| BRACKET_close
191191
| COLON
192+
| DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on.
192193
;
193194

194195

195196
characterClass
196-
//TODO check if lookahead needed, or implicit in rule order resoution
197-
//[ [lookahead ∉ {^}] ClassRanges ]
198-
: BRACKET_open CARET classRanges BRACKET_close
199-
| BRACKET_open classRanges BRACKET_close
200-
;
197+
: BRACKET_open CARET classContents BRACKET_close
198+
| BRACKET_open classContents BRACKET_close
199+
;
200+
201+
classContents
202+
: classUnion (DOUBLE_AMPERSAND classUnion)*
203+
;
204+
205+
classUnion
206+
: characterClass+ // one or more nested classes = UNION
207+
| classRanges // bare ranges
208+
;
201209

202210
classRanges
203211
:
@@ -261,6 +269,10 @@ atomEscape
261269
//------ LEXER ------------------------------
262270
// Lexer rules have first letter in upper-case
263271

272+
DOUBLE_AMPERSAND
273+
: '&&'
274+
;
275+
264276
DecimalDigit
265277
: [0-9]
266278
;

core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.evomaster.core.parser
33
import org.evomaster.core.search.gene.Gene
44
import org.evomaster.core.search.gene.regex.*
55
import org.evomaster.core.utils.CharacterRange
6+
import org.evomaster.core.utils.MultiCharacterRange
67
import org.evomaster.core.utils.ParsedFlagExpression
78
import org.evomaster.core.utils.RegexFlags
89

@@ -398,11 +399,44 @@ class GeneRegexJavaVisitor : RegexJavaBaseVisitor<VisitResult>(){
398399

399400
val negated = ctx.CARET() != null
400401

401-
val ranges = ctx.classRanges().accept(this).data as List<CharacterRange>
402+
val innerMultiCharRanges = ctx.classContents().accept(this).data as MultiCharacterRange
402403

403-
val gene = CharacterRangeRxGene(negated, ranges, currentFlags)
404+
val multiCharRanges = MultiCharacterRange(negated, innerMultiCharRanges)
404405

405-
return VisitResult(gene)
406+
return if (ctx.parent is RegexJavaParser.AtomContext){
407+
// top level character class, create gene
408+
VisitResult(CharacterRangeRxGene(multiCharRanges, currentFlags))
409+
} else {
410+
// nested char class, set MultiCharacterRange as data
411+
VisitResult(data = multiCharRanges)
412+
}
413+
}
414+
415+
override fun visitClassContents(ctx: RegexJavaParser.ClassContentsContext): VisitResult {
416+
417+
// intersect the unions of ranges
418+
val mcr = ctx.classUnion()
419+
.map { it.accept(this).data as MultiCharacterRange }
420+
.reduce { acc, item -> MultiCharacterRange.intersect(acc, item) }
421+
422+
return VisitResult(data=mcr)
423+
}
424+
425+
override fun visitClassUnion(ctx: RegexJavaParser.ClassUnionContext): VisitResult {
426+
427+
return if (ctx.characterClass().isNotEmpty()) {
428+
// union of char classes
429+
val mcr = ctx.characterClass()
430+
.map { it.accept(this).data as MultiCharacterRange }
431+
.reduce { acc, item -> MultiCharacterRange.union(acc, item) }
432+
433+
VisitResult(data=mcr)
434+
} else {
435+
// single classRanges
436+
val ranges = ctx.classRanges().accept(this).data as List<CharacterRange>
437+
438+
VisitResult(data=MultiCharacterRange(false, ranges))
439+
}
406440
}
407441

408442
override fun visitClassRanges(ctx: RegexJavaParser.ClassRangesContext): VisitResult {

core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import org.evomaster.core.utils.MultiCharacterRange
1515
import org.evomaster.core.utils.RegexFlags
1616
import org.slf4j.LoggerFactory
1717

18-
class CharacterRangeRxGene private constructor(
18+
class CharacterRangeRxGene(
1919
/**
2020
* this represents the valid ranges for a character class, removing overlaps and applying negation
2121
*/

core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ class MultiCharacterRange internal constructor(val ranges: List<CharacterRange>)
1616
return MultiCharacterRange(negated, characters.map { CharacterRange(it, it) })
1717
}
1818

19+
operator fun invoke(negated: Boolean, multiCharRange: MultiCharacterRange): MultiCharacterRange {
20+
return if (negated) {
21+
MultiCharacterRange(true, multiCharRange.ranges)
22+
} else {
23+
multiCharRange
24+
}
25+
}
26+
1927
operator fun invoke(negated: Boolean, ranges: List<CharacterRange>): MultiCharacterRange {
2028
if (ranges.isEmpty()) {
2129
throw IllegalArgumentException("No defined ranges")
@@ -93,6 +101,87 @@ class MultiCharacterRange internal constructor(val ranges: List<CharacterRange>)
93101
}
94102
}.toMutableList()
95103
}
104+
105+
/**
106+
* Create an intersection from two [org.evomaster.core.utils.MultiCharacterRange] instances
107+
* Used to allow character class intersections (e.g.: `[a-z0-9&&[0-9A-Z]]`).
108+
*/
109+
fun intersect(a: MultiCharacterRange, b: MultiCharacterRange): MultiCharacterRange {
110+
val result = mutableListOf<CharacterRange>()
111+
112+
var idxA = 0
113+
var idxB = 0
114+
115+
val lenA = a.size
116+
val lenB = b.size
117+
118+
while (idxA < lenA && idxB < lenB) {
119+
val start = maxOf(a[idxA].start, b[idxB].start)
120+
val end = minOf(a[idxA].end, b[idxB].end)
121+
122+
if (start <= end) {
123+
result.add(CharacterRange(start, end))
124+
}
125+
126+
if ( a[idxA].end < b[idxB].end ) {
127+
idxA++
128+
} else {
129+
idxB++
130+
}
131+
}
132+
133+
return MultiCharacterRange(result)
134+
}
135+
136+
/**
137+
* Creates a union from two [MultiCharacterRange] instances, merging overlapping
138+
* and adjacent ranges into a single normalized [MultiCharacterRange].
139+
* Used to allow character class unions (e.g.: `[[a-c][x-z]]`).
140+
*/
141+
fun union(a: MultiCharacterRange, b: MultiCharacterRange): MultiCharacterRange {
142+
val result = mutableListOf<CharacterRange>()
143+
var idxA = 0
144+
var idxB = 0
145+
146+
while (idxA < a.size && idxB < b.size) {
147+
// pick the range with the smaller start
148+
val (start, end) = if (a[idxA].start <= b[idxB].start) {
149+
a[idxA].start to a[idxA].end.also { idxA++ }
150+
} else {
151+
b[idxB].start to b[idxB].end.also { idxB++ }
152+
}
153+
154+
// merge with last range in result if overlapping or adjacent
155+
if (result.isNotEmpty() && start.code <= result.last().end.code + 1) {
156+
val last = result.removeLast()
157+
result.add(CharacterRange(last.start, maxOf(last.end, end)))
158+
} else {
159+
result.add(CharacterRange(start, end))
160+
}
161+
}
162+
163+
// append remaining ranges from whichever list isn't exhausted
164+
while (idxA < a.size) {
165+
val curr = a[idxA++]
166+
if (result.isNotEmpty() && curr.start.code <= result.last().end.code + 1) {
167+
val last = result.removeLast()
168+
result.add(CharacterRange(last.start, maxOf(last.end, curr.end)))
169+
} else {
170+
result.add(curr)
171+
}
172+
}
173+
while (idxB < b.size) {
174+
val curr = b[idxB++]
175+
if (result.isNotEmpty() && curr.start.code <= result.last().end.code + 1) {
176+
val last = result.removeLast()
177+
result.add(CharacterRange(last.start, maxOf(last.end, curr.end)))
178+
} else {
179+
result.add(curr)
180+
}
181+
}
182+
183+
return MultiCharacterRange(result)
184+
}
96185
}
97186

98187
/**

core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,27 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() {
251251
override fun testJSExclusiveEscapes() {
252252
// JS exclusive
253253
}
254+
255+
@Test
256+
fun testCharClassIntersectionSubtractionAndNesting(){
257+
checkSameAsJava("[abc-e[f-h]ij-l[m]n]")
258+
checkSameAsJava("[a&&a][a&&a&&a]")
259+
checkSameAsJava("[a-z&&[aeiou]]")
260+
checkSameAsJava("[a-z&&[^aeiou]]")
261+
checkSameAsJava("[a-z&&[a-p]&&[f-z]]")
262+
checkSameAsJava("[ac-e&&[a-d]]")
263+
checkSameAsJava("[\\w&&[a-z]]")
264+
checkSameAsJava("[a-z&&[b-y]]")
265+
checkSameAsJava("[a-z0-9&&[A-Z0-9]&&[2B4C]]")
266+
checkSameAsJava("[[a-c][x-z]&&[b-y]]")
267+
checkSameAsJava("[a-c&&[b-d]e-g]")
268+
checkSameAsJava("[^a-z&&[^aeiou]]")
269+
checkSameAsJava("[\\s&&[^\\n]]")
270+
checkSameAsJava("[a-c&&[c-e]]")
271+
checkSameAsJava("[a-z&&[a-z]]")
272+
checkSameAsJava("[a-ce-g&&[b-f]]")
273+
checkSameAsJava("[[a-z&&[a-p]]&&[f-z]]")
274+
checkSameAsJava("[a[b[c[d&&[\\w]]]][0-7&&\\d&&[0-5]&&1-5]]")
275+
checkSameAsJava("&&")
276+
}
254277
}

0 commit comments

Comments
 (0)