[K/N] Fix stack overflow in regex when a quantifier is matched many times

Motivation:

Users often expect simple patterns, like `[a]+` or `[^a]+`, to work fast
and without any problems, even with long strings.
Char class from the first pattern matches only 'a' and gets wrapped into
LeafQuantifierSet, and works fine with long strings indeed.
Char class from the second pattern, however, matches any character
except 'a', including supplementary code points. So, the number of chars
it consumes is not known beforehand. There is no optimization for such
char classes, and if they are matched multiple times, the stack memory
gets exhausted.

Modification:

Introduce FixedLengthQuantifierSet node.
The node represents quantifier over constructs that consume a fixed
amount of characters for a given string and index. Such constructs don't
need backtracking to find a different match. Thus, it is possible for
the node to avoid recursion when matching multiple times.

Result:

Fixes KT-46211, KT-35508 and probably KT-39789. Reproducer for the
latter issue is no longer available, but error stacktrace resembles
those of the other issues.
This commit is contained in:
Abduqodiri Qurbonzoda
2022-11-17 00:04:03 +02:00
committed by Space Team
parent 6e50bbee3b
commit fb31a29c39
16 changed files with 440 additions and 116 deletions
@@ -349,115 +349,68 @@ internal class Pattern(val pattern: String, flags: Int = 0) {
return cur
}
private fun quantifierFromLexerToken(quant: Int): Quantifier {
return when (quant) {
Lexer.QUANT_COMP, Lexer.QUANT_COMP_R, Lexer.QUANT_COMP_P -> {
lexemes.nextSpecial() as Quantifier
}
else -> {
lexemes.next()
Quantifier.fromLexerToken(quant)
}
}
}
/**
* Q->T(*|+|?...) also do some optimizations.
*/
private fun processQuantifier(last: AbstractSet, term: AbstractSet): AbstractSet {
val quant = lexemes.currentChar
if (term !is LeafSet) {
return when (quant) {
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
val q: QuantifierSet
if (term.type == AbstractSet.TYPE_DOTSET && (quant == Lexer.QUANT_STAR || quant == Lexer.QUANT_PLUS)) {
lexemes.next()
return DotQuantifierSet(term, last, quant, AbstractLineTerminator.getInstance(flags), hasFlag(Pattern.DOTALL))
}
lexemes.next()
if (term.type == AbstractSet.TYPE_DOTSET) {
q = DotQuantifierSet(term, last, quant, AbstractLineTerminator.getInstance(flags), hasFlag(Pattern.DOTALL))
} else {
q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
}
term.next = q
q
return when (quant) {
Lexer.QUANT_STAR, Lexer.QUANT_PLUS, Lexer.QUANT_ALT, Lexer.QUANT_COMP -> {
val quantifier = quantifierFromLexerToken(quant)
when {
term is LeafSet ->
LeafQuantifierSet(quantifier, term, last, quant)
term.consumesFixedLength ->
FixedLengthQuantifierSet(quantifier, term, last, quant)
else ->
GroupQuantifierSet(quantifier, term, last, quant, groupQuantifierCount++)
}
Lexer.QUANT_ALT -> {
lexemes.next()
val q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R, Lexer.QUANT_ALT_R -> {
lexemes.next()
val q = ReluctantGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P, Lexer.QUANT_ALT_P -> {
lexemes.next()
PossessiveGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
}
Lexer.QUANT_COMP -> {
val q = GroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_COMP_R -> {
val q = ReluctantGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_COMP_P -> {
return PossessiveGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
}
else -> term
}
} else {
val leaf: LeafSet = term
return when (quant) {
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
lexemes.next()
val q = LeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
}
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R -> {
lexemes.next()
val q = ReluctantLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R, Lexer.QUANT_ALT_R, Lexer.QUANT_COMP_R -> {
val quantifier = quantifierFromLexerToken(quant)
when {
term is LeafSet ->
ReluctantLeafQuantifierSet(quantifier, term, last, quant)
term.consumesFixedLength ->
ReluctantFixedLengthQuantifierSet(quantifier, term, last, quant)
else ->
ReluctantGroupQuantifierSet(quantifier, term, last, quant, groupQuantifierCount++)
}
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P -> {
lexemes.next()
val q = PossessiveLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
}
Lexer.QUANT_ALT -> {
lexemes.next()
LeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT)
}
Lexer.QUANT_ALT_R -> {
lexemes.next()
ReluctantLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_R)
}
Lexer.QUANT_ALT_P -> {
lexemes.next()
PossessiveLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_P)
}
Lexer.QUANT_COMP -> {
LeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP)
}
Lexer.QUANT_COMP_R -> {
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_R)
}
Lexer.QUANT_COMP_P -> {
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_P)
}
else -> term
}
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P, Lexer.QUANT_ALT_P, Lexer.QUANT_COMP_P -> {
val quantifier = quantifierFromLexerToken(quant)
when {
term is LeafSet ->
PossessiveLeafQuantifierSet(quantifier, term, last, quant)
term.consumesFixedLength ->
PossessiveFixedLengthQuantifierSet(quantifier, term, last, quant)
else ->
PossessiveGroupQuantifierSet(quantifier, term, last, quant, groupQuantifierCount++)
}
}
else -> term
}
}
@@ -108,6 +108,15 @@ internal abstract class AbstractSet(val type: Int = 0) {
= (rightLimit downTo leftLimit).firstOrNull { index -> matches(index, testString, matchResult) >= 0 }
?: -1
/**
* Returns `true` if this node consumes a constant number of characters and doesn't need backtracking to find a different match.
* Otherwise, returns `false`.
*
* This information is used to avoid recursion when matching a quantifier node with this inner node.
*/
open val consumesFixedLength: Boolean
get() = false
/**
* Returns true, if this node has consumed any characters during
* positive match attempt, for example node representing character always
@@ -93,7 +93,7 @@ package kotlin.text.regex
* from the parent range. This class represents the parent range split in such a manner.
*/
internal class CompositeRangeSet(/* range without surrogates */ val withoutSurrogates: AbstractSet,
/* range containing surrogates only */ val surrogates: AbstractSet) : SimpleSet() {
/* range containing surrogates only */ val surrogates: SurrogateRangeSet) : SimpleSet() {
override var next: AbstractSet = dummyNext
get() = field
@@ -103,6 +103,12 @@ internal class CompositeRangeSet(/* range without surrogates */ val withoutSurro
withoutSurrogates.next = next
}
// This node consumes a single character starting at `startIndex`.
// A single surrogate char is consumed only if it is unpaired, see [SurrogateRangeSet].
// Thus, for a given [testString] and [startIndex] a fixed amount of chars are consumed.
override val consumesFixedLength: Boolean
get() = true
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var result = withoutSurrogates.matches(startIndex, testString, matchResult)
if (result < 0) {
@@ -27,7 +27,8 @@ open internal class DecomposedCharSet(
/** Decomposition of the Unicode codepoint */
private val decomposedChar: IntArray,
/** Length of useful part of decomposedChar decomposedCharLength <= decomposedChar.length */
private val decomposedCharLength: Int) : SimpleSet() {
private val decomposedCharLength: Int
) : SimpleSet() {
/** Contains information about number of chars that were read for a codepoint last time */
private var readCharsForCodePoint = 1
@@ -42,6 +43,9 @@ open internal class DecomposedCharSet(
return@lazy strBuff.toString()
}
override val consumesFixedLength: Boolean
get() = true
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var strIndex = startIndex
val rightBound = testString.length
@@ -28,6 +28,12 @@ package kotlin.text.regex
internal class DotSet(val lt: AbstractLineTerminator, val matchLineTerminator: Boolean)
: SimpleSet(AbstractSet.TYPE_DOTSET) {
// This node consumes any character. If the character is supplementary, this node consumes both surrogate chars representing it.
// Otherwise, consumes a single char.
// Thus, for a given [testString] and [startIndex] a fixed amount of chars are consumed.
override val consumesFixedLength: Boolean
get() = true
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
if (startIndex >= rightBound) {
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.text.regex
/**
* Greedy quantifier over constructions that consume a fixed number of characters.
*/
open internal class FixedLengthQuantifierSet(
val quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int
) : QuantifierSet(innerSet, next, type) {
init {
require(innerSet.consumesFixedLength)
innerSet.next = FSet.possessiveFSet
}
val min: Int get() = quantifier.min
val max: Int get() = quantifier.max
override val consumesFixedLength: Boolean
get() = (min == max)
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
val matches = mutableListOf<Int>()
// Process occurrences between 0 and max.
while (max == Quantifier.INF || matches.size < max) {
val nextIndex = innerSet.matches(index, testString, matchResult)
if (nextIndex < 0) {
if (matches.size < min) {
return -1
} else {
break
}
}
matches.add(index)
index = nextIndex
}
// Roll back if the next node doesn't match the remaining string.
while (matches.size > min) {
val nextIndex = next.matches(index, testString, matchResult)
if (nextIndex >= 0) {
return nextIndex
}
index = matches.removeLast()
}
return next.matches(index, testString, matchResult)
}
override fun toString(): String {
return "${this::class}(innerSet = $innerSet, next = $next)"
}
}
@@ -26,7 +26,6 @@ package kotlin.text.regex
* Default quantifier over groups, in fact this type of quantifier is
* generally used for constructions we cant identify number of characters they
* consume.
*/
open internal class GroupQuantifierSet(
val quantifier: Quantifier,
@@ -36,6 +35,11 @@ open internal class GroupQuantifierSet(
val groupQuantifierIndex: Int // It's used to remember a number of the innerSet occurrences during the recursive search.
) : QuantifierSet(innerSet, next, type) {
init {
require(!innerSet.consumesFixedLength)
innerSet.next = this
}
val max: Int get() = quantifier.max
val min: Int get() = quantifier.min
@@ -38,7 +38,8 @@ internal class HangulDecomposedCharSet(
* Length of useful part of decomposedChar
* decomposedCharLength <= decomposedChar.length
*/
private val decomposedCharLength: Int) : SimpleSet() {
private val decomposedCharLength: Int
) : SimpleSet() {
/**
* String representing syllable
@@ -50,6 +51,9 @@ internal class HangulDecomposedCharSet(
override val name: String
get() = "decomposed Hangul syllable: $decomposedCharUTF16"
override val consumesFixedLength: Boolean
get() = true
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
@@ -37,10 +37,17 @@ open internal class LeafQuantifierSet(var quantifier: Quantifier,
type: Int
) : QuantifierSet(innerSet, next, type) {
init {
innerSet.next = FSet.possessiveFSet
}
val leaf: LeafSet get() = super.innerSet as LeafSet
val min: Int get() = quantifier.min
val max: Int get() = quantifier.max
override val consumesFixedLength: Boolean
get() = (min == max)
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
var occurrences = 0
@@ -32,6 +32,9 @@ internal abstract class LeafSet : SimpleSet(AbstractSet.TYPE_LEAF) {
/** Returns "shift", the number of accepted chars. Commonly internal function, but called by quantifiers. */
abstract fun accepts(startIndex: Int, testString: CharSequence): Int
override val consumesFixedLength: Boolean
get() = true
/**
* Checks if we can enter this state and pass the control to the next one.
* Return positive value if match succeeds, negative otherwise.
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.text.regex
/**
* Possessive version of the fixed length quantifier set.
*/
internal class PossessiveFixedLengthQuantifierSet(
quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int
) : FixedLengthQuantifierSet(quantifier, innerSet, next, type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
// Process occurrences between 0 and max.
var occurrences = 0
while (max == Quantifier.INF || occurrences < max) {
val nextIndex = innerSet.matches(index, testString, matchResult)
if (nextIndex < 0) {
break
}
index = nextIndex
occurrences += 1
}
return if (occurrences < min) -1 else next.matches(index, testString, matchResult)
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.text.regex
/**
* Reluctant version of the fixed length quantifier set.
*/
internal class ReluctantFixedLengthQuantifierSet(
quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int
) : FixedLengthQuantifierSet(quantifier, innerSet, next, type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
// Process first min occurrences.
repeat(min) {
val nextIndex = innerSet.matches(index, testString, matchResult)
if (nextIndex < 0) {
return -1
}
index = nextIndex
}
// Process occurrences between min and max.
var occurrences = min
while (max == Quantifier.INF || occurrences < max) {
var nextIndex = next.matches(index, testString, matchResult)
if (nextIndex < 0) {
nextIndex = innerSet.matches(index, testString, matchResult)
if (nextIndex < 0) {
return -1
}
index = nextIndex
occurrences += 1
} else {
return nextIndex
}
}
return next.matches(index, testString, matchResult)
}
}
@@ -96,6 +96,12 @@ open internal class SupplementaryRangeSet(charClass: AbstractCharClass, val igno
val chars = charClass.instance
// This node can consume a single char or a supplementary code point consisting of two surrogate chars.
// But this node can't match an unpaired surrogate char.
// Thus, for a given [testString] and [startIndex] a fixed amount of chars are consumed.
override val consumesFixedLength: Boolean
get() = true
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
if (startIndex >= rightBound) {
@@ -87,7 +87,7 @@
package kotlin.text.regex
/*
/**
* This class is a range that contains only surrogate characters.
*/
internal class SurrogateRangeSet(surrChars: AbstractCharClass) : RangeSet(surrChars) {
@@ -0,0 +1,174 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package test.text.harmony_regex
import kotlin.text.*
import kotlin.test.*
class FixedLengthQuantifierTest {
companion object {
private val quantifierMatchCount = 100_000
private val compositeMin = 50_000
private val compositeMax = 150_000
private val input = "a".repeat(quantifierMatchCount)
private val inputDescription = "\"a\".repeat($quantifierMatchCount)"
}
private fun testMatches(regex: Regex, input: String, inputDescription: String, expected: Boolean = true) {
val message = "$regex should ${if (expected) "" else "not " }match $inputDescription"
assertEquals(expected, regex.matches(input), message)
}
@Test
fun fixedLengthQualifierGreedy() {
val plusRegex = Regex("[^\\s]+")
testMatches(plusRegex, input, inputDescription)
val starRegex = Regex("[^\\s]*")
testMatches(starRegex, input, inputDescription)
Regex("[^\\s\\d]{$compositeMin,$compositeMax}").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("[^\\s\\d]{$compositeMin,$quantifierMatchCount}").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("[^\\s\\d]{$compositeMin,${quantifierMatchCount - 1}}").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
Regex("[^\\s\\d]{$quantifierMatchCount,$compositeMax}").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("[^\\s\\d]{${quantifierMatchCount + 1},$compositeMax}").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
}
@Test
fun fixedLengthQualifierReluctant() {
val plusRegex = Regex(".+?")
testMatches(plusRegex, input, inputDescription)
val starRegex = Regex(".*?")
testMatches(starRegex, input, inputDescription)
Regex(".{$compositeMin,$compositeMax}?").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex(".{$compositeMin,$quantifierMatchCount}?").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex(".{$compositeMin,${quantifierMatchCount - 1}}?").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
Regex(".{$quantifierMatchCount,$compositeMax}?").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex(".{${quantifierMatchCount + 1},$compositeMax}?").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
}
@Test
fun fixedLengthQualifierPossesive() {
val plusRegex = Regex("\\p{Ll}++")
testMatches(plusRegex, input, inputDescription)
val starRegex = Regex("\\p{Ll}*+")
testMatches(starRegex, input, inputDescription)
Regex("\\p{Ll}{$compositeMin,$compositeMax}+").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("\\p{Ll}{$compositeMin,$quantifierMatchCount}+").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("\\p{Ll}{$compositeMin,${quantifierMatchCount - 1}}+").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
Regex("\\p{Ll}{$quantifierMatchCount,$compositeMax}+").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription)
}
Regex("\\p{Ll}{${quantifierMatchCount + 1},$compositeMax}+").let { compositeRegex ->
testMatches(compositeRegex, input, inputDescription, expected = false)
}
}
@Test
fun leafQuantifierGreedy() {
val plusRegex = Regex("a+")
testMatches(plusRegex, input, inputDescription)
val starRegex = Regex("a*")
testMatches(starRegex, input, inputDescription)
val compositeRegex = Regex("a{$compositeMin,$compositeMax}")
testMatches(compositeRegex, input, inputDescription)
}
@Test
fun kt46211_space() {
val regex = "(https?|ftp)://[^\\s/$.?#].[^\\s]*".toRegex(RegexOption.IGNORE_CASE)
val link = "http://" + input
testMatches(regex, link, "\"http://\" + $inputDescription")
}
@Test
fun kt46211() {
val regex = Regex("[a]+")
val output = regex.replace(input, "")
assertEquals("", output)
}
@Test
fun kt53352() {
val test = input + "b c"
val regex = """(.*?b.*?c)""".toRegex()
val res = regex.find(test)!!
assertEquals(test, res.groupValues[1])
}
@Test
fun kt35508() {
val doesNotWork = """=== EREIGNISLISTE ======
""" + "\u001b" + """Kn
BEGINN 28.06 13:25
EREIGNISSE 62
50 5 28.06 1325
3402 28.06 1325
3412 28.06 1325
63 3 28.06 1325
63 0 28.06 1325
EE06 28.06 1325
EE06 28.06 1322
EE07 28.06 1322
63 3 28.06 1322
EE06 28.06 1322
EE07 28.06 1322
63 3 28.06 1322
63 3 28.06 1322
63 3 28.06 1323
63 3 28.06 1500
50 4 28.06 1500
50 5 30.06 1226
3402 30.06 1226
3412 30.06 1226
50 4 30.06 1227
50 5 30.06 1228
3402 30.06 1228"""
val regex = Regex("(\\x1b\\w[\\s\\S]{1,2})([\\s\\S]+?(?=\\x1b\\w[\\s\\S]{1,2}|\$))")
fun regexTest(content: String): List<String> {
return regex.findAll(content).map {
it.groupValues[1]
}.toList()
}
assertEquals("\u001BKn\n", regexTest(doesNotWork).single())
}
}
@@ -452,23 +452,27 @@ class MatchResultTest {
* Regression test for https://github.com/JetBrains/kotlin-native/issues/2297
*/
@Test fun test2297() {
assertTrue(Regex("^(:[0-5]?[0-9])+$").matches(":20:30"))
assertTrue(Regex("(.{1,}){2}").matches("aa"))
fun testMatches(pattern: String, input: String) {
assertTrue(Regex(pattern).matches(input), "\"$pattern\" should match \"$input\"")
}
assertTrue(Regex("(.+b)+").matches("0b0b"))
assertTrue(Regex("(.+?b)+").matches("0b0b"))
assertTrue(Regex("(.?b)+").matches("0b0b"))
assertTrue(Regex("(.??b)+").matches("0b0b"))
assertTrue(Regex("(.*b)+").matches("0b0b"))
assertTrue(Regex("(.*?b)+").matches("0b0b"))
assertTrue(Regex("(.{1,2}b)+").matches("0b00b"))
assertTrue(Regex("(.{1,2}?b)+").matches("0b00b"))
testMatches("^(:[0-5]?[0-9])+$", input = ":20:30")
testMatches("(.{1,}){2}", input = "aa")
assertTrue(Regex("([0]?[0]?)+").matches("0000"))
assertTrue(Regex("([0]?[0]?b)+").matches("00b00b"))
assertTrue(Regex("((b{2}){3})+").matches("bbbbbbbbbbbb"))
testMatches("(.+b)+", input = "0b0b")
testMatches("(.+?b)+", input = "0b0b")
testMatches("(.?b)+", input = "0b0b")
testMatches("(.??b)+", input = "0b0b")
testMatches("(.*b)+", input = "0b0b")
testMatches("(.*?b)+", input = "0b0b")
testMatches("(.{1,2}b)+", input = "0b00b")
testMatches("(.{1,2}?b)+", input = "0b00b")
assertTrue(Regex("[^a]").matches("b"))
testMatches("([0]?[0]?)+", input = "0000")
testMatches("([0]?[0]?b)+", input = "00b00b")
testMatches("((b{2}){3})+", input = "bbbbbbbbbbbb")
testMatches("[^a]", input = "b")
}
@Test fun kt28158() {