syft/internal/spdxlicense/generate/generate_license_list.go
Jonas Xavier 69fb0a6f3b
Overwrite deprecated SPDX licenses automatically (#1009)
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2022-08-02 15:25:33 -04:00

161 lines
4.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"regexp"
"sort"
"strings"
"text/template"
"time"
"github.com/scylladb/go-set/strset"
)
// This program generates license_list.go.
const (
source = "license_list.go"
url = "https://spdx.org/licenses/licenses.json"
)
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at {{ .Timestamp }}
// using data from {{ .URL }}
package spdxlicense
const Version = {{ printf "%q" .Version }}
var licenseIDs = map[string]string{
{{- range $k, $v := .LicenseIDs }}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end }}
}
`))
var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
func main() {
if err := run(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
func run() error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("unable to get licenses list: %+v", err)
}
var result LicenseList
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("unable to decode license list: %+v", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Fatalf("unable to close body: %+v", err)
}
}()
f, err := os.Create(source)
if err != nil {
return fmt.Errorf("unable to create %q: %+v", source, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("unable to close %q: %+v", source, err)
}
}()
licenseIDs := processSPDXLicense(result)
err = tmp.Execute(f, struct {
Timestamp time.Time
URL string
Version string
LicenseIDs map[string]string
}{
Timestamp: time.Now(),
URL: url,
Version: result.Version,
LicenseIDs: licenseIDs,
})
if err != nil {
return fmt.Errorf("unable to generate template: %+v", err)
}
return nil
}
// Parsing the provided SPDX license list necessitates a three pass approach.
// The first pass is only related to what SPDX considers the truth. We use license info to
// find replacements for deprecated licenses.
// The second pass attempts to generate known short/long version listings for each key.
// For info on some short name conventions see this document:
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name.
// The short long listing generation attempts to build all license permutations for a given key.
// The new keys are then also associated with their relative SPDX value. If a key has already been entered
// we know to ignore it since it came from the first pass which is considered the SPDX source of truth.
// We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`.
// The third pass is for overwriting deprecated licenses with replacements, for example GPL-2.0+ is deprecated
// and now maps to GPL-2.0-or-later.
func processSPDXLicense(result LicenseList) map[string]string {
// first pass build map
var licenseIDs = make(map[string]string)
for _, l := range result.Licenses {
cleanID := strings.ToLower(l.ID)
if _, exists := licenseIDs[cleanID]; exists {
log.Fatalf("duplicate license ID found: %q", cleanID)
}
licenseIDs[cleanID] = l.ID
}
// The order of variations/permutations of a license ID matters because of we how shuffle its digits,
// that is because the permutation code can generate the same value for two difference licenses,
// for example: The licenses `ABC-1.0` and `ABC-1.1` can both map to `ABC-1`,
// so we need to guarantee the order they are created to avoid mapping them wrongly. So we use a sorted list.
// To overwrite deprecated licenses during the first pass we would later on rely on map order,
// [which in go is not consistent by design](https://stackoverflow.com/a/55925880).
sort.Slice(result.Licenses, func(i, j int) bool {
return result.Licenses[i].ID < result.Licenses[j].ID
})
// second pass to build exceptions and replacements
replaced := strset.New()
for _, l := range result.Licenses {
var multipleID []string
cleanID := strings.ToLower(l.ID)
var replacement *License
if l.Deprecated {
replacement = result.findReplacementLicense(l)
if replacement != nil {
licenseIDs[cleanID] = replacement.ID
}
}
multipleID = append(multipleID, buildLicensePermutations(cleanID)...)
for _, id := range multipleID {
// don't make replacements for IDs that have already been replaced. Since we have a sorted license list
// the earliest replacement is correct (any future replacements are not.
// e.g. replace lgpl-2 with LGPL-2.1-only is wrong, but with LGPL-2.0-only is correct)
if replacement == nil || replaced.Has(id) {
if _, exists := licenseIDs[id]; !exists {
licenseIDs[id] = l.ID
}
} else {
// a useful debugging line during builds
log.Printf("replacing %s with %s\n", id, replacement.ID)
licenseIDs[id] = replacement.ID
replaced.Add(id)
}
}
}
return licenseIDs
}