From 299e69e5e1b553523e3b0630c0feed8acd8c2e8a Mon Sep 17 00:00:00 2001 From: "E. Lynette Rayle" Date: Mon, 5 Oct 2026 17:28:32 -0400 Subject: [PATCH 1/3] fix LicenseRef handled incorrectly during satisfies check --- spdxexp/satisfies.go | 2 +- spdxexp/satisfies_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 179b0f8..28c1959 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -346,7 +346,7 @@ func (n *node) expandOr() [][]*node { // expandOrTerm expands the terms of an OR expression. func expandOrTerm(term *node, result [][]*node) [][]*node { - if term.isLicense() { + if term.isLicense() || term.isLicenseRef() { result = append(result, []*node{term}) } else if term.isExpression() { if term.isOrExpression() { diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 30cacdb..dc2b014 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -641,6 +641,15 @@ func TestSatisfies(t *testing.T) { {"licenseRef alone not allowed, but with documentRef allowed", "MIT AND LicenseRef-X-BSD-3-Clause-Golang", []string{"MIT", "Apache-2.0", "DocumentRef-spdx-tool-1.2:LicenseRef-X-BSD-3-Clause-Golang"}, false, nil}, + {"first licenseRef satisfies (licenseRef OR licenseRef)", "LicenseRef-x OR LicenseRef-y", []string{"LicenseRef-x"}, true, nil}, + {"second licenseRef satisfies (licenseRef OR licenseRef)", "LicenseRef-x OR LicenseRef-y", []string{"LicenseRef-y"}, true, nil}, + {"licenseRef satisfies (license OR licenseRef)", "MIT OR LicenseRef-x", []string{"LicenseRef-x"}, true, nil}, + {"2nd license satisfies (license OR license)", "MIT OR ISC", []string{"ISC"}, true, nil}, + {"licenseRef alone does not satisfy (license AND (licenseRef OR licenseRef))", "MIT AND (LicenseRef-a OR LicenseRef-b)", []string{"MIT"}, false, nil}, + {"ORed license alone does not satisfy (license AND (license OR license))", "MIT AND (ISC OR BSD-3-Clause)", []string{"MIT"}, false, nil}, + {"licenseRef satisfies (licenseRef OR license)", "LicenseRef-x OR MIT", []string{"LicenseRef-x"}, true, nil}, + {"licenseRef alone does not satisfy ((licenseRef OR licenseRef) AND license)", "(LicenseRef-a OR LicenseRef-b) AND MIT", []string{"MIT"}, false, nil}, + {"documentRef satisfies (license OR documentRef)", "MIT OR DocumentRef-x:LicenseRef-y", []string{"DocumentRef-x:LicenseRef-y"}, true, nil}, } for _, test := range tests { From 086377defd75f81dc6d498aadf8eadf98a344135 Mon Sep 17 00:00:00 2001 From: "E. Lynette Rayle" Date: Mon, 5 Oct 2026 18:01:03 -0400 Subject: [PATCH 2/3] test and fix satisfies for deep nested licenseRefs --- spdxexp/satisfies.go | 4 ++-- spdxexp/satisfies_test.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 28c1959..707a35b 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -353,8 +353,8 @@ func expandOrTerm(term *node, result [][]*node) [][]*node { left := term.expandOr() result = append(result, left...) } else if term.isAndExpression() { - left := term.expandAnd()[0] - result = append(result, left) + left := term.expandAnd() + result = append(result, left...) } } return result diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index dc2b014..c4dc8d9 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -650,6 +650,16 @@ func TestSatisfies(t *testing.T) { {"licenseRef satisfies (licenseRef OR license)", "LicenseRef-x OR MIT", []string{"LicenseRef-x"}, true, nil}, {"licenseRef alone does not satisfy ((licenseRef OR licenseRef) AND license)", "(LicenseRef-a OR LicenseRef-b) AND MIT", []string{"MIT"}, false, nil}, {"documentRef satisfies (license OR documentRef)", "MIT OR DocumentRef-x:LicenseRef-y", []string{"DocumentRef-x:LicenseRef-y"}, true, nil}, + {"deep nested (license AND (licenseRef OR licenseRef)) satisfied by license, first licenseRef", "MIT OR (ISC AND (LicenseRef-a OR LicenseRef-b))", []string{"ISC", "LicenseRef-a"}, true, nil}, + {"deep nested (license AND (licenseRef OR licenseRef)) satisfied by license, second licenseRef", "MIT OR (ISC AND (LicenseRef-a OR LicenseRef-b))", []string{"ISC", "LicenseRef-b"}, true, nil}, + {"deep nested (license AND (licenseRef OR licenseRef)) not satisfied by licenseRef alone", "MIT OR (ISC AND (LicenseRef-a OR LicenseRef-b))", []string{"LicenseRef-a"}, false, nil}, + {"nested AND on left of OR satisfied by second licenseRef", "(ISC AND (LicenseRef-a OR LicenseRef-b)) OR MIT", []string{"ISC", "LicenseRef-b"}, true, nil}, + {"nested AND on either side of OR satisfied by left second licenseRef", "(MIT AND (LicenseRef-a OR LicenseRef-b)) OR (ISC AND (LicenseRef-c OR LicenseRef-d))", []string{"MIT", "LicenseRef-b"}, true, nil}, + {"nested AND on either side of OR satisfied by right second licenseRef", "(MIT AND (LicenseRef-a OR LicenseRef-b)) OR (ISC AND (LicenseRef-c OR LicenseRef-d))", []string{"ISC", "LicenseRef-d"}, true, nil}, + {"two licenseRef OR groups produce Cartesian alternatives", "(LicenseRef-a OR LicenseRef-b) AND (LicenseRef-c OR LicenseRef-d)", []string{"LicenseRef-b", "LicenseRef-d"}, true, nil}, + {"two licenseRef OR groups require one licenseRef from each", "(LicenseRef-a OR LicenseRef-b) AND (LicenseRef-c OR LicenseRef-d)", []string{"LicenseRef-b"}, false, nil}, + {"nested license and licenseRef OR groups satisfy second alternatives", "MIT OR ((ISC OR BSD-3-Clause) AND (LicenseRef-a OR LicenseRef-b))", []string{"BSD-3-Clause", "LicenseRef-b"}, true, nil}, + {"nested documentRef OR satisfies second alternative", "MIT OR (ISC AND (DocumentRef-x:LicenseRef-a OR DocumentRef-x:LicenseRef-b))", []string{"ISC", "DocumentRef-x:LicenseRef-b"}, true, nil}, } for _, test := range tests { From c1ef0d0c0d632a9fbd1b6b07b4d3275a4c1c87b2 Mon Sep 17 00:00:00 2001 From: "E. Lynette Rayle" Date: Mon, 5 Oct 2026 18:13:17 -0400 Subject: [PATCH 3/3] handle deeper more complex licenses for satisfies --- spdxexp/satisfies.go | 3 ++- spdxexp/satisfies_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 707a35b..0c9f1fd 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -412,7 +412,8 @@ func appendTerms(left, right [][]*node) [][]*node { var result [][]*node for _, r := range right { for _, l := range left { - tmp := l + tmp := make([]*node, 0, len(l)+len(r)) + tmp = append(tmp, l...) tmp = append(tmp, r...) result = append(result, tmp) } diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index c4dc8d9..dc81df7 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -660,6 +660,12 @@ func TestSatisfies(t *testing.T) { {"two licenseRef OR groups require one licenseRef from each", "(LicenseRef-a OR LicenseRef-b) AND (LicenseRef-c OR LicenseRef-d)", []string{"LicenseRef-b"}, false, nil}, {"nested license and licenseRef OR groups satisfy second alternatives", "MIT OR ((ISC OR BSD-3-Clause) AND (LicenseRef-a OR LicenseRef-b))", []string{"BSD-3-Clause", "LicenseRef-b"}, true, nil}, {"nested documentRef OR satisfies second alternative", "MIT OR (ISC AND (DocumentRef-x:LicenseRef-a OR DocumentRef-x:LicenseRef-b))", []string{"ISC", "DocumentRef-x:LicenseRef-b"}, true, nil}, + {"deep AND with licenseRef and documentRef is satisfied", "MIT AND ISC AND LicenseRef-a AND DocumentRef-x:LicenseRef-b", []string{"MIT", "ISC", "LicenseRef-a", "DocumentRef-x:LicenseRef-b"}, true, nil}, + {"deep AND with licenseRef and documentRef requires every term", "MIT AND ISC AND LicenseRef-a AND DocumentRef-x:LicenseRef-b", []string{"MIT", "ISC", "LicenseRef-a"}, false, nil}, + {"deep mixed AND satisfies first alternatives", "(MIT OR Apache-2.0) AND (LicenseRef-a OR LicenseRef-b) AND (DocumentRef-x:LicenseRef-c OR DocumentRef-x:LicenseRef-d)", []string{"MIT", "LicenseRef-a", "DocumentRef-x:LicenseRef-c"}, true, nil}, + {"deep mixed AND satisfies second alternatives", "(MIT OR Apache-2.0) AND (LicenseRef-a OR LicenseRef-b) AND (DocumentRef-x:LicenseRef-c OR DocumentRef-x:LicenseRef-d)", []string{"Apache-2.0", "LicenseRef-b", "DocumentRef-x:LicenseRef-d"}, true, nil}, + {"deep mixed AND requires one term from every OR group", "(MIT OR Apache-2.0) AND (LicenseRef-a OR LicenseRef-b) AND (DocumentRef-x:LicenseRef-c OR DocumentRef-x:LicenseRef-d)", []string{"Apache-2.0", "LicenseRef-b"}, false, nil}, + {"deep mixed AND requires matching documentRef", "(MIT OR Apache-2.0) AND (LicenseRef-a OR LicenseRef-b) AND (DocumentRef-x:LicenseRef-c OR DocumentRef-x:LicenseRef-d)", []string{"Apache-2.0", "LicenseRef-b", "DocumentRef-y:LicenseRef-d"}, false, nil}, } for _, test := range tests { @@ -870,6 +876,22 @@ func TestExpandAnd(t *testing.T) { } } +func TestAppendTermsDoesNotAliasBranches(t *testing.T) { + mit := getLicenseNode("MIT", false) + isc := getLicenseNode("ISC", false) + apache := getLicenseNode("Apache-2.0", false) + + leftTerm := make([]*node, 1, 2) + leftTerm[0] = mit + + actual := appendTerms( + [][]*node{leftTerm}, + [][]*node{{isc}, {apache}}, + ) + + assert.Equal(t, [][]*node{{mit, isc}, {mit, apache}}, actual) +} + type testCaseData struct { name string expression string