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