Skip to content

Commit

Permalink
Fix version parsing crash on Gradle rich version string
Browse files Browse the repository at this point in the history
Gradle accepts different types of version string declaration:
https://docs.gradle.org/current/userguide/single_versions.html. Now code
 should parse such declarations into semantic versioning without
 exception.

^KT-55255 Fixed
  • Loading branch information
Tapchicoma authored and Space Team committed Dec 13, 2022
1 parent f603c0e commit 2e829ed
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 3 deletions.
Expand Up @@ -43,7 +43,7 @@ private val Dependency.isKotlinTestRootDependency: Boolean

private val kotlin150Version = SemVer(1.toBigInteger(), 5.toBigInteger(), 0.toBigInteger())

private fun isAtLeast1_5(version: String) = SemVer.from(version) >= kotlin150Version
private fun isAtLeast1_5(version: String) = SemVer.fromGradleRichVersion(version) >= kotlin150Version

private val jvmPlatforms = setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm)

Expand Down
Expand Up @@ -67,7 +67,7 @@ internal fun ConfigurationContainer.configureStdlibVersionAlignment() = all { co
if (dependency.group == KOTLIN_MODULE_GROUP &&
(dependency.name == "kotlin-stdlib" || dependency.name == "kotlin-stdlib-jdk7") &&
dependency.version != null &&
SemVer.from(dependency.version!!) >= kotlin180Version
SemVer.fromGradleRichVersion(dependency.version!!) >= kotlin180Version
) {
if (configuration.isCanBeResolved) configuration.alignStdlibJvmVariantVersions(dependency)

Expand Down
Expand Up @@ -83,6 +83,94 @@ data class SemVer(
build.takeIf { it.isNotBlank() }
)
}

/**
* Parses Gradle [rich versions](https://docs.gradle.org/current/userguide/single_versions.html) version string.
* In case of ranges, version prefixes or latest status - returned version will be closest using [Int.MAX_VALUE] as highest possible
* version number for major, minor or patch.
*/
fun fromGradleRichVersion(version: String): SemVer {
return when {
version == "+" || version.startsWith("latest.") ->
SemVer(Int.MAX_VALUE.toBigInteger(), Int.MAX_VALUE.toBigInteger(), Int.MAX_VALUE.toBigInteger())
version.matches(MAJOR_PREFIX_VERSION) ->
from("${version.replaceFirst("+", Int.MAX_VALUE.toString())}.${Int.MAX_VALUE}", loose = true)
version.matches(MINOR_PREFIX_VERSION) ->
from(version.replaceFirst("+", Int.MAX_VALUE.toString()), loose = true)
version.matches(FINITE_RANGE) -> {
if (version.endsWith(CLOSE_INC)) {
from(FINITE_RANGE.find(version)!!.groups[2]!!.value, loose = true)
} else {
from(FINITE_RANGE.find(version)!!.groups[2]!!.value, loose = true).decrement()
}
}
version.matches(LOWER_INFINITE_RANGE) -> {
if (version.endsWith(CLOSE_INC)) {
from(LOWER_INFINITE_RANGE.find(version)!!.groups[1]!!.value, loose = true)
} else {
from(LOWER_INFINITE_RANGE.find(version)!!.groups[1]!!.value, loose = true).decrement()
}
}
version.matches(UPPER_INFINITE_RANGE) -> {
SemVer(Int.MAX_VALUE.toBigInteger(), Int.MAX_VALUE.toBigInteger(), Int.MAX_VALUE.toBigInteger())
}
version.matches(SINGLE_VALUE_RANGE) -> {
from(SINGLE_VALUE_RANGE.find(version)!!.groups[1]!!.value, loose = true)
}
else -> from(version, loose = true)
}
}

private fun SemVer.decrement(): SemVer {
return if (patch == 0.toBigInteger()) {
if (minor == 0.toBigInteger()) {
SemVer(major.dec(), Int.MAX_VALUE.toBigInteger(), Int.MAX_VALUE.toBigInteger())
} else {
SemVer(major, minor.dec(), Int.MAX_VALUE.toBigInteger())
}
} else {
SemVer(major, minor, patch.dec())
}
}

private val MAJOR_PREFIX_VERSION = "^[0-9]+\\.\\+$".toRegex()
private val MINOR_PREFIX_VERSION = "^[0-9]+\\.[0-9]+\\.\\+$".toRegex()

// Following constants and logic around was peeked from
// https://github.com/gradle/gradle/blob/master/subprojects/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/ivyresolve/strategy/VersionRangeSelector.java
private const val OPEN_INC = "["
private const val OPEN_EXC = "]"
private const val OPEN_EXC_MAVEN = "("
private const val CLOSE_INC = "]"
private const val CLOSE_EXC = "["
private const val CLOSE_EXC_MAVEN = ")"
private const val LOWER_INFINITE = "("
private const val UPPER_INFINITE = ")"
private const val SEPARATOR = ","

private const val OPEN_INC_PATTERN = "\\" + OPEN_INC
private const val OPEN_EXC_PATTERN = "\\" + OPEN_EXC + "\\" + OPEN_EXC_MAVEN
private const val CLOSE_INC_PATTERN = "\\" + CLOSE_INC
private const val CLOSE_EXC_PATTERN = "\\" + CLOSE_EXC + "\\" + CLOSE_EXC_MAVEN
private const val LI_PATTERN = "\\" + LOWER_INFINITE
private const val UI_PATTERN = "\\" + UPPER_INFINITE
private const val SEP_PATTERN = "\\s*\\$SEPARATOR\\s*"
private const val OPEN_PATTERN = "[$OPEN_INC_PATTERN$OPEN_EXC_PATTERN]"
private const val CLOSE_PATTERN = "[$CLOSE_INC_PATTERN$CLOSE_EXC_PATTERN]"
private const val ANY_NON_SPECIAL_PATTERN = ("[^\\s" + SEPARATOR + OPEN_INC_PATTERN
+ OPEN_EXC_PATTERN + CLOSE_INC_PATTERN + CLOSE_EXC_PATTERN + LI_PATTERN + UI_PATTERN
+ "]")
private const val FINITE_PATTERN = (OPEN_PATTERN + "\\s*(" + ANY_NON_SPECIAL_PATTERN
+ "+)" + SEP_PATTERN + "(" + ANY_NON_SPECIAL_PATTERN + "+)\\s*" + CLOSE_PATTERN)
private const val LOWER_INFINITE_PATTERN = (LI_PATTERN + SEP_PATTERN + "("
+ ANY_NON_SPECIAL_PATTERN + "+)\\s*" + CLOSE_PATTERN)
private const val UPPER_INFINITE_PATTERN = (OPEN_PATTERN + "\\s*("
+ ANY_NON_SPECIAL_PATTERN + "+)" + SEP_PATTERN + UI_PATTERN)
private const val SINGLE_VALUE_PATTERN = "$OPEN_INC_PATTERN\\s*($ANY_NON_SPECIAL_PATTERN+)$CLOSE_INC_PATTERN"
private val FINITE_RANGE = FINITE_PATTERN.toRegex()
private val LOWER_INFINITE_RANGE = LOWER_INFINITE_PATTERN.toRegex()
private val UPPER_INFINITE_RANGE = UPPER_INFINITE_PATTERN.toRegex()
private val SINGLE_VALUE_RANGE = SINGLE_VALUE_PATTERN.toRegex()
}
}

Expand Down Expand Up @@ -195,4 +283,4 @@ fun min(a: SemVer, b: SemVer): SemVer =
if (a < b) a else b

fun max(a: SemVer, b: SemVer): SemVer =
if (a > b) a else b
if (a > b) a else b
Expand Up @@ -44,4 +44,41 @@ class SemVerTest {
).sorted().joinToString(", ")
)
}

@Test
fun testParseGradleRichVersions() {
val maxInt = Int.MAX_VALUE.toBigInteger()
listOf(
// Version prefix
SemVer(maxInt, maxInt, maxInt) to SemVer.fromGradleRichVersion("+"),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("1.+"),
SemVer(1.toBigInteger(), 10.toBigInteger(), maxInt) to SemVer.fromGradleRichVersion("1.10.+"),
SemVer(1.toBigInteger(), 10.toBigInteger(), 0.toBigInteger()) to SemVer.fromGradleRichVersion("1.10.0.+"),

// Version latest state
SemVer(maxInt, maxInt, maxInt) to SemVer.fromGradleRichVersion("latest.release"),
SemVer(maxInt, maxInt, maxInt) to SemVer.fromGradleRichVersion("latest.integration"),

// Ranges
SemVer(1.toBigInteger(), 5.toBigInteger(), 0.toBigInteger()) to SemVer.fromGradleRichVersion("(1.2,1.5]"),
SemVer(1.toBigInteger(), 5.toBigInteger(), 55.toBigInteger()) to SemVer.fromGradleRichVersion("[1.2,1.5.55]"),
SemVer(1.toBigInteger(), 5.toBigInteger(), 54.toBigInteger()) to SemVer.fromGradleRichVersion("[1.2,1.5.55-SNAPSHOT["),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("[1.1,2.0)"),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("(1.1,2.0)"),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("(1.1,2.0-SNAPSHOT)"),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("]1.0, 2.0["),
SemVer(1.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("[1.0, 2.0["),
SemVer(1.toBigInteger(), 0.toBigInteger(), 0.toBigInteger()) to SemVer.fromGradleRichVersion("(,1.0]"),
SemVer(1.toBigInteger(), 0.toBigInteger(), 0.toBigInteger(), "SNAPSHOT") to SemVer.fromGradleRichVersion("(,1.0-SNAPSHOT]"),
SemVer(0.toBigInteger(), maxInt, maxInt) to SemVer.fromGradleRichVersion("(,1.0)"),
SemVer(maxInt, maxInt, maxInt) to SemVer.fromGradleRichVersion("[1.0,)"),
SemVer(10.toBigInteger(), 0.toBigInteger(), 20.toBigInteger()) to SemVer.fromGradleRichVersion("[10.0.20]"),

// Normal
SemVer(10.toBigInteger(), 0.toBigInteger(), 20.toBigInteger()) to SemVer.fromGradleRichVersion("10.0.20"),
SemVer(10.toBigInteger(), 0.toBigInteger(), 0.toBigInteger(), "SNAPSHOT") to SemVer.fromGradleRichVersion("10.0-SNAPSHOT"),
).forEach { (expected, actual) ->
assertEquals(expected, actual)
}
}
}

0 comments on commit 2e829ed

Please sign in to comment.