github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/pkg/cataloger/r/package.go (about)

     1  package r
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/nextlinux/gosbom/gosbom/file"
     7  	"github.com/nextlinux/gosbom/gosbom/pkg"
     8  
     9  	"github.com/anchore/packageurl-go"
    10  )
    11  
    12  func newPackage(pd parseData, locations ...file.Location) pkg.Package {
    13  	locationSet := file.NewLocationSet()
    14  	for _, loc := range locations {
    15  		locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
    16  	}
    17  
    18  	licenses := parseLicenseData(pd.License)
    19  
    20  	result := pkg.Package{
    21  		Name:         pd.Package,
    22  		Version:      pd.Version,
    23  		Locations:    locationSet,
    24  		Licenses:     pkg.NewLicenseSet(licenses...),
    25  		Language:     pkg.R,
    26  		Type:         pkg.Rpkg,
    27  		PURL:         packageURL(pd),
    28  		MetadataType: pkg.RDescriptionFileMetadataType,
    29  		Metadata:     pd.RDescriptionFileMetadata,
    30  	}
    31  
    32  	result.SetID()
    33  	return result
    34  }
    35  
    36  func packageURL(m parseData) string {
    37  	return packageurl.NewPackageURL("cran", "", m.Package, m.Version, nil, "").ToString()
    38  }
    39  
    40  // https://r-pkgs.org/description.html#the-license-field
    41  // four forms:
    42  // 1. "GPL (>= 2)"
    43  // 2. "GPL-2"
    44  // 3. "MIT + file LICENSE"
    45  // 4. "pointer to the full text of the license; file LICENSE"
    46  // Multiple licences can be specified separated by ‘|’
    47  // (surrounded by spaces) in which case the user can choose any of the above cases.
    48  // https://cran.rstudio.com/doc/manuals/r-devel/R-exts.html#Licensing
    49  func parseLicenseData(license string, locations ...file.Location) []pkg.License {
    50  	licenses := make([]pkg.License, 0)
    51  
    52  	// check if multiple licenses are separated by |
    53  	splitField := strings.Split(license, "|")
    54  	for _, l := range splitField {
    55  		// check case 1 for surrounding parens
    56  		l = strings.TrimSpace(l)
    57  		if strings.Contains(l, "(") && strings.Contains(l, ")") {
    58  			licenseVersion := strings.SplitN(l, " ", 2)
    59  			if len(licenseVersion) == 2 {
    60  				l = strings.Join([]string{licenseVersion[0], parseVersion(licenseVersion[1])}, "")
    61  				licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...))
    62  				continue
    63  			}
    64  		}
    65  
    66  		// case 3
    67  		if strings.Contains(l, "+") && strings.Contains(l, "LICENSE") {
    68  			splitField := strings.Split(l, " ")
    69  			if len(splitField) > 0 {
    70  				licenses = append(licenses, pkg.NewLicenseFromLocations(splitField[0], locations...))
    71  				continue
    72  			}
    73  		}
    74  
    75  		// TODO: case 4 if we are able to read the location data and find the adjacent file?
    76  		if l == "file LICENSE" {
    77  			continue
    78  		}
    79  
    80  		// no specific case found for the above so assume case 2
    81  		// check if the common name in case 2 is valid SDPX otherwise value will be populated
    82  		licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...))
    83  		continue
    84  	}
    85  	return licenses
    86  }
    87  
    88  // attempt to make best guess at SPDX license ID from version operator in case 2
    89  /*
    90  ‘<’, ‘<=’, ‘>’, ‘>=’, ‘==’, or ‘!=’
    91  cant be (>= 2.0) OR (>= 2.0, < 3)
    92  since there is no way in SPDX licenses to express < some other version
    93  we attempt to check the constraint to see if this should be + or not
    94  */
    95  func parseVersion(version string) string {
    96  	version = strings.ReplaceAll(version, "(", "")
    97  	version = strings.ReplaceAll(version, ")", "")
    98  
    99  	// multiple constraints
   100  	if strings.Contains(version, ",") {
   101  		multipleConstraints := strings.Split(version, ",")
   102  		// SPDX does not support considering multiple constraints
   103  		// so we will just take the first one and attempt to form the best SPDX ID we can
   104  		for _, v := range multipleConstraints {
   105  			constraintVersion := strings.SplitN(v, " ", 2)
   106  			if len(constraintVersion) == 2 {
   107  				// switch on the operator and return the version with + or without
   108  				switch constraintVersion[0] {
   109  				case ">", ">=":
   110  					return constraintVersion[1] + "+"
   111  				default:
   112  					return constraintVersion[1]
   113  				}
   114  			}
   115  		}
   116  	}
   117  	// single constraint
   118  	singleContraint := strings.Split(version, " ")
   119  	if len(singleContraint) == 2 {
   120  		switch singleContraint[0] {
   121  		case ">", ">=":
   122  			return singleContraint[1] + "+"
   123  		default:
   124  			return singleContraint[1]
   125  		}
   126  	}
   127  
   128  	// could not parse version constraint so return ""
   129  	return ""
   130  }