Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply license choices to SPDX expressions #3425

Merged
merged 1 commit into from
Jan 13, 2021
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
2 changes: 1 addition & 1 deletion spdx-utils/src/main/kotlin/SpdxException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

package org.ossreviewtoolkit.spdx

class SpdxException : RuntimeException {
open class SpdxException : RuntimeException {
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(message: String?) : super(message)
constructor(cause: Throwable?) : super(cause)
Expand Down
68 changes: 68 additions & 0 deletions spdx-utils/src/main/kotlin/SpdxExpression.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2017-2019 HERE Europe B.V.
* Copyright (C) 2020-2021 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -153,6 +154,22 @@ sealed class SpdxExpression {
*/
open fun offersChoice(): Boolean = false

/**
* Apply a license [choice], optionally limited to the given [subExpression], and return a [SpdxExpression] where
* the choice is resolved.
*/
open fun applyChoice(choice: SpdxExpression, subExpression: SpdxExpression = this): SpdxExpression {
if (this != subExpression) {
throw InvalidSubExpressionException("$subExpression is not a valid subExpression for $this")
}

if (this != choice) {
throw InvalidLicenseChoiceException("Cannot select $choice for expression $this.")
}

return this
}

/**
* Concatenate [this][SpdxExpression] and [other] using [SpdxOperator.AND].
*/
Expand Down Expand Up @@ -236,6 +253,47 @@ class SpdxCompoundExpression(
SpdxOperator.AND -> left.offersChoice() || right.offersChoice()
}

override fun applyChoice(choice: SpdxExpression, subExpression: SpdxExpression): SpdxExpression {
if (!subExpression.validChoices().containsAll(choice.validChoices())) {
throw InvalidLicenseChoiceException(
"$choice is not a valid choice for $subExpression. Valid choices are: ${validChoices()}."
)
}

if (!isValidSubExpression(subExpression)) {
throw InvalidSubExpressionException("$subExpression is not not a valid subExpression of $this")
}

return replaceSubexpressionWithChoice(subExpression, choice)
}

private fun replaceSubexpressionWithChoice(subExpression: SpdxExpression, choice: SpdxExpression): SpdxExpression {
val expressionString = toString()
val subExpressionString = subExpression.toString()
val choiceString = choice.toString()

return if (expressionString.contains(subExpressionString)) {
expressionString.replace(subExpressionString, choiceString).toSpdx()
} else {
val dismissedLicense = subExpression.validChoices().first { it != choice }
val unchosenLicenses = validChoices().filter { it != dismissedLicense }

if (unchosenLicenses.isEmpty()) {
throw IllegalArgumentException("No licenses left after applying choice $choice to $subExpression")
} else {
unchosenLicenses.reduce(SpdxExpression::or)
}
}
}

private fun isValidSubExpression(subExpression: SpdxExpression): Boolean {
val expressionString = toString()
val subExpressionString = subExpression.toString()

return validChoices().containsAll(subExpression.validChoices()) ||
expressionString.contains(subExpressionString)
}

override fun equals(other: Any?): Boolean {
if (other !is SpdxExpression) return false

Expand Down Expand Up @@ -360,12 +418,14 @@ class SpdxLicenseWithExceptionExpression(
override fun equals(other: Any?) =
when (other) {
is SpdxLicenseWithExceptionExpression -> license == other.license && exception == other.exception

is SpdxExpression -> {
val decomposed = other.decompose()
decomposed.size == 1 && decomposed.first().let {
it is SpdxLicenseWithExceptionExpression && it.license == license && it.exception == exception
}
}

else -> false
}

Expand Down Expand Up @@ -425,12 +485,14 @@ class SpdxLicenseIdExpression(
override fun equals(other: Any?) =
when (other) {
is SpdxLicenseIdExpression -> id == other.id && orLaterVersion == other.orLaterVersion

is SpdxExpression -> {
val decomposed = other.decompose()
decomposed.size == 1 && decomposed.first().let {
it is SpdxLicenseIdExpression && it.id == id && it.orLaterVersion == orLaterVersion
}
}

else -> false
}

Expand Down Expand Up @@ -477,10 +539,12 @@ data class SpdxLicenseReferenceExpression(
override fun equals(other: Any?) =
when (other) {
is SpdxLicenseReferenceExpression -> id == other.id

is SpdxExpression -> {
val decomposed = other.decompose()
decomposed.size == 1 && decomposed.first().let { it is SpdxLicenseReferenceExpression && it.id == id }
}

else -> false
}

Expand Down Expand Up @@ -516,3 +580,7 @@ enum class SpdxOperator(
*/
OR(0)
}

class InvalidLicenseChoiceException(message: String) : SpdxException(message)

class InvalidSubExpressionException(message: String) : SpdxException(message)
108 changes: 108 additions & 0 deletions spdx-utils/src/test/kotlin/SpdxExpressionTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2017-2019 HERE Europe B.V.
* Copyright (C) 2020-2021 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -367,6 +368,113 @@ class SpdxExpressionTest : WordSpec() {
}
}

"applyChoice()" should {
"return the choice for a simple expression" {
val expression = "a".toSpdx()
val choice = "a".toSpdx()

val result = expression.applyChoice(choice)

result shouldBe "a".toSpdx()
}

"throw an exception if the user chose a wrong license for a simple expression" {
val expression = "a".toSpdx()
val choice = "b".toSpdx()

shouldThrow<InvalidLicenseChoiceException> { expression.applyChoice(choice) }
}

"return the new expression if only a part of the expression is matched by the subExpression" {
val expression = "a OR b OR c".toSpdx()
val choice = "b".toSpdx()
val subExpression = "a OR b".toSpdx()

val result = expression.applyChoice(choice, subExpression)

result shouldBe "b OR c".toSpdx()
}

"work with choices that itself are a choice" {
val expression = "a OR b OR c OR d".toSpdx()
val choice = "a OR b".toSpdx()
val subExpression = "a OR b OR c".toSpdx()

val result = expression.applyChoice(choice, subExpression)

result shouldBe "a OR b OR d".toSpdx()
}

"apply the choice if the expression contains multiple choices" {
val expression = "a OR b OR c".toSpdx()
val choice = "b".toSpdx()

val result = expression.applyChoice(choice)

result shouldBe "b".toSpdx()
}

"throw an exception if the chosen license is not a valid option" {
val expression = "a OR b".toSpdx()
val choice = "c".toSpdx()

shouldThrow<InvalidLicenseChoiceException> { expression.applyChoice(choice) }
}

"apply the choice if the expression is not in DNF" {
val expression = "(a OR b) AND c".toSpdx()
val choice = "a AND c".toSpdx()

val result = expression.applyChoice(choice)

result shouldBe "a AND c".toSpdx()
}

"return the reduced subExpression in DNF if the choice was valid" {
val expression = "(a OR b) AND c AND (d OR e)".toSpdx()
val choice = "a AND c AND d".toSpdx()
val subExpression = "a AND c AND d OR a AND c AND e".toSpdx()

val result = expression.applyChoice(choice, subExpression)

result shouldBe "a AND c AND d OR b AND c AND d OR b AND c AND e".toSpdx()
}

"throw an exception if the subExpression does not match the simple expression" {
val expression = "a".toSpdx()
val choice = "x".toSpdx()
val subExpression = "x OR y".toSpdx()

shouldThrow<InvalidSubExpressionException> { expression.applyChoice(choice, subExpression) }
}

"throw an exception if the subExpression does not match the expression" {
val expression = "a OR b OR c".toSpdx()
val choice = "x".toSpdx()
val subExpression = "x OR y OR z".toSpdx()

shouldThrow<InvalidSubExpressionException> { expression.applyChoice(choice, subExpression) }
}

"throw an exception if the subExpression does not match and needs to be converted to a DNF" {
val expression = "(a OR b) AND c AND (d OR e)".toSpdx()
val choice = "a AND c AND d".toSpdx()
val subExpression = "(a AND c AND d) OR (x AND y AND z)".toSpdx()

shouldThrow<InvalidSubExpressionException> { expression.applyChoice(choice, subExpression) }
}

"apply the choice when the subExpression matches only a part of the expression" {
val expression = "(a OR b) AND c".toSpdx()
val choice = "a".toSpdx()
val subExpression = "a OR b".toSpdx()

val result = expression.applyChoice(choice, subExpression)

result shouldBe "a AND c".toSpdx()
}
}

"equals()" should {
"return true for semantically equal expressions" {
"a".toSpdx() shouldBe "a".toSpdx()
Expand Down