github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/dart/parse_pubspec_lock.go (about) 1 package dart 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "regexp" 8 "sort" 9 10 "go.yaml.in/yaml/v3" 11 12 "github.com/anchore/syft/internal/log" 13 "github.com/anchore/syft/internal/unknown" 14 "github.com/anchore/syft/syft/artifact" 15 "github.com/anchore/syft/syft/file" 16 "github.com/anchore/syft/syft/pkg" 17 "github.com/anchore/syft/syft/pkg/cataloger/generic" 18 ) 19 20 var _ generic.Parser = parsePubspecLock 21 22 const defaultPubRegistry string = "https://pub.dartlang.org" 23 24 type pubspecLock struct { 25 Packages map[string]pubspecLockPackage `yaml:"packages"` 26 Sdks map[string]string `yaml:"sdks"` 27 } 28 29 type pubspecLockPackage struct { 30 Dependency string `yaml:"dependency" mapstructure:"dependency"` 31 Description pubspecLockDescription `yaml:"description" mapstructure:"description"` 32 Source string `yaml:"source" mapstructure:"source"` 33 Version string `yaml:"version" mapstructure:"version"` 34 } 35 36 type pubspecLockDescription struct { 37 Name string `yaml:"name" mapstructure:"name"` 38 URL string `yaml:"url" mapstructure:"url"` 39 Path string `yaml:"path" mapstructure:"path"` 40 Ref string `yaml:"ref" mapstructure:"ref"` 41 ResolvedRef string `yaml:"resolved-ref" mapstructure:"resolved-ref"` 42 } 43 44 func (p *pubspecLockDescription) UnmarshalYAML(value *yaml.Node) error { 45 type pld pubspecLockDescription 46 var p2 pld 47 48 if value.Decode(&p.Name) == nil { 49 return nil 50 } 51 52 if err := value.Decode(&p2); err != nil { 53 return err 54 } 55 56 *p = pubspecLockDescription(p2) 57 58 return nil 59 } 60 61 func parsePubspecLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 62 var pkgs []pkg.Package 63 64 dec := yaml.NewDecoder(reader) 65 66 var p pubspecLock 67 if err := dec.Decode(&p); err != nil { 68 return nil, nil, fmt.Errorf("failed to parse pubspec.lock file: %w", err) 69 } 70 71 var names []string 72 for name, pkg := range p.Packages { 73 if pkg.Source == "sdk" && pkg.Version == "0.0.0" { 74 // Packages that are delivered as part of an SDK (e.g. Flutter) have their 75 // version set to "0.0.0" in the package definition. The actual version 76 // should refer to the SDK version, which is defined in a dedicated section 77 // in the pubspec.lock file and uses a version range constraint. 78 // 79 // If such a package is detected, look up the version range constraint of 80 // its matching SDK, and set the minimum supported version as its new version. 81 sdkName := pkg.Description.Name 82 sdkVersion, err := p.getSdkVersion(sdkName) 83 84 if err != nil { 85 log.Tracef("failed to resolve %s SDK version for package %s: %v", sdkName, name, err) 86 continue 87 } 88 pkg.Version = sdkVersion 89 p.Packages[name] = pkg 90 } 91 92 names = append(names, name) 93 } 94 95 // always ensure there is a stable ordering of packages 96 sort.Strings(names) 97 98 for _, name := range names { 99 pubPkg := p.Packages[name] 100 pkgs = append(pkgs, 101 newPubspecLockPackage( 102 name, 103 pubPkg, 104 reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 105 ), 106 ) 107 } 108 109 return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages") 110 } 111 112 // Look up the version range constraint for a given sdk name, if found, 113 // and return its lowest supported version matching that constraint. 114 // 115 // The sdks and their constraints are defined in the pubspec.lock file, e.g. 116 // 117 // sdks: 118 // dart: ">=2.12.0 <3.0.0" 119 // flutter: ">=3.24.5" 120 // 121 // and stored in the pubspecLock.Sdks map during parsing. 122 // 123 // Example based on the data above: 124 // 125 // getSdkVersion("dart") -> "2.12.0" 126 // getSdkVersion("flutter") -> "3.24.5" 127 // getSdkVersion("undefined") -> error 128 func (psl *pubspecLock) getSdkVersion(sdk string) (string, error) { 129 constraint, found := psl.Sdks[sdk] 130 131 if !found { 132 return "", fmt.Errorf("cannot find %s SDK", sdk) 133 } 134 135 return parseMinimumSdkVersion(constraint) 136 } 137 138 // semverRegex is a regex pattern that allows for both two-part (major.minor) and three-part (major.minor.patch) versions. 139 // additionally allows for: 140 // 1. start with either "^" or ">=" (Dart SDK constraints only use those two) 141 // 2. followed by a valid semantic version (which may be two or three components) 142 // 3. followed by a space (if there's a range) or end of string 143 var semverRegex = regexp.MustCompile(`^(\^|>=)(?P<version>(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?(?:-[0-9A-Za-z\-\.]+)?(?:\+[0-9A-Za-z\-\.]+)?)( |$)`) 144 145 // Parse a given version range constraint and return its lowest supported version. 146 // 147 // This is intended for packages that are part of an SDK (e.g. Flutter) and don't 148 // have an explicit version string set. This will take the given constraint 149 // parameter, ensure it's a valid constraint string, and return the lowest version 150 // within that constraint range. 151 // 152 // Examples: 153 // 154 // parseMinimumSdkVersion("^1.2.3") -> "1.2.3" 155 // parseMinimumSdkVersion(">=1.2.3") -> "1.2.3" 156 // parseMinimumSdkVersion(">=1.2.3 <2.0.0") -> "1.2.3" 157 // parseMinimumSdkVersion("1.2.3") -> error 158 // 159 // see https://dart.dev/tools/pub/dependencies#version-constraints for the 160 // constraint format used in Dart SDK defintions. 161 func parseMinimumSdkVersion(constraint string) (string, error) { 162 if !semverRegex.MatchString(constraint) { 163 return "", fmt.Errorf("unsupported or invalid constraint '%s'", constraint) 164 } 165 166 // Read "version" subexpression into version variable 167 var version []byte 168 matchIndex := semverRegex.FindStringSubmatchIndex(constraint) 169 version = semverRegex.ExpandString(version, "$version", constraint, matchIndex) 170 171 return string(version), nil 172 } 173 174 func (p *pubspecLockPackage) getVcsURL() string { 175 if p.Source == "git" { 176 if p.Description.Path == "." { 177 return fmt.Sprintf("%s@%s", p.Description.URL, p.Description.ResolvedRef) 178 } 179 180 return fmt.Sprintf("%s@%s#%s", p.Description.URL, p.Description.ResolvedRef, p.Description.Path) 181 } 182 183 return "" 184 } 185 186 func (p *pubspecLockPackage) getHostedURL() string { 187 if p.Source == "hosted" && p.Description.URL != defaultPubRegistry { 188 u, err := url.Parse(p.Description.URL) 189 if err != nil { 190 log.Debugf("Unable to parse registry url %w", err) 191 return p.Description.URL 192 } 193 return u.Host 194 } 195 196 return "" 197 }