Skip to content

Commit 0a1c1cd

Browse files
authored
Merge pull request #1577 from WebFuzzing/regex-support-extension
Java regex unsatisfiable alternatives support
2 parents 6f4cf01 + 83abb65 commit 0a1c1cd

9 files changed

Lines changed: 269 additions & 54 deletions

File tree

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

Lines changed: 92 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,22 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
4040
* Capture groups in order of appearance (1-based index -> list index 0).
4141
* Populated as the tree is walked. A backreference is only valid if it
4242
* appears after the group it references, which Java regex requires anyway.
43+
* The value is nullable to represent a captured group that is unsatisfiable,
44+
* for example when the group contains an empty character class like `([a&&b])`.
45+
* In that case the map holds null instead of a DisjunctionListRxGene.
46+
* @see buildDisjunctionList
4347
*/
4448
private val captureGroups = mutableListOf<DisjunctionListRxGene?>()
4549

4650
/**
4751
* Same as [captureGroups] but for named backreferences, which can be accessed
4852
* with their name or number.
53+
* The value is nullable to represent a captured group that is unsatisfiable,
54+
* for example when the group contains an empty character class like `([a&&b])`.
55+
* In that case the map holds null instead of a DisjunctionListRxGene.
56+
* @see buildDisjunctionList
4957
*/
50-
private val namedCaptureGroups = mutableMapOf<String, DisjunctionListRxGene>()
58+
private val namedCaptureGroups = mutableMapOf<String, DisjunctionListRxGene?>()
5159

5260
/**
5361
* Tracks the flags active in the current lexical scope.
@@ -74,14 +82,48 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
7482
)
7583
}
7684

85+
/**
86+
* Builds DisjunctionListRxGenes from a disjunction context, returns null if disjunction is unsatisfiable.
87+
*/
88+
private fun buildDisjunctionList(ctx: RegexJavaParser.DisjunctionContext): DisjunctionListRxGene? {
89+
val res = ctx.accept(this)
90+
val validDisjunctions = res.genes.map { it as DisjunctionRxGene }
91+
92+
val satisfiableDisjunctions = validDisjunctions.filter{ !it.isUnsatisfiable() }
93+
94+
if(satisfiableDisjunctions.isEmpty()){
95+
// As DisjunctionListRxGene extends CompositeFixedGene, its disjunctions list cannot be empty.
96+
// In this case we return null to represent an unsatisfiable DisjunctionListRxGene.
97+
return null
98+
}
99+
100+
val disjList = DisjunctionListRxGene(satisfiableDisjunctions)
101+
102+
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
103+
for (gene in disjList.disjunctions) {
104+
gene.extraPrefix = false
105+
gene.extraPostfix = false
106+
gene.matchStart = true
107+
gene.matchEnd = true
108+
}
109+
return disjList
110+
}
77111

78112
override fun visitPattern(ctx: RegexJavaParser.PatternContext): VisitResult {
79113

80114
val res = ctx.disjunction().accept(this)
81115

82116
val text = RegexUtils.getRegexExpByParserRuleContext(ctx)
83117

84-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
118+
val satisfiableDisjunctions = res.genes
119+
.map { it as DisjunctionRxGene }
120+
.filter{ !it.isUnsatisfiable() }
121+
122+
if (satisfiableDisjunctions.isEmpty()) {
123+
throw IllegalStateException("Regex is unsatisfiable.")
124+
}
125+
126+
val disjList = DisjunctionListRxGene(satisfiableDisjunctions)
85127

86128
// we remove the <EOF> token from end of the string to store as sourceRegex
87129
val gene = RegexGene(
@@ -102,9 +144,19 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
102144
val matchStart = assertionMatches.first
103145
val matchEnd = assertionMatches.second
104146

105-
val disj = DisjunctionRxGene("disj", altRes.genes.map { it }, matchStart, matchEnd)
147+
val res = VisitResult()
148+
149+
// add disjunction if it has genes, OR if the alternative was purely assertions (^$) or flag scopes
150+
// in that case altRes.genes is empty but the alternative is valid (matches "")
151+
val hasOnlyAssertionsOrFlagScopes = ctx.alternative().term().isNotEmpty() &&
152+
ctx.alternative().term().all { it.assertion() != null || it.FLAG_SCOPE_OPEN() != null }
153+
154+
if (altRes.genes.isNotEmpty() || hasOnlyAssertionsOrFlagScopes || ctx.alternative().term().isEmpty()) {
155+
val disj = DisjunctionRxGene("disj", altRes.genes.map { it }, matchStart, matchEnd)
106156

107-
val res = VisitResult(disj)
157+
res.genes.add(disj)
158+
}
159+
// else: had non-assertion terms but all produced nothing (empty char class etc.), skip
108160

109161
if(ctx.disjunction() != null){
110162
val disjRes = ctx.disjunction().accept(this)
@@ -170,7 +222,7 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
170222
// term is not a back ref: we use the default behavior, term results may only have 0-1 genes
171223
// if there is a gene, we add it to result
172224
res.genes.add(gene)
173-
} else {
225+
} else if (resTerm.data is String) {
174226

175227
val assertion = resTerm.data as String
176228
if(i==0 && assertion == "^"){
@@ -185,6 +237,9 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
185237
*/
186238
throw IllegalStateException("Cannot support $assertion at position $i")
187239
}
240+
} else {
241+
// unsatisfiable term, return with no genes
242+
return VisitResult(data=Pair(false, false))
188243
}
189244
}
190245

@@ -204,12 +259,23 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
204259

205260
val resAtom = ctx.atom().accept(this)
206261
val atom = resAtom.genes.firstOrNull()
207-
?: return res
208262

209263
if(ctx.quantifier() != null){
210264

211265
val limits = ctx.quantifier().accept(this).data as Pair<Int,Int>
212266

267+
// if quantified atom is unsatisfiable we must then check the limits
268+
if (atom == null ||
269+
((atom as? RxTerm)?.isUnsatisfiable() == true) && resAtom.genes.size == 1) {
270+
return if (limits.first == 0) {
271+
// if 0 appearances is allowed then the regex is satisfiable only with empty string
272+
VisitResult(PatternCharacterBlockGene("0_QuantifierOnEmptyRegex", ""))
273+
} else {
274+
// if not then unsatisfiable, return with no genes
275+
res
276+
}
277+
}
278+
213279
// if atom is not a back ref then we use the default behavior, results may only have one gene
214280
var template: Gene = atom
215281

@@ -234,10 +300,11 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
234300
if (ctx.atom()?.atomEscape()?.BackReference() != null){
235301
// if atom is a BackReference we addAll genes from result as there may be more than one if digits are dropped
236302
res.genes.addAll(resAtom.genes)
237-
} else {
303+
} else if (atom != null) {
238304
// if atom is not a back ref we fall back to the default behavior, results only have one gene
239305
res.genes.add(atom)
240306
}
307+
// else atom is unsatisfiable, return no genes
241308
}
242309

243310
return res
@@ -307,21 +374,16 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
307374

308375
currentFlags = merged
309376

310-
val res = ctx.disjunction().accept(this)
377+
val disjList = buildDisjunctionList(ctx.disjunction())
311378

312379
currentFlags = previous
313380

314-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
315-
316-
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
317-
for (gene in disjList.disjunctions) {
318-
gene.extraPrefix = false
319-
gene.extraPostfix = false
320-
gene.matchStart = true
321-
gene.matchEnd = true
381+
return if (disjList != null) {
382+
VisitResult(disjList)
383+
} else {
384+
// unsatisfiable, return with no genes.
385+
VisitResult()
322386
}
323-
324-
return VisitResult(disjList)
325387
}
326388

327389
if(ctx.quote() != null){
@@ -355,17 +417,7 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
355417
val groupIndex = captureGroups.size
356418
captureGroups.add(null) // add placeholder for the gene
357419

358-
val res = ctx.disjunction().accept(this)
359-
360-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
361-
362-
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
363-
for(gene in disjList.disjunctions){
364-
gene.extraPrefix = false
365-
gene.extraPostfix = false
366-
gene.matchStart = true
367-
gene.matchEnd = true
368-
}
420+
val disjList = buildDisjunctionList(ctx.disjunction())
369421

370422
val isCapturingGroup = !ctx.text.startsWith("(?:")
371423
val isNamedCaptureGroup = ctx.NAMED_CAPTURE_GROUP_OPEN() != null
@@ -381,7 +433,12 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
381433
namedCaptureGroups[name] = disjList
382434
}
383435

384-
return VisitResult(disjList)
436+
return if (disjList != null) {
437+
VisitResult(disjList)
438+
} else {
439+
// unsatisfiable, return with no genes.
440+
VisitResult()
441+
}
385442
}
386443

387444
if(ctx.DOT() != null){
@@ -600,15 +657,12 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
600657
maxDigits > allDigits.length -> allDigits.length
601658
allDigits.take(maxDigits).toInt() <= captureGroups.size -> maxDigits
602659
maxDigits > 1 -> maxDigits - 1
603-
else -> throw IllegalStateException(
604-
"Backreference ${txt.take(2)} refers to group ${allDigits[0]} but only ${captureGroups.size} " +
605-
"capture group(s) have been defined so far"
606-
)
660+
else -> 1
607661
}
608662

609663
val n = allDigits.take(backRefDigitCount).toInt()
610664

611-
val result = VisitResult(BackReferenceRxGene(n, captureGroups[n - 1]!!))
665+
val result = VisitResult(BackReferenceRxGene(n, captureGroups.getOrNull(n - 1)))
612666

613667
val remainingChars = allDigits.drop(backRefDigitCount)
614668

@@ -624,8 +678,10 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
624678
if (ctx.NamedBackReference() != null) {
625679
// strip "\k<" and ">"
626680
val name = txt.drop(3).dropLast(1)
681+
if(name !in namedCaptureGroups){
682+
throw IllegalStateException("Named backreference \\k<$name> refers to unknown group '$name'")
683+
}
627684
val group = namedCaptureGroups[name]
628-
?: throw IllegalStateException("Named backreference \\k<$name> refers to unknown group '$name'")
629685
val groupIndex = captureGroups.indexOf(group) + 1 // 1-based, for the gene name
630686
return VisitResult(BackReferenceRxGene(groupIndex, group))
631687
}

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation
1414
* Represents a backreference \N in a regex (N being a number).
1515
* Its value is always identical to the current value of its [captureGroup].
1616
* It has no independent state and is therefore immutable.
17+
* If capture group is null then the referenced group was unsatisfiable,
18+
* in which case the same is true for the backreference to it.
1719
*/
1820
class BackReferenceRxGene(
1921
val groupIndex: Int,
20-
val captureGroup: DisjunctionListRxGene
22+
val captureGroup: DisjunctionListRxGene?
2123
) : RxAtom, SimpleGene("\\$groupIndex") {
2224

25+
override fun isUnsatisfiable(): Boolean {
26+
return captureGroup == null || captureGroup.isUnsatisfiable()
27+
}
28+
2329
override fun checkForLocallyValidIgnoringChildren(): Boolean = true
2430

2531
/**
@@ -59,7 +65,12 @@ class BackReferenceRxGene(
5965
mode: GeneUtils.EscapeMode?,
6066
targetFormat: OutputFormat?,
6167
extraCheck: Boolean
62-
): String = captureGroup.getValueAsPrintableString(targetFormat = null)
68+
): String {
69+
if (captureGroup == null) {
70+
throw IllegalStateException("Cannot get value from invalid backreference \\$groupIndex")
71+
}
72+
return captureGroup.getValueAsPrintableString(previousGenes, mode, targetFormat)
73+
}
6374

6475
override fun containsSameValueAs(other: Gene): Boolean {
6576
if (other !is BackReferenceRxGene) return false

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class CharacterClassEscapeRxGene(
8282
// create both normal and negated version for all
8383
.flatMap { (key, value) ->
8484
listOf(
85-
key to MultiCharacterRange(value),
85+
key to MultiCharacterRange(false, value),
8686
"^$key" to MultiCharacterRange(true, value)
8787
)
8888
}.toMap()
@@ -132,6 +132,12 @@ class CharacterClassEscapeRxGene(
132132
}
133133
}
134134

135+
override fun isUnsatisfiable(): Boolean = multiCharRange.isEmpty
136+
137+
override fun isMutable(): Boolean {
138+
return !isUnsatisfiable()
139+
}
140+
135141
override fun checkForLocallyValidIgnoringChildren() : Boolean{
136142
// we pass the same embedded flags to the regex to accurately match the expected behavior
137143
return value.matches(Regex("${flags.getScopeString()}\\$type"))
@@ -193,6 +199,9 @@ class CharacterClassEscapeRxGene(
193199
}
194200

195201
override fun getValueAsPrintableString(previousGenes: List<Gene>, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String {
202+
if (isUnsatisfiable()) {
203+
throw IllegalStateException("Cannot get value from empty CharacterClassEscape")
204+
}
196205
return if (!flags.isCaseable(value[0])) {
197206
value[0].toString()
198207
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,19 @@ class CharacterRangeRxGene(
3030
private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java)
3131
}
3232

33-
var value : Char = validRanges[0].start
33+
// '\u0000' is a placeholder for the unsatisfiable case (empty MCR with no valid ranges).
34+
// This gene will never be randomized or mutated when isUnsatisfiable() is true, as it would be immutable.
35+
// getValueAsPrintableString throws when isUnsatisfiable, so that value should be unreachable.
36+
var value : Char = if (isUnsatisfiable()) '\u0000' else validRanges[0].start
3437

3538
/**
3639
* Whether to output the character in uppercase.
3740
* Only meaningful when flags.caseInsensitive is true.
3841
*/
3942
var useUpperCase: Boolean = false
4043

44+
override fun isUnsatisfiable(): Boolean = validRanges.isEmpty
45+
4146
override fun checkForLocallyValidIgnoringChildren() : Boolean{
4247
return validRanges.any {
4348
value in it ||
@@ -49,6 +54,9 @@ class CharacterRangeRxGene(
4954
}
5055

5156
override fun isMutable(): Boolean {
57+
if (isUnsatisfiable()) {
58+
return false
59+
}
5260
// check if there is more than one character or if the character is caseable
5361
return validRanges.charCount > 1 || flags.isCaseable(value)
5462
}
@@ -134,6 +142,9 @@ class CharacterRangeRxGene(
134142
TODO should \ be handled specially?
135143
In any case, would have same handling as AnyCharacterRxGene
136144
*/
145+
if (isUnsatisfiable()) {
146+
throw IllegalStateException("Cannot get value from empty CharacterRange")
147+
}
137148
return if (!flags.isCaseable(value)) {
138149
value.toString()
139150
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class DisjunctionRxGene(
4747
private val log : Logger = LoggerFactory.getLogger(DisjunctionRxGene::class.java)
4848
}
4949

50+
override fun isUnsatisfiable(): Boolean =
51+
terms.isNotEmpty() && terms.any { (it as? RxTerm)?.isUnsatisfiable() == true }
5052

5153
override fun checkForLocallyValidIgnoringChildren() : Boolean{
5254
return true

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class QuantifierRxGene(
4949
if (min < 0) {
5050
throw IllegalArgumentException("Invalid min value '$min': should be positive")
5151
}
52-
if (max < 1) {
53-
throw IllegalArgumentException("Invalid max value '$max': should be at least 1")
52+
if (max < 0) {
53+
throw IllegalArgumentException("Invalid max value '$max': should be positive")
5454
}
5555
if (min > max) {
5656
throw IllegalArgumentException("Invalid min-max values '$min-$max': min is greater than max")

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ import org.evomaster.core.search.StructuralElement
44
import org.evomaster.core.search.gene.Gene
55

66

7-
interface RxTerm
7+
interface RxTerm {
8+
/**
9+
* Returns true if this gene can never produce a valid value,
10+
* for example an empty character class intersection like [a&&b].
11+
* Used at construction time to filter unsatisfiable branches from disjunctions.
12+
*/
13+
fun isUnsatisfiable(): Boolean = false
14+
}

0 commit comments

Comments
 (0)