From 31dc834e009ee6dcede9386635cc63ef98f76532 Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Mon, 24 Oct 2022 17:09:49 +0200 Subject: [PATCH 1/6] Add a function to validate licenses --- spdxexp/satisfies.go | 14 ++++++++++++++ spdxexp/satisfies_test.go | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index d59d251..7a83c52 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -5,6 +5,20 @@ import ( "sort" ) + +// Checks if given licenses are valid according to spdx +// +// Returns all the invalid licenses contained in the `licenses` argument +func ValidateLicenses(licenses []string) (invalidLicenes []string) { + for _, license := range licenses { + _, err := parse(license) + if err != nil { + invalidLicenes = append(invalidLicenes, license) + } + } + return +} + // Satisfies determines if test license expression satisfies allowed list of licenses. // // Examples: diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 529adb6..276542f 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -3,10 +3,17 @@ package spdxexp import ( "errors" "testing" + "reflect" "github.com/stretchr/testify/assert" ) +func TestValidateLicenses(t *testing.T) { + someLicenses := []string {"MTI", "Apche-2.0", "0xDEADBEEF", ""} + invalidLicenses := ValidateLicenses(someLicenses) + assert.True(t, reflect.DeepEqual(someLicenses, invalidLicenses)) +} + func TestSatisfies(t *testing.T) { tests := []struct { name string @@ -28,6 +35,8 @@ func TestSatisfies(t *testing.T) { errors.New("allowedList requires at least one element, but is empty")}, {"err - invalid license", "NON-EXISTENT-LICENSE", []string{"MIT", "Apache-2.0"}, false, errors.New("unknown license 'NON-EXISTENT-LICENSE' at offset 0")}, + {"err - invalid license in allowed list", "MIT", []string{"NON-EXISTENT-LICENSE", "Apache-2.0"}, false, + errors.New("unknown license 'NON-EXISTENT-LICENSE' at offset 0")}, {"MIT satisfies [MIT, Apache-2.0]", "MIT", []string{"MIT", "Apache-2.0"}, true, nil}, {"MIT OR Apache-2.0 satisfies [MIT]", "MIT OR Apache-2.0", []string{"MIT"}, true, nil}, From 205ae30b0553cbf0d21ffb92ea37dd12f9fa9adc Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Mon, 24 Oct 2022 17:28:17 +0200 Subject: [PATCH 2/6] Apply formatting --- spdxexp/satisfies.go | 102 ++++++++++++++++++++------------------ spdxexp/satisfies_test.go | 4 +- spdxexp/scan.go | 10 ++-- 3 files changed, 61 insertions(+), 55 deletions(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 7a83c52..9dd081e 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -5,7 +5,6 @@ import ( "sort" ) - // Checks if given licenses are valid according to spdx // // Returns all the invalid licenses contained in the `licenses` argument @@ -22,32 +21,32 @@ func ValidateLicenses(licenses []string) (invalidLicenes []string) { // Satisfies determines if test license expression satisfies allowed list of licenses. // // Examples: -// "MIT" satisfies "MIT" is true // -// "MIT" satisfies ["MIT", "Apache-2.0"] is true -// "MIT OR Apache-2.0" satisfies ["MIT"] is true -// "GPL" satisfies ["MIT", "Apache-2.0"] is false -// "MIT OR Apache-2.0" satisfies ["GPL"] is false +// "MIT" satisfies "MIT" is true // -// "Apache-2.0 AND MIT" satisfies ["MIT", "Apache-2.0"] is true -// "MIT AND Apache-2.0" satisfies ["MIT", "Apache-2.0"] is true -// "MIT AND Apache-2.0" satisfies ["MIT"] is false -// "GPL" satisfies ["MIT", "Apache-2.0"] is false +// "MIT" satisfies ["MIT", "Apache-2.0"] is true +// "MIT OR Apache-2.0" satisfies ["MIT"] is true +// "GPL" satisfies ["MIT", "Apache-2.0"] is false +// "MIT OR Apache-2.0" satisfies ["GPL"] is false // -// "MIT AND Apache-2.0" satisfies ["MIT", "Apache-1.0", "Apache-2.0"] is true +// "Apache-2.0 AND MIT" satisfies ["MIT", "Apache-2.0"] is true +// "MIT AND Apache-2.0" satisfies ["MIT", "Apache-2.0"] is true +// "MIT AND Apache-2.0" satisfies ["MIT"] is false +// "GPL" satisfies ["MIT", "Apache-2.0"] is false // -// "Apache-1.0" satisfies ["Apache-2.0+"] is false -// "Apache-2.0" satisfies ["Apache-2.0+"] is true -// "Apache-3.0" satisfies ["Apache-2.0+"] returns error about Apache-3.0 license not existing +// "MIT AND Apache-2.0" satisfies ["MIT", "Apache-1.0", "Apache-2.0"] is true // -// "Apache-1.0" satisfies ["Apache-2.0-or-later"] is false -// "Apache-2.0" satisfies ["Apache-2.0-or-later"] is true -// "Apache-3.0" satisfies ["Apache-2.0-or-later"] returns error about Apache-3.0 license not existing +// "Apache-1.0" satisfies ["Apache-2.0+"] is false +// "Apache-2.0" satisfies ["Apache-2.0+"] is true +// "Apache-3.0" satisfies ["Apache-2.0+"] returns error about Apache-3.0 license not existing // -// "Apache-1.0" satisfies ["Apache-2.0-only"] is false -// "Apache-2.0" satisfies ["Apache-2.0-only"] is true -// "Apache-3.0" satisfies ["Apache-2.0-only"] returns error about Apache-3.0 license not existing +// "Apache-1.0" satisfies ["Apache-2.0-or-later"] is false +// "Apache-2.0" satisfies ["Apache-2.0-or-later"] is true +// "Apache-3.0" satisfies ["Apache-2.0-or-later"] returns error about Apache-3.0 license not existing // +// "Apache-1.0" satisfies ["Apache-2.0-only"] is false +// "Apache-2.0" satisfies ["Apache-2.0-only"] is true +// "Apache-3.0" satisfies ["Apache-2.0-only"] returns error about Apache-3.0 license not existing func Satisfies(testExpression string, allowedList []string) (bool, error) { expressionNode, err := parse(testExpression) if err != nil { @@ -117,25 +116,26 @@ func isCompatible(expressionPart, allowed []*node) bool { // grouped in an array and ORed licenses each in a separate array. // // Example: -// License node: "MIT" becomes [["MIT"]] -// OR Expression: "MIT OR Apache-2.0" becomes [["MIT"], ["Apache-2.0"]] -// AND Expression: "MIT AND Apache-2.0" becomes [["MIT", "Apache-2.0"]] -// OR-AND Expression: "MIT OR Apache-2.0 AND GPL-2.0" becomes [["MIT"], ["Apache-2.0", "GPL-2.0"]] -// OR(AND) Expression: "MIT OR (Apache-2.0 AND GPL-2.0)" becomes [["MIT"], ["Apache-2.0", "GPL-2.0"]] -// AND-OR Expression: "MIT AND Apache-2.0 OR GPL-2.0" becomes [["Apache-2.0", "MIT], ["GPL-2.0"]] -// AND(OR) Expression: "MIT AND (Apache-2.0 OR GPL-2.0)" becomes [["Apache-2.0", "MIT], ["GPL-2.0", "MIT"]] -// OR-AND-OR Expression: "MIT OR ISC AND Apache-2.0 OR GPL-2.0" becomes -// [["MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0"]] -// (OR)AND(OR) Expression: "(MIT OR ISC) AND (Apache-2.0 OR GPL-2.0)" becomes -// [["Apache-2.0", "MIT"], ["GPL-2.0", "MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0", "ISC"]] -// OR(AND)OR Expression: "MIT OR (ISC AND Apache-2.0) OR GPL-2.0" becomes -// [["MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0"]] -// AND-OR-AND Expression: "MIT AND ISC OR Apache-2.0 AND GPL-2.0" becomes -// [["ISC", "MIT"], ["Apache-2.0", "GPL-2.0"]] -// (AND)OR(AND) Expression: "(MIT AND ISC) OR (Apache-2.0 AND GPL-2.0)" becomes -// [["ISC", "MIT"], ["Apache-2.0", "GPL-2.0"]] -// AND(OR)AND Expression: "MIT AND (ISC OR Apache-2.0) AND GPL-2.0" becomes -// [["GPL-2.0", "ISC", "MIT"], ["Apache-2.0", "GPL-2.0", "MIT"]] +// +// License node: "MIT" becomes [["MIT"]] +// OR Expression: "MIT OR Apache-2.0" becomes [["MIT"], ["Apache-2.0"]] +// AND Expression: "MIT AND Apache-2.0" becomes [["MIT", "Apache-2.0"]] +// OR-AND Expression: "MIT OR Apache-2.0 AND GPL-2.0" becomes [["MIT"], ["Apache-2.0", "GPL-2.0"]] +// OR(AND) Expression: "MIT OR (Apache-2.0 AND GPL-2.0)" becomes [["MIT"], ["Apache-2.0", "GPL-2.0"]] +// AND-OR Expression: "MIT AND Apache-2.0 OR GPL-2.0" becomes [["Apache-2.0", "MIT], ["GPL-2.0"]] +// AND(OR) Expression: "MIT AND (Apache-2.0 OR GPL-2.0)" becomes [["Apache-2.0", "MIT], ["GPL-2.0", "MIT"]] +// OR-AND-OR Expression: "MIT OR ISC AND Apache-2.0 OR GPL-2.0" becomes +// [["MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0"]] +// (OR)AND(OR) Expression: "(MIT OR ISC) AND (Apache-2.0 OR GPL-2.0)" becomes +// [["Apache-2.0", "MIT"], ["GPL-2.0", "MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0", "ISC"]] +// OR(AND)OR Expression: "MIT OR (ISC AND Apache-2.0) OR GPL-2.0" becomes +// [["MIT"], ["Apache-2.0", "ISC"], ["GPL-2.0"]] +// AND-OR-AND Expression: "MIT AND ISC OR Apache-2.0 AND GPL-2.0" becomes +// [["ISC", "MIT"], ["Apache-2.0", "GPL-2.0"]] +// (AND)OR(AND) Expression: "(MIT AND ISC) OR (Apache-2.0 AND GPL-2.0)" becomes +// [["ISC", "MIT"], ["Apache-2.0", "GPL-2.0"]] +// AND(OR)AND Expression: "MIT AND (ISC OR Apache-2.0) AND GPL-2.0" becomes +// [["GPL-2.0", "ISC", "MIT"], ["Apache-2.0", "GPL-2.0", "MIT"]] func (n *node) expand(withDeepSort bool) [][]*node { if n.isLicense() || n.isLicenseRef() { return [][]*node{{n}} @@ -157,7 +157,8 @@ func (n *node) expand(withDeepSort bool) [][]*node { // expandOr expands the given expression into an equivalent array representing ORed licenses each in a separate array. // // Example: -// OR Expression: "MIT OR Apache-2.0" becomes [["MIT"], ["Apache-2.0"]] +// +// OR Expression: "MIT OR Apache-2.0" becomes [["MIT"], ["Apache-2.0"]] func (n *node) expandOr() [][]*node { var result [][]*node result = expandOrTerm(n.left(), result) @@ -186,8 +187,10 @@ func expandOrTerm(term *node, result [][]*node) [][]*node { // expressions are combined with the ANDed expressions. // // Example: -// AND Expression: "MIT AND Apache-2.0" becomes [["MIT", "Apache-2.0"]] -// AND(OR) Expression: "MIT AND (Apache-2.0 OR GPL-2.0)" becomes [["Apache-2.0", "MIT], ["GPL-2.0", "MIT"]] +// +// AND Expression: "MIT AND Apache-2.0" becomes [["MIT", "Apache-2.0"]] +// AND(OR) Expression: "MIT AND (Apache-2.0 OR GPL-2.0)" becomes [["Apache-2.0", "MIT], ["GPL-2.0", "MIT"]] +// // See more examples under func expand. func (n *node) expandAnd() [][]*node { left := expandAndTerm(n.left()) @@ -224,8 +227,9 @@ func expandAndTerm(term *node) [][]*node { // producing more results than exists in the left or right results. // // Example: -// left: {{"MIT"}} right: {{"ISC"}, {"Apache-2.0"}} becomes -// {{"MIT", "ISC"}, {"MIT", "Apache-2.0"}} +// +// left: {{"MIT"}} right: {{"ISC"}, {"Apache-2.0"}} becomes +// {{"MIT", "ISC"}, {"MIT", "Apache-2.0"}} func appendTerms(left, right [][]*node) [][]*node { var result [][]*node for _, r := range right { @@ -243,8 +247,9 @@ func appendTerms(left, right [][]*node) [][]*node { // are merged left and right results. // // Example: -// left: {{"MIT"}} right: {{"ISC", "Apache-2.0"}} becomes -// {{"MIT", "ISC", "Apache-2.0"}} +// +// left: {{"MIT"}} right: {{"ISC", "Apache-2.0"}} becomes +// {{"MIT", "ISC", "Apache-2.0"}} func mergeTerms(left, right [][]*node) [][]*node { results := left for _, r := range right { @@ -277,8 +282,9 @@ func sortAndDedup(nodes []*node) []*node { // Then each array of nodes are sorted relative to the other arrays. // // Example: -// BEFORE {{"MIT", "GPL-2.0"}, {"ISC", "Apache-2.0"}} -// AFTER {{"Apache-2.0", "ISC"}, {"GPL-2.0", "MIT"}} +// +// BEFORE {{"MIT", "GPL-2.0"}, {"ISC", "Apache-2.0"}} +// AFTER {{"Apache-2.0", "ISC"}, {"GPL-2.0", "MIT"}} func deepSort(nodes2d [][]*node) [][]*node { if len(nodes2d) == 0 || len(nodes2d) == 1 && len(nodes2d[0]) <= 1 { return nodes2d diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 276542f..70b196b 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -2,14 +2,14 @@ package spdxexp import ( "errors" - "testing" "reflect" + "testing" "github.com/stretchr/testify/assert" ) func TestValidateLicenses(t *testing.T) { - someLicenses := []string {"MTI", "Apche-2.0", "0xDEADBEEF", ""} + someLicenses := []string{"MTI", "Apche-2.0", "0xDEADBEEF", ""} invalidLicenses := ValidateLicenses(someLicenses) assert.True(t, reflect.DeepEqual(someLicenses, invalidLicenses)) } diff --git a/spdxexp/scan.go b/spdxexp/scan.go index d1ffe0e..0af567c 100644 --- a/spdxexp/scan.go +++ b/spdxexp/scan.go @@ -225,11 +225,11 @@ func (exp *expressionStream) readLicense() *token { // Generate a token using the normalized form of the license name. // // License name can be in the form: -// * a_license-2.0, a_license, a_license-ab - there is variability in the form of the base license. a_license-2.0 is used for these -// examples, but any base license form can have the suffixes described. -// * a_license-2.0-only - normalizes to a_license-2.0 if the -only form is not specifically in the set of licenses -// * a_license-2.0-or-later - normalizes to a_license-2.0+ if the -or-later form is not specifically in the set of licenses -// * a_license-2.0+ - normalizes to a_license-2.0-or-later if the -or-later form is specifically in the set of licenses +// - a_license-2.0, a_license, a_license-ab - there is variability in the form of the base license. a_license-2.0 is used for these +// examples, but any base license form can have the suffixes described. +// - a_license-2.0-only - normalizes to a_license-2.0 if the -only form is not specifically in the set of licenses +// - a_license-2.0-or-later - normalizes to a_license-2.0+ if the -or-later form is not specifically in the set of licenses +// - a_license-2.0+ - normalizes to a_license-2.0-or-later if the -or-later form is specifically in the set of licenses func (exp *expressionStream) normalizeLicense(license string) *token { if token := licenseLookup(license); token != nil { // checks active and exception license lists From 272cedfc211bf4896606ca7b9eb8753b0e967a19 Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Mon, 24 Oct 2022 17:44:23 +0200 Subject: [PATCH 3/6] Adjust codestyle to better match golang's requirements * Get rid of named return values and naked return * Change function doc to start with function's name --- spdxexp/satisfies.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 9dd081e..001c28e 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -5,17 +5,18 @@ import ( "sort" ) -// Checks if given licenses are valid according to spdx +// ValidateLicenses checks if given licenses are valid according to spdx // // Returns all the invalid licenses contained in the `licenses` argument -func ValidateLicenses(licenses []string) (invalidLicenes []string) { +func ValidateLicenses(licenses []string) []string { + invalidLicenses := []string{} for _, license := range licenses { _, err := parse(license) if err != nil { - invalidLicenes = append(invalidLicenes, license) + invalidLicenses = append(invalidLicenses, license) } } - return + return invalidLicenses } // Satisfies determines if test license expression satisfies allowed list of licenses. From 2c05723a030b82a6bd98b9a2d58e25a6154a54be Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Tue, 25 Oct 2022 10:46:16 +0200 Subject: [PATCH 4/6] Change return type for ValidateLicenses, add more tests --- spdxexp/satisfies.go | 9 +++++---- spdxexp/satisfies_test.go | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 001c28e..38962da 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -8,15 +8,16 @@ import ( // ValidateLicenses checks if given licenses are valid according to spdx // // Returns all the invalid licenses contained in the `licenses` argument -func ValidateLicenses(licenses []string) []string { +func ValidateLicenses(licenses []string) (bool, []string) { + valid := true invalidLicenses := []string{} for _, license := range licenses { - _, err := parse(license) - if err != nil { + if _, err := parse(license); err != nil { + valid = false invalidLicenses = append(invalidLicenses, license) } } - return invalidLicenses + return valid, invalidLicenses } // Satisfies determines if test license expression satisfies allowed list of licenses. diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 70b196b..4b1d110 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -2,16 +2,30 @@ package spdxexp import ( "errors" - "reflect" "testing" "github.com/stretchr/testify/assert" ) func TestValidateLicenses(t *testing.T) { - someLicenses := []string{"MTI", "Apche-2.0", "0xDEADBEEF", ""} - invalidLicenses := ValidateLicenses(someLicenses) - assert.True(t, reflect.DeepEqual(someLicenses, invalidLicenses)) + tests := []struct { + name string + inputLicenses []string + allValid bool + invalidLicenses []string + }{ + {"All invalid", []string{"MTI", "Apche-2.0", "0xDEADBEEF", ""}, false, []string{"MTI", "Apche-2.0", "0xDEADBEEF", ""}}, + {"All valid", []string{"MIT", "Apache-2.0", "GPL-2.0"}, true, []string{}}, + {"Some invalid", []string{"MTI", "Apche-2.0", "GPL-2.0"}, false, []string{"MTI", "Apche-2.0"}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + valid, invalidLicenses := ValidateLicenses(test.inputLicenses) + assert.EqualValues(t, test.invalidLicenses, invalidLicenses) + assert.Equal(t, test.allValid, valid) + }) + } } func TestSatisfies(t *testing.T) { From 17bfa7fabd79a9d6f8783bdde8d5082c84cc6e0f Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Tue, 25 Oct 2022 14:19:01 +0200 Subject: [PATCH 5/6] Update readme to mention ValidateLicenses function --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c2fc8ad..012fc31 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ Golang implementation of a checker for determining if a set of SPDX IDs satisfie ## Public API -_NOTE: The public API is initially limited to the Satisfies function. If there is interest in the -output of the parser or license checking being public, please submit an issue for consideration._ +_NOTE: The public API is initially limited to the Satisfies and ValidateLicenses functions. If +there is interest in the output of the parser or license checking being public, please submit an +issue for consideration._ ### Function: Satisfies @@ -45,6 +46,10 @@ Example allowedList: []string{"GPL-2.0-or-later"} ``` +**N.B.** If at least one of expressions from `allowedList` is not a valid SPDX expression, the call +to `Satisfies` will produce an error. Use [`ValidateLicenses`](###-ValidateLicenses) function +to first check if all of the expressions from `allowedList` are valid. + #### Examples: Satisfies returns true ```go @@ -65,6 +70,43 @@ Satisfies("Apache-1.0", []string{"Apache-2.0+"}) Satisfies("MIT AND Apache-2.0", []string{"MIT"}) ``` +### ValidateLicenses + +```go +func ValidateLicenses(licenses []string) (bool, []string) +``` + +Function `ValidateLicenses` is used to determine if any of the provided license expressions is +invalid. + +**parameter: licenses** + +Licenses is a slice of strings which must be validated as SPDX expressions. + +**returns** + +Function `ValidateLicenses` has 2 return values. First is `bool` which equals `true` if all of +the provided licenses provided are valid, and `false` otherwise. + +The second parameter is a slice of all invalid licenses which were provided. + +#### Examples: ValidateLicenses returns no invalid licenses + +```go +valid, invalidLicenses := ValidateLicenses([]string{"Apache-2.0"}) +assert.True(valid) +assert.Empty(invalidLicenses) +``` + +#### Examples: ValidateLicenses returns invalid licenses + +```go +valid, invalidLicenses := ValidateLicenses([]string{"NON-EXISTENT-LICENSE", "MIT"}) +assert.False(valid) +assert.Contains(invalidLicenses, "NON-EXISTENT-LICENSE") +assert.NotContains(invalidLicenses, "MIT") +``` + ## Background This package was developed to support testing whether a repository's license requirements are met by an allowed-list of licenses. From 261c83bc77504843705ed1d5b4077992ce7de875 Mon Sep 17 00:00:00 2001 From: Roman Iakovlev <2363458+RomanIakovlev@users.noreply.github.com> Date: Tue, 25 Oct 2022 16:57:40 +0200 Subject: [PATCH 6/6] Update Godoc punctuation Co-authored-by: E. Lynette Rayle --- spdxexp/satisfies.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index 38962da..c4c81b0 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -5,9 +5,9 @@ import ( "sort" ) -// ValidateLicenses checks if given licenses are valid according to spdx +// ValidateLicenses checks if given licenses are valid according to spdx. // -// Returns all the invalid licenses contained in the `licenses` argument +// Returns all the invalid licenses contained in the `licenses` argument. func ValidateLicenses(licenses []string) (bool, []string) { valid := true invalidLicenses := []string{}