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 }