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