Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions model/src/main/kotlin/config/PackageConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import org.ossreviewtoolkit.model.RepositoryProvenance
import org.ossreviewtoolkit.model.SourceCodeOrigin
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType
import org.ossreviewtoolkit.model.utils.hasIvyVersionRange
import org.ossreviewtoolkit.model.utils.isApplicableIvyVersion
import org.ossreviewtoolkit.model.utils.isVersionRange
import org.ossreviewtoolkit.utils.common.replaceCredentialsInUri

/**
Expand Down Expand Up @@ -86,7 +86,7 @@ data class PackageConfiguration(
"A package configuration must contain at most one of 'sourceArtifactUrl', 'vcs' or 'sourceCodeOrigin'."
}

if (id.isVersionRange()) {
if (id.hasIvyVersionRange()) {
require(vcs == null && sourceArtifactUrl == null) {
"A package configuration cannot have a version range and a 'vcs' or 'sourceArtifactUrl'."
}
Expand Down
41 changes: 20 additions & 21 deletions model/src/main/kotlin/utils/VersionUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,40 @@
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.model.utils

import org.apache.logging.log4j.kotlin.logger

import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.utils.common.withoutSuffix
import org.ossreviewtoolkit.utils.ort.showStackTrace

import org.semver4j.Semver
import org.semver4j.processor.IvyProcessor
import org.semver4j.range.RangeList
import org.semver4j.range.RangeListFactory

/**
* A list of Strings that are used by Ivy-style version ranges.
*/
private val IVY_VERSION_RANGE_INDICATORS = listOf(",", "~", "*", "+", ">", "<", "=", " - ", "^", ".x", "||")

/**
* Return true if the version of this [Identifier] is an Ivy version range.
*/
fun Identifier.isVersionRange(): Boolean {
val ranges = getVersionRanges()?.get()?.flatten() ?: return false
val rangeVersions = ranges.mapTo(mutableSetOf()) { it.rangeVersion }
val isSingleVersion = rangeVersions.size <= 1 && ranges.all { range ->
// Determine whether the non-accessible `Range.rangeOperator` is `RangeOperator.EQUALS`.
range.toString().startsWith("=")
}

return !isSingleVersion
}
fun Identifier.hasIvyVersionRange(): Boolean = version.getIvyVersionRanges().get().isNotEmpty()

/**
* Return true if the version of this [Identifier] interpreted as an Ivy version matcher is applicable to the
* package with the given [identifier][pkgId].
*/
internal fun Identifier.isApplicableIvyVersion(pkgId: Identifier): Boolean =
runCatching {
// Support "Exact Revision Matcher" syntax.
if (version == pkgId.version) return true

// Support "Sub Revision Matcher" syntax.
if (version.withoutSuffix("+")?.let { prefix -> pkgId.version.startsWith(prefix) } == true) return true

// `Semver.satisfies(String)` requires a valid version range to work as expected, see:
// https://github.com/semver4j/semver4j/issues/132.
val ranges = getVersionRanges() ?: return false
val ranges = version.getIvyVersionRanges()

return Semver.coerce(pkgId.version)?.satisfies(ranges) == true
}.onFailure {
Expand All @@ -68,10 +61,16 @@ internal fun Identifier.isApplicableIvyVersion(pkgId: Identifier): Boolean =
it.showStackTrace()
}.getOrDefault(false)

private fun Identifier.getVersionRanges(): RangeList? {
if (IVY_VERSION_RANGE_INDICATORS.none { version.contains(it, ignoreCase = true) }) return null
/**
* Get the version ranges contained in this string. Return an empty list if no (non-pathological) ranges are contained.
*/
internal fun String.getIvyVersionRanges(): RangeList {
if (isBlank()) return RangeList(/* includePreRelease = */ false)

val ranges = RangeListFactory.create(this, IvyProcessor())

val isSingleVersion = ranges.get().flatten().singleOrNull { it.toString().startsWith("=") } != null
if (isSingleVersion) return RangeList(/* includePreRelease = */ false)

return runCatching {
RangeListFactory.create(version).takeUnless { it.get().isEmpty() }
}.getOrNull()
return ranges
}
79 changes: 79 additions & 0 deletions model/src/test/kotlin/utils/VersionUtilsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2026 The ORT Project Copyright Holders <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.model.utils

import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.collections.beEmpty
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe

import org.ossreviewtoolkit.model.Identifier

import org.semver4j.range.Range

class VersionUtilsTest : WordSpec({
"getIvyVersionRanges()" should {
"return an empty range list for pathological cases" {
"".getIvyVersionRanges().get() should beEmpty()
" ".getIvyVersionRanges().get() should beEmpty()
"foo".getIvyVersionRanges().get() should beEmpty()
"null".getIvyVersionRanges().get() should beEmpty()
}

"return an empty range list for invalid ranges" {
"[a,b]".getIvyVersionRanges().get() should beEmpty()
"[1,2))".getIvyVersionRanges().get() should beEmpty()
"[1.2.3,4.5.6".getIvyVersionRanges().get() should beEmpty()
"[1.2.3.4,5.6.7.8]".getIvyVersionRanges().get() should beEmpty()
}

"return an empty list for single versions" {
"2".getIvyVersionRanges().get() shouldBe listOf()
"2.0".getIvyVersionRanges().get() shouldBe listOf()
"2.0.0".getIvyVersionRanges().get() shouldBe listOf()
}

"return the correct range list for Ivy version ranges" {
"[1.2.3,4.5.6]".getIvyVersionRanges().get() shouldBe listOf(
listOf(Range("1.2.3", Range.RangeOperator.GTE), Range("4.5.6", Range.RangeOperator.LTE))
)

"[3.3.0,)".getIvyVersionRanges().get() shouldBe listOf(
listOf(Range("3.3.0", Range.RangeOperator.GTE))
)
}
}

"isApplicableIvyVersion()" should {
"support 'Sub Revision Matcher' syntax" {
with(Identifier(":::1.0.+")) {
isApplicableIvyVersion(Identifier(":::1.0.1")) shouldBe true
isApplicableIvyVersion(Identifier(":::1.0.a")) shouldBe true
isApplicableIvyVersion(Identifier(":::1.0.1.0")) shouldBe true
}

with(Identifier(":::1.1+")) {
isApplicableIvyVersion(Identifier(":::1.1")) shouldBe true
isApplicableIvyVersion(Identifier(":::1.1.5")) shouldBe true
isApplicableIvyVersion(Identifier(":::1.10")) shouldBe true
}
}
}
})
Loading