github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/syft/pkg/package.go (about) 1 /* 2 Package pkg provides the data structures for a package, a package catalog, package types, and domain-specific metadata. 3 */ 4 package pkg 5 6 import ( 7 "fmt" 8 "sort" 9 "strings" 10 11 "github.com/kastenhq/syft/internal/log" 12 "github.com/kastenhq/syft/syft/artifact" 13 "github.com/kastenhq/syft/syft/cpe" 14 "github.com/kastenhq/syft/syft/file" 15 ) 16 17 // Package represents an application or library that has been bundled into a distributable format. 18 // TODO: if we ignore FoundBy for ID generation should we merge the field to show it was found in two places? 19 type Package struct { 20 id artifact.ID `hash:"ignore"` 21 Name string // the package name 22 Version string // the version of the package 23 FoundBy string `hash:"ignore" cyclonedx:"foundBy"` // the specific cataloger that discovered this package 24 Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) 25 Licenses LicenseSet // licenses discovered with the package metadata 26 Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) 27 Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) 28 CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) 29 PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) 30 MetadataType MetadataType `cyclonedx:"metadataType"` // the shape of the additional data in the "metadata" field 31 Metadata interface{} // additional data found while parsing the package source 32 } 33 34 func (p *Package) OverrideID(id artifact.ID) { 35 p.id = id 36 } 37 38 func (p *Package) SetID() { 39 id, err := artifact.IDByHash(p) 40 if err != nil { 41 // TODO: what to do in this case? 42 log.Warnf("unable to get fingerprint of package=%s@%s: %+v", p.Name, p.Version, err) 43 return 44 } 45 p.id = id 46 } 47 48 func (p Package) ID() artifact.ID { 49 return p.id 50 } 51 52 // Stringer to represent a package. 53 func (p Package) String() string { 54 return fmt.Sprintf("Pkg(name=%q version=%q type=%q id=%q)", p.Name, p.Version, p.Type, p.id) 55 } 56 57 func (p *Package) merge(other Package) error { 58 if p.id != other.id { 59 return fmt.Errorf("cannot merge packages with different IDs: %q vs %q", p.id, other.id) 60 } 61 62 if p.PURL != other.PURL { 63 log.Warnf("merging packages have with different pURLs: %q=%q vs %q=%q", p.id, p.PURL, other.id, other.PURL) 64 } 65 66 p.Locations.Add(other.Locations.ToSlice()...) 67 p.Licenses.Add(other.Licenses.ToSlice()...) 68 69 p.CPEs = cpe.Merge(p.CPEs, other.CPEs) 70 71 if p.PURL == "" { 72 p.PURL = other.PURL 73 } 74 return nil 75 } 76 77 // IsValid checks whether a package has the minimum necessary info 78 // which is a non-empty name. 79 // The nil-check was added as a helper as often, in this code base, packages 80 // move between callers as pointers. 81 // CycloneDX and SPDX define Name as the minimum required info for a valid package: 82 // * https://spdx.github.io/spdx-spec/package-information/#73-package-version-field 83 // * https://cyclonedx.org/docs/1.4/json/#components_items_name 84 func IsValid(p *Package) bool { 85 return p != nil && p.Name != "" 86 } 87 88 //nolint:gocognit 89 func Less(i, j Package) bool { 90 if i.Name == j.Name { 91 if i.Version == j.Version { 92 iLocations := i.Locations.ToSlice() 93 jLocations := j.Locations.ToSlice() 94 if i.Type == j.Type { 95 maxLen := len(iLocations) 96 if len(jLocations) > maxLen { 97 maxLen = len(jLocations) 98 } 99 for l := 0; l < maxLen; l++ { 100 if len(iLocations) < l+1 || len(jLocations) < l+1 { 101 if len(iLocations) == len(jLocations) { 102 break 103 } 104 return len(iLocations) < len(jLocations) 105 } 106 if iLocations[l].RealPath == jLocations[l].RealPath { 107 continue 108 } 109 return iLocations[l].RealPath < jLocations[l].RealPath 110 } 111 // compare remaining metadata as a final fallback 112 // note: we cannot guarantee that IDs (which digests the metadata) are stable enough to sort on 113 // when there are potentially missing elements there is too much reduction in the dimensions to 114 // lean on ID comparison. The best fallback is to look at the string representation of the metadata. 115 return strings.Compare(fmt.Sprintf("%#v", i.Metadata), fmt.Sprintf("%#v", j.Metadata)) < 0 116 } 117 return i.Type < j.Type 118 } 119 return i.Version < j.Version 120 } 121 return i.Name < j.Name 122 } 123 func Sort(pkgs []Package) { 124 sort.SliceStable(pkgs, func(i, j int) bool { 125 return Less(pkgs[i], pkgs[j]) 126 }) 127 }