github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/internal/spdxlicense/generate/generate_license_list.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"os"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  	"text/template"
    13  	"time"
    14  )
    15  
    16  // This program generates license_list.go.
    17  const (
    18  	source = "license_list.go"
    19  	url    = "https://spdx.org/licenses/licenses.json"
    20  )
    21  
    22  var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
    23  // This file was generated by robots at {{ .Timestamp }}
    24  // using data from {{ .URL }}
    25  package spdxlicense
    26  
    27  const Version = {{ printf "%q" .Version }}
    28  
    29  var licenseIDs = map[string]string{
    30  {{- range $k, $v := .LicenseIDs }}
    31  	{{ printf "%q" $k }}: {{ printf "%q" $v }},
    32  {{- end }}
    33  }
    34  `))
    35  
    36  var versionMatch = regexp.MustCompile(`([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
    37  
    38  func main() {
    39  	if err := run(); err != nil {
    40  		fmt.Println(err.Error())
    41  		os.Exit(1)
    42  	}
    43  }
    44  
    45  func run() error {
    46  	resp, err := http.Get(url)
    47  	if err != nil {
    48  		return fmt.Errorf("unable to get licenses list: %w", err)
    49  	}
    50  	var result LicenseList
    51  	if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
    52  		return fmt.Errorf("unable to decode license list: %w", err)
    53  	}
    54  	defer func() {
    55  		if err := resp.Body.Close(); err != nil {
    56  			log.Fatalf("unable to close body: %+v", err)
    57  		}
    58  	}()
    59  
    60  	f, err := os.Create(source)
    61  	if err != nil {
    62  		return fmt.Errorf("unable to create %q: %w", source, err)
    63  	}
    64  	defer func() {
    65  		if err := f.Close(); err != nil {
    66  			log.Fatalf("unable to close %q: %+v", source, err)
    67  		}
    68  	}()
    69  
    70  	licenseIDs := processSPDXLicense(result)
    71  
    72  	err = tmp.Execute(f, struct {
    73  		Timestamp  time.Time
    74  		URL        string
    75  		Version    string
    76  		LicenseIDs map[string]string
    77  	}{
    78  		Timestamp:  time.Now(),
    79  		URL:        url,
    80  		Version:    result.Version,
    81  		LicenseIDs: licenseIDs,
    82  	})
    83  
    84  	if err != nil {
    85  		return fmt.Errorf("unable to generate template: %w", err)
    86  	}
    87  	return nil
    88  }
    89  
    90  // Parsing the provided SPDX license list necessitates a three pass approach.
    91  // The first pass is only related to what SPDX considers the truth. We use license info to
    92  // find replacements for deprecated licenses.
    93  // The second pass attempts to generate known short/long version listings for each key.
    94  // For info on some short name conventions see this document:
    95  // https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name.
    96  // The short long listing generation attempts to build all license permutations for a given key.
    97  // The new keys are then also associated with their relative SPDX value. If a key has already been entered
    98  // we know to ignore it since it came from the first pass which is considered the SPDX source of truth.
    99  // 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`.
   100  // The third pass is for overwriting deprecated licenses with replacements, for example GPL-2.0+ is deprecated
   101  // and now maps to GPL-2.0-or-later.
   102  func processSPDXLicense(result LicenseList) map[string]string {
   103  	// The order of variations/permutations of a license ID matter.
   104  	// The permutation code can generate the same value for two difference licenses,
   105  	// for example: The licenses `ABC-1.0` and `ABC-1.1` can both map to `ABC1`,
   106  	// we need to guarantee the order they are created to avoid mapping them incorrectly.
   107  	// To do this we use a sorted list.
   108  	sort.Slice(result.Licenses, func(i, j int) bool {
   109  		return result.Licenses[i].ID < result.Licenses[j].ID
   110  	})
   111  
   112  	// keys are simplified by removing dashes and lowercasing ID
   113  	// this is so license declarations in the wild like: LGPL3 LGPL-3 lgpl3 and lgpl-3 can all match
   114  	licenseIDs := make(map[string]string)
   115  	for _, l := range result.Licenses {
   116  		// licensePerms includes the cleanID in return slice
   117  		cleanID := cleanLicenseID(l.ID)
   118  		licensePerms := buildLicenseIDPermutations(cleanID)
   119  
   120  		// if license is deprecated, find its replacement and add to licenseIDs
   121  		if l.Deprecated {
   122  			idToMap := l.ID
   123  			replacement := result.findReplacementLicense(l)
   124  			if replacement != nil {
   125  				idToMap = replacement.ID
   126  			}
   127  			// it's important to use the original licensePerms here so that the deprecated license
   128  			// can now point to the new correct license
   129  			for _, id := range licensePerms {
   130  				if _, exists := licenseIDs[id]; exists {
   131  					// can be used to debug duplicate license permutations and confirm that examples like GPL1
   132  					// do not point to GPL-1.1
   133  					// log.Println("duplicate license list permutation found when mapping deprecated license to replacement")
   134  					// log.Printf("already have key: %q for SPDX ID: %q; attempted to map replacement ID: %q for deprecated ID: %q\n", id, value, replacement.ID, l.ID)
   135  					continue
   136  				}
   137  				licenseIDs[id] = idToMap
   138  			}
   139  		}
   140  
   141  		// if license is not deprecated, add all permutations to licenseIDs
   142  		for _, id := range licensePerms {
   143  			if _, exists := licenseIDs[id]; exists {
   144  				// log.Println("found duplicate license permutation key for non deprecated license")
   145  				// log.Printf("already have key: %q for SPDX ID: %q; tried to insert as SPDX ID:%q\n", id, value, l.ID)
   146  				continue
   147  			}
   148  			licenseIDs[id] = l.ID
   149  		}
   150  	}
   151  
   152  	return licenseIDs
   153  }
   154  
   155  func cleanLicenseID(id string) string {
   156  	cleanID := strings.ToLower(id)
   157  	return strings.ReplaceAll(cleanID, "-", "")
   158  }