github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/common/cyclonedxhelpers/licenses.go (about)

     1  package cyclonedxhelpers
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/CycloneDX/cyclonedx-go"
     8  	"github.com/nextlinux/gosbom/gosbom/pkg"
     9  	"github.com/nextlinux/gosbom/internal/spdxlicense"
    10  )
    11  
    12  // This should be a function that just surfaces licenses already validated in the package struct
    13  func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
    14  	spdx, other, ex := separateLicenses(p)
    15  	out := spdx
    16  	out = append(out, other...)
    17  
    18  	if len(other) > 0 || len(spdx) > 0 {
    19  		// found non spdx related licenses
    20  		// build individual license choices for each
    21  		// complex expressions are not combined and set as NAME fields
    22  		for _, e := range ex {
    23  			if e == "" {
    24  				continue
    25  			}
    26  			out = append(out, cyclonedx.LicenseChoice{
    27  				License: &cyclonedx.License{
    28  					Name: e,
    29  				},
    30  			})
    31  		}
    32  	} else if len(ex) > 0 {
    33  		// only expressions found
    34  		e := mergeSPDX(ex)
    35  		if e != "" {
    36  			out = append(out, cyclonedx.LicenseChoice{
    37  				Expression: e,
    38  			})
    39  		}
    40  	}
    41  
    42  	if len(out) > 0 {
    43  		return &out
    44  	}
    45  
    46  	return nil
    47  }
    48  
    49  func decodeLicenses(c *cyclonedx.Component) []pkg.License {
    50  	licenses := make([]pkg.License, 0)
    51  	if c == nil || c.Licenses == nil {
    52  		return licenses
    53  	}
    54  
    55  	for _, l := range *c.Licenses {
    56  		if l.License == nil {
    57  			continue
    58  		}
    59  		// these fields are mutually exclusive in the spec
    60  		switch {
    61  		case l.License.ID != "":
    62  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
    63  		case l.License.Name != "":
    64  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
    65  		case l.Expression != "":
    66  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.Expression, l.License.URL))
    67  		default:
    68  		}
    69  	}
    70  
    71  	return licenses
    72  }
    73  
    74  // nolint:funlen
    75  func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) {
    76  	ex := make([]string, 0)
    77  	spdxc := cyclonedx.Licenses{}
    78  	otherc := cyclonedx.Licenses{}
    79  	/*
    80  			pkg.License can be a couple of things: see above declarations
    81  			- Complex SPDX expression
    82  			- Some other Valid license ID
    83  			- Some non-standard non spdx license
    84  
    85  			To determine if an expression is a singular ID we first run it against the SPDX license list.
    86  
    87  		The weird case we run into is if there is a package with a license that is not a valid SPDX expression
    88  			and a license that is a valid complex expression. In this case we will surface the valid complex expression
    89  			as a license choice and the invalid expression as a license string.
    90  
    91  	*/
    92  	seen := make(map[string]bool)
    93  	for _, l := range p.Licenses.ToSlice() {
    94  		// singular expression case
    95  		// only ID field here since we guarantee that the license is valid
    96  		if value, exists := spdxlicense.ID(l.SPDXExpression); exists {
    97  			if !l.URLs.Empty() {
    98  				processLicenseURLs(l, value, &spdxc)
    99  				continue
   100  			}
   101  
   102  			if _, exists := seen[value]; exists {
   103  				continue
   104  			}
   105  			// try making set of license choices to avoid duplicates
   106  			// only update if the license has more information
   107  			spdxc = append(spdxc, cyclonedx.LicenseChoice{
   108  				License: &cyclonedx.License{
   109  					ID: value,
   110  				},
   111  			})
   112  			seen[value] = true
   113  			// we have added the license to the SPDX license list check next license
   114  			continue
   115  		}
   116  
   117  		if l.SPDXExpression != "" {
   118  			// COMPLEX EXPRESSION CASE
   119  			ex = append(ex, l.SPDXExpression)
   120  			continue
   121  		}
   122  
   123  		// license string that are not valid spdx expressions or ids
   124  		// we only use license Name here since we cannot guarantee that the license is a valid SPDX expression
   125  		if !l.URLs.Empty() {
   126  			processLicenseURLs(l, "", &otherc)
   127  			continue
   128  		}
   129  		otherc = append(otherc, cyclonedx.LicenseChoice{
   130  			License: &cyclonedx.License{
   131  				Name: l.Value,
   132  			},
   133  		})
   134  	}
   135  	return spdxc, otherc, ex
   136  }
   137  
   138  func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
   139  	for _, url := range l.URLs.ToSlice() {
   140  		if spdxID == "" {
   141  			*populate = append(*populate, cyclonedx.LicenseChoice{
   142  				License: &cyclonedx.License{
   143  					URL:  url,
   144  					Name: l.Value,
   145  				},
   146  			})
   147  		} else {
   148  			*populate = append(*populate, cyclonedx.LicenseChoice{
   149  				License: &cyclonedx.License{
   150  					ID:  spdxID,
   151  					URL: url,
   152  				},
   153  			})
   154  		}
   155  	}
   156  }
   157  
   158  func mergeSPDX(ex []string) string {
   159  	var candidate []string
   160  	for _, e := range ex {
   161  		// if the expression does not have balanced parens add them
   162  		if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
   163  			e = "(" + e + ")"
   164  			candidate = append(candidate, e)
   165  		}
   166  	}
   167  
   168  	if len(candidate) == 1 {
   169  		return reduceOuter(strings.Join(candidate, " AND "))
   170  	}
   171  
   172  	return strings.Join(candidate, " AND ")
   173  }
   174  
   175  func reduceOuter(expression string) string {
   176  	var (
   177  		sb        strings.Builder
   178  		openCount int
   179  	)
   180  
   181  	for _, c := range expression {
   182  		if string(c) == "(" && openCount > 0 {
   183  			_, _ = fmt.Fprintf(&sb, "%c", c)
   184  		}
   185  		if string(c) == "(" {
   186  			openCount++
   187  			continue
   188  		}
   189  		if string(c) == ")" && openCount > 1 {
   190  			_, _ = fmt.Fprintf(&sb, "%c", c)
   191  		}
   192  		if string(c) == ")" {
   193  			openCount--
   194  			continue
   195  		}
   196  		_, _ = fmt.Fprintf(&sb, "%c", c)
   197  	}
   198  
   199  	return sb.String()
   200  }