fix: stop omitting redundantly parenthesized licenses in CDX formatter (#3517)

Previously, a bug in the formatter would cause SPDX expressions that
were surrounded in redundant parentheses to be dropped instead of
normalized.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
This commit is contained in:
William Murphy 2024-12-11 10:06:08 -05:00 committed by GitHub
parent 561ed50c2d
commit 445142886e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 24 deletions

View File

@ -1,7 +1,6 @@
package helpers
import (
"fmt"
"strings"
"github.com/CycloneDX/cyclonedx-go"
@ -158,40 +157,42 @@ func mergeSPDX(ex []string) string {
// if the expression does not have balanced parens add them
if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
e = "(" + e + ")"
candidate = append(candidate, e)
}
candidate = append(candidate, e)
}
if len(candidate) == 1 {
return reduceOuter(strings.Join(candidate, " AND "))
return reduceOuter(candidate[0])
}
return strings.Join(candidate, " AND ")
return reduceOuter(strings.Join(candidate, " AND "))
}
func reduceOuter(expression string) string {
var (
sb strings.Builder
openCount int
)
expression = strings.TrimSpace(expression)
for _, c := range expression {
if string(c) == "(" && openCount > 0 {
_, _ = fmt.Fprintf(&sb, "%c", c)
// If the entire expression is wrapped in parentheses, check if they are redundant.
if strings.HasPrefix(expression, "(") && strings.HasSuffix(expression, ")") {
trimmed := expression[1 : len(expression)-1]
if isBalanced(trimmed) {
return reduceOuter(trimmed) // Recursively reduce the trimmed expression.
}
if string(c) == "(" {
openCount++
continue
}
if string(c) == ")" && openCount > 1 {
_, _ = fmt.Fprintf(&sb, "%c", c)
}
if string(c) == ")" {
openCount--
continue
}
_, _ = fmt.Fprintf(&sb, "%c", c)
}
return sb.String()
return expression
}
func isBalanced(expression string) bool {
count := 0
for _, c := range expression {
if c == '(' {
count++
} else if c == ')' {
count--
if count < 0 {
return false
}
}
}
return count == 0
}

View File

@ -164,6 +164,29 @@ func Test_encodeLicense(t *testing.T) {
},
},
},
{
name: "single parenthesized SPDX expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(MIT OR Apache-2.0)")...),
},
expected: &cyclonedx.Licenses{
{
Expression: "MIT OR Apache-2.0",
},
},
},
{
name: "single license AND to parenthesized SPDX expression",
// (LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
},
expected: &cyclonedx.Licenses{
{
Expression: "(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later",
},
},
},
// TODO: do we drop the non SPDX ID license and do a single expression
// OR do we keep the non SPDX ID license and do multiple licenses where the complex
// expressions are set as the NAME field?

View File

@ -20,6 +20,11 @@ func TestParseExpression(t *testing.T) {
expression: "MIT OR Apache-2.0",
want: "MIT OR Apache-2.0",
},
{
name: "accept redundant parentheses",
expression: "(MIT OR Apache-2.0)",
want: "(MIT OR Apache-2.0)",
},
{
name: "Invalid SPDX expression returns error",
expression: "MIT OR Apache-2.0 OR invalid",