From eac47a9c209d6165464f1f1f71f25e0c3471118d Mon Sep 17 00:00:00 2001 From: Beorn Facchini Date: Sun, 12 Feb 2023 15:53:05 +1100 Subject: [PATCH] Support for version constraint wildcards --- pkg/version/constraint.go | 90 +++++++++++++++++++++++++++++----- pkg/version/constraint_test.go | 72 ++++++++++++++++++++------- pkg/version/version.go | 65 +++++++++++++++--------- 3 files changed, 175 insertions(+), 52 deletions(-) diff --git a/pkg/version/constraint.go b/pkg/version/constraint.go index 6bb0175..cf11349 100644 --- a/pkg/version/constraint.go +++ b/pkg/version/constraint.go @@ -5,6 +5,8 @@ import ( "regexp" "strings" + "github.com/aquasecurity/go-version/pkg/part" + "github.com/aquasecurity/go-version/pkg/prerelease" "golang.org/x/xerrors" ) @@ -30,6 +32,11 @@ var ( type operatorFunc func(v, c Version) bool +const cvRegex = `v?([0-9|x|X|\*]+(\.[0-9|x|X|\*]+)*)` + + `(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` + + `(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` + + `?` + func init() { ops := make([]string, 0, len(constraintOperators)) for k := range constraintOperators { @@ -39,12 +46,12 @@ func init() { constraintRegexp = regexp.MustCompile(fmt.Sprintf( `(%s)\s*(%s)`, strings.Join(ops, "|"), - regex)) + cvRegex)) validConstraintRegexp = regexp.MustCompile(fmt.Sprintf( `^\s*(\s*(%s)\s*(%s)\s*\,?)*\s*$`, strings.Join(ops, "|"), - regex)) + cvRegex)) } // Constraints is one or more constraint that a version can be checked against. @@ -90,23 +97,64 @@ func NewConstraints(v string) (Constraints, error) { } func newConstraint(c string) (constraint, error) { + o := prereleaseCheck + if c == "" { + return constraint{ + version: Version{}, + operator: o, + original: c, + }, nil + } + m := constraintRegexp.FindStringSubmatch(c) if m == nil { return constraint{}, xerrors.Errorf("improper constraint: %s", c) } - v, err := Parse(m[2]) + v, err := newConstraintVersion(m[2:]) if err != nil { return constraint{}, xerrors.Errorf("version parse error (%s): %w", m[2], err) } + if len(v.segments) > 0 { + o = constraintOperators[m[1]] + } + return constraint{ version: v, - operator: constraintOperators[m[1]], + operator: o, original: c, }, nil } +func newConstraintVersion(matches []string) (Version, error) { + var segments []part.Uint64 + for _, str := range strings.Split(matches[1], ".") { + if _, err := part.NewAny(str); err == nil { + break + } + + val, err := part.NewUint64(str) + if err != nil { + return Version{}, xerrors.Errorf("error parsing version: %w", err) + } + + segments = append(segments, val) + } + + pre := matches[7] + if pre == "" { + pre = matches[4] + } + + return Version{ + segments: segments, + buildMetadata: matches[10], + preRelease: part.NewParts(pre), + original: matches[0], + }, nil +} + func (c constraint) check(v Version) bool { return c.operator(v, c.version) } @@ -149,36 +197,52 @@ func andCheck(v Version, constraints []constraint) bool { return true } +func prereleaseCheck(v, c Version) bool { + if !v.preRelease.IsNull() && c.preRelease.IsNull() { + return false + } + return true +} + //------------------------------------------------------------------- // Constraint functions //------------------------------------------------------------------- func constraintEqual(v, c Version) bool { - return v.Equal(c) + if prerelease.Compare(v.preRelease, c.preRelease) != 0 { + return false + } + return v.GreaterThanOrEqual(c) && v.LessThan(c.NextBump()) } func constraintNotEqual(v, c Version) bool { - return !v.Equal(c) + return !constraintEqual(v, c) } func constraintGreaterThan(v, c Version) bool { - return v.GreaterThan(c) + if !prereleaseCheck(v, c) { + return false + } + if !v.preRelease.IsNull() { + return v.GreaterThan(c) + } + return v.GreaterThanOrEqual(c.NextBump()) } func constraintLessThan(v, c Version) bool { - return v.LessThan(c) + return prereleaseCheck(v, c) && v.LessThan(c) } func constraintGreaterThanEqual(v, c Version) bool { - return v.GreaterThanOrEqual(c) + return prereleaseCheck(v, c) && v.GreaterThanOrEqual(c) } func constraintLessThanEqual(v, c Version) bool { - return v.LessThanOrEqual(c) + return prereleaseCheck(v, c) && v.LessThan(c.NextBump()) } func constraintPessimistic(v, c Version) bool { - return v.GreaterThanOrEqual(c) && v.LessThan(c.PessimisticBump()) + return prereleaseCheck(v, c) && v.GreaterThanOrEqual(c) && v.LessThan(c.PessimisticBump()) } func constraintTilde(v, c Version) bool { @@ -188,7 +252,7 @@ func constraintTilde(v, c Version) bool { // ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0 // ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0 // ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0 - return v.GreaterThanOrEqual(c) && v.LessThan(c.TildeBump()) + return prereleaseCheck(v, c) && v.GreaterThanOrEqual(c) && v.LessThan(c.TildeBump()) } func constraintCaret(v, c Version) bool { @@ -201,5 +265,5 @@ func constraintCaret(v, c Version) bool { // ^0.0.3 --> >=0.0.3 <0.0.4 // ^0.0 --> >=0.0.0 <0.1.0 // ^0 --> >=0.0.0 <1.0.0 - return v.GreaterThanOrEqual(c) && v.LessThan(c.CaretBump()) + return prereleaseCheck(v, c) && v.GreaterThanOrEqual(c) && v.LessThan(c.CaretBump()) } diff --git a/pkg/version/constraint_test.go b/pkg/version/constraint_test.go index 3108a7b..0ae08a4 100644 --- a/pkg/version/constraint_test.go +++ b/pkg/version/constraint_test.go @@ -52,7 +52,7 @@ func TestVersion_Check(t *testing.T) { {"=4.1-alpha", "4.1.0-alpha", true}, {"=2.0", "1.2.3", false}, {"=2.0", "2.0.0", true}, - {"=2.0", "2.0.1", false}, + {"=2.0", "2.0.1", true}, {"=0", "1.0.0", false}, {"== 2.0.0", "1.2.3", false}, @@ -62,30 +62,39 @@ func TestVersion_Check(t *testing.T) { {"2", "1.0.0", false}, {"2", "3.4.5", false}, - {"2", "2.1.1", false}, - {"2.1", "2.1.1", false}, + {"2", "2.1.1", true}, + {"2.1", "2.1.1", true}, {"2.1", "2.2.1", false}, {"4.1", "4.1.0", true}, {"1.0", "1.0.0", true}, + {"1.x", "1.2.3", true}, + {"4.1.x", "4.1.3", true}, // Not equal {"!=4.1.0", "4.1.0", false}, {"!=4.1.0", "4.1.1", true}, {"!=4.1", "5.1.0-alpha.1", true}, {"!=4.1-alpha", "4.1.0", true}, + {"!=4.x", "5.1.0", true}, + {"!=4.1.x", "4.2.0", true}, + {"!=4.2.x", "4.2.3", false}, // Less than {"<0.0.5", "0.1.0", false}, {"<1.0.0", "0.1.0", true}, - {"<0", "0.0.0-alpha", true}, + {"<0", "0.0.0-alpha", false}, {"<0-z", "0.0.0-alpha", true}, {"<0", "1.0.0-alpha", false}, - {"<1", "1.0.0-alpha", true}, + {"<1", "1.0.0-alpha", false}, {"<11", "0.1.0", true}, {"<11", "11.1.0", false}, {"<1.1", "0.1.0", true}, {"<1.1", "1.1.0", false}, {"<1.1", "1.1.1", false}, + {"<1.x", "1.1.1", false}, + {"<2.x", "1.1.1", true}, + {"<1.1.x", "1.2.1", false}, + {"<1.2.x", "1.1.1", true}, // Less than or equal {"<=0.2.3", "1.2.3", false}, @@ -93,18 +102,21 @@ func TestVersion_Check(t *testing.T) { {"<= 2.1.0-a", "2.0.0", true}, {"<=11", "1.2.3", true}, {"<=11", "12.2.3", false}, - {"<=11", "11.2.3", false}, // different + {"<=11", "11.2.3", true}, {"<=1.1", "1.2.3", false}, {"<=1.1", "0.1.0", true}, {"<=1.1", "1.1.0", true}, - {"<=1.1", "1.1.1", false}, // different + {"<=1.1", "1.1.1", true}, + {"<=1.x", "1.1.1", true}, + {"<=2.x", "3.0.0", false}, + {"<=1.1.x", "1.2.1", false}, // Greater than {">5.0.0", "4.1.0", false}, {">4.0.0", "4.1.0", true}, - {"> 2.0", "2.1.0-beta", true}, - {">0", "0.0.1-alpha", true}, - {">0.0", "0.0.1-alpha", true}, + {"> 2.0", "2.1.0-beta", false}, + {">0", "0.0.1-alpha", false}, + {">0.0", "0.0.1-alpha", false}, {">0-0", "0.0.1-alpha", true}, {">0.0-0", "0.0.1-alpha", true}, {">0", "0.0.0-alpha", false}, @@ -116,10 +128,12 @@ func TestVersion_Check(t *testing.T) { {">1.1", "1.1.0", false}, {">0", "0.0.0", false}, {">0", "1.0.0", true}, - {">11", "11.1.0", true}, // different + {">11", "11.1.0", false}, {">11.1", "11.1.0", false}, - {">11.1", "11.1.1", true}, // different + {">11.1", "11.1.1", false}, {">11.1", "11.2.1", true}, + {">11.x", "11.2.1", false}, + {">11.1.x", "11.2.1", true}, // Greater than or equal {">=11.1.3", "11.1.2", false}, @@ -127,11 +141,11 @@ func TestVersion_Check(t *testing.T) { {">= 1.0, < 1.2", "1.1.5", true}, {">= 2.1.0-a", "2.1.0-beta", true}, {">= 2.1.0-a", "2.1.1-beta", true}, - {">= 2.0.0", "2.1.0-beta", true}, + {">= 2.0.0", "2.1.0-beta", false}, {">= 2.1.0-a", "2.1.1", true}, {">= 2.1.0-a", "2.1.0", true}, - {">=0", "0.0.1-alpha", true}, - {">=0.0", "0.0.1-alpha", true}, + {">=0", "0.0.1-alpha", false}, + {">=0.0", "0.0.1-alpha", false}, {">=0-0", "0.0.1-alpha", true}, {">=0.0-0", "0.0.1-alpha", true}, {">=0", "0.0.0-alpha", false}, @@ -146,6 +160,8 @@ func TestVersion_Check(t *testing.T) { {">=1.1", "1.1.0", true}, {">=1.1", "0.0.9", false}, {">=0", "0.0.0", true}, + {">=11.x", "11.1.2", true}, + {">=11.1.x", "11.1.2", true}, // Pessimistic {"~> 1.0", "2.0", false}, @@ -165,11 +181,15 @@ func TestVersion_Check(t *testing.T) { {"~> 1.0.9.5", "1.0.9.6", true}, {"~> 1.0.9.5", "1.0.9.5.0", true}, {"~> 1.0.9.5", "1.0.9.5.1", true}, - {"~> 2.0", "2.1.0-beta", true}, + {"~> 2.0", "2.1.0-beta", false}, {"~> 2.1.0-a", "2.2.0", false}, {"~> 2.1.0-a", "2.1.0", true}, {"~> 2.1.0-a", "2.1.0-beta", true}, {"~> 2.1.0-a", "2.2.0-alpha", true}, + {"~> 1.x", "2.0", false}, + {"~> 1.x", "1.1", true}, + {"~> 1.0.x", "1.2.3", true}, + {"~> 1.0.x", "1.0.7", true}, // Tilde {"~1.2.3", "1.2.4", true}, @@ -184,6 +204,8 @@ func TestVersion_Check(t *testing.T) { {"~1.2.3-beta.2", "1.2.3-beta.4", true}, {"~1.2.3-beta.2", "1.2.4-beta.2", true}, {"~1.2.3-beta.2", "1.3.4-beta.2", false}, + {"~1.x", "2.1.1", false}, + {"~1.x", "1.3.5", true}, // Caret {"^1.2.3", "1.8.9", true}, @@ -206,7 +228,7 @@ func TestVersion_Check(t *testing.T) { {"^0.0", "1.0.4", false}, {"^0", "0.2.3", true}, {"^0", "1.1.4", false}, - {"^1.2.0", "1.2.1-alpha.1", true}, + {"^1.2.0", "1.2.1-alpha.1", false}, {"^1.2.0-alpha.0", "1.2.1-alpha.1", true}, {"^1.2.0-alpha.0", "1.2.1-alpha.0", true}, {"^1.2.0-alpha.2", "1.2.0-alpha.1", false}, @@ -214,6 +236,22 @@ func TestVersion_Check(t *testing.T) { {"^0.2.3-beta.2", "0.2.4-beta.2", true}, {"^0.2.3-beta.2", "0.3.4-beta.2", false}, {"^0.2.3-beta.2", "0.2.3-beta.2", true}, + {"^1.x", "1.1.1", true}, + {"^2.x", "1.1.1", false}, + + // Wildcards + {"", "1", true}, + {"", "4.5.6", true}, + {"", "1.2.3-alpha.1", false}, + {"*", "1", true}, + {"*", "4.5.6", true}, + {"*", "1.2.3-alpha.1", false}, + {"*-0", "1.2.3-alpha.1", true}, + {"2.*", "1", false}, + {"2.*", "3.4.5", false}, + {"2.*", "2.1.1", true}, + {"2.1.*", "2.1.1", true}, + {"2.1.*", "2.2.1", false}, // More than 3 numbers {"< 1.0.0.1 || = 2.0.1.2.3", "2.0", false}, diff --git a/pkg/version/version.go b/pkg/version/version.go index 62c7836..294263e 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -139,43 +139,67 @@ func (v Version) Original() string { return v.original } +// Prerelease returns the pre-release version. +func (v Version) Prerelease() string { + return v.preRelease.String() +} + +// NextBump returns the next incremental version. +func (v Version) NextBump() Version { + segments := make([]part.Uint64, len(v.segments)) + copy(segments, v.segments) + bump := Version{segments: segments} + + bump.segments[len(bump.segments)-1] += 1 + return bump +} + // PessimisticBump returns the maximum version of "~>" // It works like Gem::Version.bump() // https://docs.ruby-lang.org/en/2.6.0/Gem/Version.html#method-i-bump func (v Version) PessimisticBump() Version { - size := len(v.segments) + segments := make([]part.Uint64, len(v.segments)) + copy(segments, v.segments) + bump := Version{segments: segments} + + size := len(bump.segments) if size == 1 { - v.segments[0] += 1 - return v + bump.segments[0] += 1 + return bump } - v.segments[size-1] = 0 - v.segments[size-2] += 1 - - v.preRelease = part.Parts{} - v.buildMetadata = "" + bump.segments[size-1] = 0 + bump.segments[size-2] += 1 - return v + return bump } // TildeBump returns the maximum version of "~" // https://docs.npmjs.com/cli/v6/using-npm/semver#tilde-ranges-123-12-1 func (v Version) TildeBump() Version { - if len(v.segments) == 2 { - v.segments[1] += 1 - return v + segments := make([]part.Uint64, len(v.segments)) + copy(segments, v.segments) + bump := Version{segments: segments} + + if len(bump.segments) == 2 { + bump.segments[1] += 1 + return bump } - return v.PessimisticBump() + return bump.PessimisticBump() } // CaretBump returns the maximum version of "^" // https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004 func (v Version) CaretBump() Version { + segments := make([]part.Uint64, len(v.segments)) + copy(segments, v.segments) + bump := Version{segments: segments} + found := -1 - for i, s := range v.segments { + for i, s := range bump.segments { if s != 0 { - v.segments[i] += 1 + bump.segments[i] += 1 found = i break } @@ -184,16 +208,13 @@ func (v Version) CaretBump() Version { if found >= 0 { // zero padding // ^1.2.3 => 2.0.0 - for i := found + 1; i < len(v.segments); i++ { - v.segments[i] = 0 + for i := found + 1; i < len(bump.segments); i++ { + bump.segments[i] = 0 } } else { // ^0.0 => 0.1 - v.segments[len(v.segments)-1] += 1 + bump.segments[len(bump.segments)-1] += 1 } - v.preRelease = part.Parts{} - v.buildMetadata = "" - - return v + return bump }