github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/license.go (about)

     1  package pkg
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/scylladb/go-set/strset"
     8  
     9  	"github.com/anchore/syft/internal/log"
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/license"
    13  )
    14  
    15  var _ sort.Interface = (*Licenses)(nil)
    16  
    17  // License represents an SPDX Expression or license value extracted from a packages metadata
    18  // We want to ignore URLs and Location since we merge these fields across equal licenses.
    19  // A License is a unique combination of value, expression and type, where
    20  // its sources are always considered merged and additions to the evidence
    21  // of where it was found and how it was sourced.
    22  // This is different from how we treat a package since we consider package paths
    23  // in order to distinguish if packages should be kept separate
    24  // this is different for licenses since we're only looking for evidence
    25  // of where a license was declared/concluded for a given package
    26  type License struct {
    27  	Value          string
    28  	SPDXExpression string
    29  	Type           license.Type
    30  	URLs           []string         `hash:"ignore"`
    31  	Locations      file.LocationSet `hash:"ignore"`
    32  }
    33  
    34  type Licenses []License
    35  
    36  func (l Licenses) Len() int {
    37  	return len(l)
    38  }
    39  
    40  func (l Licenses) Less(i, j int) bool {
    41  	if l[i].Value == l[j].Value {
    42  		if l[i].SPDXExpression == l[j].SPDXExpression {
    43  			if l[i].Type == l[j].Type {
    44  				// While URLs and location are not exclusive fields
    45  				// returning true here reduces the number of swaps
    46  				// while keeping a consistent sort order of
    47  				// the order that they appear in the list initially
    48  				// If users in the future have preference to sorting based
    49  				// on the slice representation of either field we can update this code
    50  				return true
    51  			}
    52  			return l[i].Type < l[j].Type
    53  		}
    54  		return l[i].SPDXExpression < l[j].SPDXExpression
    55  	}
    56  	return l[i].Value < l[j].Value
    57  }
    58  
    59  func (l Licenses) Swap(i, j int) {
    60  	l[i], l[j] = l[j], l[i]
    61  }
    62  
    63  func NewLicense(value string) License {
    64  	return NewLicenseFromType(value, license.Declared)
    65  }
    66  
    67  func NewLicenseFromType(value string, t license.Type) License {
    68  	var spdxExpression string
    69  	if value != "" {
    70  		var err error
    71  		spdxExpression, err = license.ParseExpression(value)
    72  		if err != nil {
    73  			log.Trace("unable to parse license expression: %w", err)
    74  		}
    75  	}
    76  
    77  	return License{
    78  		Value:          value,
    79  		SPDXExpression: spdxExpression,
    80  		Type:           t,
    81  		Locations:      file.NewLocationSet(),
    82  	}
    83  }
    84  
    85  func NewLicensesFromValues(values ...string) (licenses []License) {
    86  	for _, v := range values {
    87  		licenses = append(licenses, NewLicense(v))
    88  	}
    89  	return
    90  }
    91  
    92  func NewLicensesFromLocation(location file.Location, values ...string) (licenses []License) {
    93  	for _, v := range values {
    94  		if v == "" {
    95  			continue
    96  		}
    97  		licenses = append(licenses, NewLicenseFromLocations(v, location))
    98  	}
    99  	return
   100  }
   101  
   102  func NewLicenseFromLocations(value string, locations ...file.Location) License {
   103  	l := NewLicense(value)
   104  	for _, loc := range locations {
   105  		l.Locations.Add(loc)
   106  	}
   107  	return l
   108  }
   109  
   110  func NewLicenseFromURLs(value string, urls ...string) License {
   111  	l := NewLicense(value)
   112  	s := strset.New()
   113  	for _, url := range urls {
   114  		if url != "" {
   115  			s.Add(url)
   116  		}
   117  	}
   118  
   119  	l.URLs = s.List()
   120  	sort.Strings(l.URLs)
   121  
   122  	return l
   123  }
   124  
   125  func NewLicenseFromFields(value, url string, location *file.Location) License {
   126  	l := NewLicense(value)
   127  	if location != nil {
   128  		l.Locations.Add(*location)
   129  	}
   130  	if url != "" {
   131  		l.URLs = append(l.URLs, url)
   132  	}
   133  
   134  	return l
   135  }
   136  
   137  // Merge two licenses into a new license object. If the merge is not possible due to unmergeable fields
   138  // (e.g. different values for Value, SPDXExpression, Type, or any non-collection type) an error is returned.
   139  // TODO: this is a bit of a hack to not infinitely recurse when hashing a license
   140  func (s License) Merge(l License) (*License, error) {
   141  	sHash, err := artifact.IDByHash(s)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	lHash, err := artifact.IDByHash(l)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	if sHash != lHash {
   150  		return nil, fmt.Errorf("cannot merge licenses with different hash")
   151  	}
   152  
   153  	// try to keep s.URLs unallocated unless necessary (which is the default state from the constructor)
   154  	if len(l.URLs) > 0 {
   155  		s.URLs = append(s.URLs, l.URLs...)
   156  	}
   157  
   158  	if len(s.URLs) > 0 {
   159  		s.URLs = strset.New(s.URLs...).List()
   160  		sort.Strings(s.URLs)
   161  	}
   162  
   163  	if l.Locations.Empty() {
   164  		return &s, nil
   165  	}
   166  
   167  	// since the set instance has a reference type (map) we must make a new instance
   168  	locations := file.NewLocationSet(s.Locations.ToSlice()...)
   169  	locations.Add(l.Locations.ToSlice()...)
   170  	s.Locations = locations
   171  
   172  	return &s, nil
   173  }