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 }