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 }