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  }