fix: fetch Dart package versions from sdk entries (#3572)

* fix: fetch Dart package versions from sdk entries

Packages that are provided by an SDK, mainly Flutter, will have their
version set to 0.0.0 in Dart's pubspec.lock file. Their actual version
is linked to that SDK, which is defined either as a version range or a
minimum supported version, rather than an explicit, single version.

The pubspec.lock file has a dedicated section to define those SDK
version range constraints, which is already stored internally when
parsing the file itself. The solution now is to look up such a package's
SDK name, retrieve the defined version range / lower version boundary,
and set the minimum supported version as the package's new version.

Signed-off-by: Sven Gregori <sven@craplab.fi>

* Ignore Dart package if SDK version cannot be fetched

Signed-off-by: Sven Gregori <sven@craplab.fi>

* fix linting issues

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Sven Gregori <sven@craplab.fi>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Sven Gregori 2025-03-13 12:10:32 +02:00 committed by GitHub
parent 616c8dfe2a
commit 2846bb18d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 278 additions and 4 deletions

View File

@ -4,8 +4,10 @@ import (
"context"
"fmt"
"net/url"
"regexp"
"sort"
"github.com/Masterminds/semver"
"gopkg.in/yaml.v3"
"github.com/anchore/syft/internal/log"
@ -68,7 +70,26 @@ func parsePubspecLock(_ context.Context, _ file.Resolver, _ *generic.Environment
}
var names []string
for name := range p.Packages {
for name, pkg := range p.Packages {
if pkg.Source == "sdk" && pkg.Version == "0.0.0" {
// Packages that are delivered as part of an SDK (e.g. Flutter) have their
// version set to "0.0.0" in the package definition. The actual version
// should refer to the SDK version, which is defined in a dedicated section
// in the pubspec.lock file and uses a version range constraint.
//
// If such a package is detected, look up the version range constraint of
// its matching SDK, and set the minimum supported version as its new version.
sdkName := pkg.Description.Name
sdkVersion, err := p.getSdkVersion(sdkName)
if err != nil {
log.Tracef("failed to resolve %s SDK version for package %s: %v", sdkName, name, err)
continue
}
pkg.Version = sdkVersion
p.Packages[name] = pkg
}
names = append(names, name)
}
@ -89,6 +110,68 @@ func parsePubspecLock(_ context.Context, _ file.Resolver, _ *generic.Environment
return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages")
}
// Look up the version range constraint for a given sdk name, if found,
// and return its lowest supported version matching that constraint.
//
// The sdks and their constraints are defined in the pubspec.lock file, e.g.
//
// sdks:
// dart: ">=2.12.0 <3.0.0"
// flutter: ">=3.24.5"
//
// and stored in the pubspecLock.Sdks map during parsing.
//
// Example based on the data above:
//
// getSdkVersion("dart") -> "2.12.0"
// getSdkVersion("flutter") -> "3.24.5"
// getSdkVersion("undefined") -> error
func (psl *pubspecLock) getSdkVersion(sdk string) (string, error) {
constraint, found := psl.Sdks[sdk]
if !found {
return "", fmt.Errorf("cannot find %s SDK", sdk)
}
return parseMinimumSdkVersion(constraint)
}
// Parse a given version range constraint and return its lowest supported version.
//
// This is intended for packages that are part of an SDK (e.g. Flutter) and don't
// have an explicit version string set. This will take the given constraint
// parameter, ensure it's a valid constraint string, and return the lowest version
// within that constraint range.
//
// Examples:
//
// parseMinimumSdkVersion("^1.2.3") -> "1.2.3"
// parseMinimumSdkVersion(">=1.2.3") -> "1.2.3"
// parseMinimumSdkVersion(">=1.2.3 <2.0.0") -> "1.2.3"
// parseMinimumSdkVersion("1.2.3") -> error
//
// see https://dart.dev/tools/pub/dependencies#version-constraints for the
// constraint format used in Dart SDK defintions.
func parseMinimumSdkVersion(constraint string) (string, error) {
// Match strings that
// 1. start with either "^" or ">=" (Dart SDK constraints only use those two)
// 2. followed by a valid semantic version, matched as "version" named subexpression
// 3. followed by a space (if there's a range) or end of string (if there's only a lower boundary)
// |---1--||------------------2------------------||-3-|
re := regexp.MustCompile(`^(\^|>=)(?P<version>` + semver.SemVerRegex + `)( |$)`)
if !re.MatchString(constraint) {
return "", fmt.Errorf("unsupported or invalid constraint '%s'", constraint)
}
// Read "version" subexpression (see 2. above) into version variable
var version []byte
matchIndex := re.FindStringSubmatchIndex(constraint)
version = re.ExpandString(version, "$version", constraint, matchIndex)
return string(version), nil
}
func (p *pubspecLockPackage) getVcsURL() string {
if p.Source == "git" {
if p.Description.Path == "." {

View File

@ -3,6 +3,8 @@ package dart
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@ -76,14 +78,14 @@ func TestParsePubspecLock(t *testing.T) {
},
{
Name: "flutter",
Version: "0.0.0",
PURL: "pkg:pub/flutter@0.0.0",
Version: "3.24.5",
PURL: "pkg:pub/flutter@3.24.5",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "flutter",
Version: "0.0.0",
Version: "3.24.5",
},
},
{
@ -113,3 +115,163 @@ func Test_corruptPubspecLock(t *testing.T) {
WithError().
TestParser(t, parsePubspecLock)
}
func Test_missingSdkEntryPubspecLock(t *testing.T) {
fixture := "test-fixtures/missing-sdk/pubspec.lock"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
// SDK version is missing, so flutter version cannot be determined and
// is ignored, expecting args as only package in the list as a result.
expected := []pkg.Package{
{
Name: "args",
Version: "1.6.0",
PURL: "pkg:pub/args@1.6.0",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "args",
Version: "1.6.0",
},
},
}
// TODO: relationships are not under test
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
}
func Test_invalidSdkEntryPubspecLock(t *testing.T) {
fixture := "test-fixtures/invalid-sdk/pubspec.lock"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
// SDK version is invalid, so flutter version cannot be determined and
// is ignored, expecting args as only package in the list as a result.
expected := []pkg.Package{
{
Name: "args",
Version: "1.6.0",
PURL: "pkg:pub/args@1.6.0",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "args",
Version: "1.6.0",
},
},
}
// TODO: relationships are not under test
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
}
func Test_sdkVersionLookup(t *testing.T) {
psl := &pubspecLock{
Sdks: make(map[string]string, 5),
}
psl.Sdks["minVersionSdk"] = ">=0.1.2"
psl.Sdks["rangeVersionSdk"] = ">=1.2.3 <2.0.0"
psl.Sdks["caretVersionSdk"] = "^2.3.4"
psl.Sdks["emptyVersionSdk"] = ""
psl.Sdks["invalidVersionSdk"] = "not a constraint"
var version string
var err error
version, err = psl.getSdkVersion("minVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "0.1.2", version)
version, err = psl.getSdkVersion("rangeVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "1.2.3", version)
version, err = psl.getSdkVersion("caretVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "2.3.4", version)
version, err = psl.getSdkVersion("emptyVersionSdk")
assert.Error(t, err)
assert.Equal(t, "", version)
version, err = psl.getSdkVersion("invalidVersionSdk")
assert.Error(t, err)
assert.Equal(t, "", version)
version, err = psl.getSdkVersion("nonexistantSdk")
assert.Error(t, err)
assert.Equal(t, "", version)
}
func Test_sdkVersionParser_valid(t *testing.T) {
var version string
var err error
// map constraints to expected version
patterns := map[string]string{
"^0.0.0": "0.0.0",
">=0.0.0": "0.0.0",
"^1.23.4": "1.23.4",
">=1.23.4": "1.23.4",
"^11.22.33": "11.22.33",
">=11.22.33": "11.22.33",
"^123.123456.12345678": "123.123456.12345678",
">=123.123456.12345678": "123.123456.12345678",
">=1.2.3 <2.3.4": "1.2.3",
">=1.2.3 random string": "1.2.3",
">=1.2.3 >=0.1.2": "1.2.3",
"^1.2": "1.2",
">=1.2": "1.2",
"^1.2.3-rc4": "1.2.3-rc4",
">=1.2.3-rc4": "1.2.3-rc4",
"^2.34.5+hotfix6": "2.34.5+hotfix6",
">=2.34.5+hotfix6": "2.34.5+hotfix6",
}
for constraint, expected := range patterns {
version, err = parseMinimumSdkVersion(constraint)
assert.NoError(t, err)
assert.Equalf(t, expected, version, "constraint '%s", constraint)
}
}
func Test_sdkVersionParser_invalid(t *testing.T) {
var version string
var err error
patterns := []string{
"",
"abc",
"^abc",
">=abc",
"^a.b.c",
">=a.b.c",
"1.2.34",
">1.2.34",
"<=1.2.34",
"<1.2.34",
"^1.2.3.4",
">=1.2.3.4",
"^1.x.0",
">=1.x.0",
"^1x2x3",
">=1x2x3",
"^1.-2.3",
">=1.-2.3",
"abc <1.2.34",
"^2.3.45hotfix6",
">=2.3.45hotfix6",
}
for _, pattern := range patterns {
version, err = parseMinimumSdkVersion(pattern)
assert.Error(t, err)
assert.Equalf(t, "", version, "constraint '%s'", pattern)
}
}

View File

@ -0,0 +1,15 @@
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
sdks:
flutter: "3.24.5"

View File

@ -0,0 +1,13 @@
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"

View File

@ -52,3 +52,4 @@ packages:
version: "1.11.20"
sdks:
dart: ">=2.12.0 <3.0.0"
flutter: ">=3.24.5"