github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/r/package.go (about)

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