github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/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/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/cpe" 14 "github.com/anchore/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 ComponentType ComponentType `cyclonedx:"type"` // the type of component (e.g. application, library, framework, etc) 28 Type Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) 29 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) 30 PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) 31 MetadataType MetadataType `cyclonedx:"metadataType"` // the shape of the additional data in the "metadata" field 32 Metadata interface{} // additional data found while parsing the package source 33 } 34 35 func (p *Package) OverrideID(id artifact.ID) { 36 p.id = id 37 } 38 39 func (p *Package) SetID() { 40 id, err := artifact.IDByHash(p) 41 if err != nil { 42 // TODO: what to do in this case? 43 log.Warnf("unable to get fingerprint of package=%s@%s: %+v", p.Name, p.Version, err) 44 return 45 } 46 p.id = id 47 } 48 49 func (p Package) ID() artifact.ID { 50 return p.id 51 } 52 53 // Stringer to represent a package. 54 func (p Package) String() string { 55 return fmt.Sprintf("Pkg(name=%q version=%q type=%q id=%q)", p.Name, p.Version, p.Type, p.id) 56 } 57 58 func (p *Package) merge(other Package) error { 59 if p.id != other.id { 60 return fmt.Errorf("cannot merge packages with different IDs: %q vs %q", p.id, other.id) 61 } 62 63 if p.PURL != other.PURL { 64 log.Warnf("merging packages have with different pURLs: %q=%q vs %q=%q", p.id, p.PURL, other.id, other.PURL) 65 } 66 67 p.Locations.Add(other.Locations.ToSlice()...) 68 p.Licenses.Add(other.Licenses.ToSlice()...) 69 70 p.CPEs = cpe.Merge(p.CPEs, other.CPEs) 71 72 if p.PURL == "" { 73 p.PURL = other.PURL 74 } 75 return nil 76 } 77 78 // IsValid checks whether a package has the minimum necessary info 79 // which is a non-empty name. 80 // The nil-check was added as a helper as often, in this code base, packages 81 // move between callers as pointers. 82 // CycloneDX and SPDX define Name as the minimum required info for a valid package: 83 // * https://spdx.github.io/spdx-spec/package-information/#73-package-version-field 84 // * https://cyclonedx.org/docs/1.4/json/#components_items_name 85 func IsValid(p *Package) bool { 86 return p != nil && p.Name != "" 87 } 88 89 //nolint:gocognit 90 func Less(i, j Package) bool { 91 if i.Name == j.Name { 92 if i.Version == j.Version { 93 iLocations := i.Locations.ToSlice() 94 jLocations := j.Locations.ToSlice() 95 if i.Type == j.Type { 96 maxLen := len(iLocations) 97 if len(jLocations) > maxLen { 98 maxLen = len(jLocations) 99 } 100 for l := 0; l < maxLen; l++ { 101 if len(iLocations) < l+1 || len(jLocations) < l+1 { 102 if len(iLocations) == len(jLocations) { 103 break 104 } 105 return len(iLocations) < len(jLocations) 106 } 107 if iLocations[l].RealPath == jLocations[l].RealPath { 108 continue 109 } 110 return iLocations[l].RealPath < jLocations[l].RealPath 111 } 112 // compare remaining metadata as a final fallback 113 // note: we cannot guarantee that IDs (which digests the metadata) are stable enough to sort on 114 // when there are potentially missing elements there is too much reduction in the dimensions to 115 // lean on ID comparison. The best fallback is to look at the string representation of the metadata. 116 return strings.Compare(fmt.Sprintf("%#v", i.Metadata), fmt.Sprintf("%#v", j.Metadata)) < 0 117 } 118 return i.Type < j.Type 119 } 120 return i.Version < j.Version 121 } 122 return i.Name < j.Name 123 } 124 func Sort(pkgs []Package) { 125 sort.SliceStable(pkgs, func(i, j int) bool { 126 return Less(pkgs[i], pkgs[j]) 127 }) 128 }