Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for version constraint wildcards #3

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,6 @@ Supported operators:
- e.g. `~1.2.3` := `>=1.2.3, <1.3.0`
- `~>` : you accept any version equal to or greater than in the last digit
- e.g. `~>3.0.3` := `>= 3.0.3, < 3.1`

**NOTE** : `version` package doesn't support wildcards such as `x`, `X`, and `*`.

#### Pre-release
Unlike the `semver` package, `version` package always includes pre-release versions even with no pre-releases constraint.
Expand Down
11 changes: 5 additions & 6 deletions pkg/part/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ func (parts Parts) Padding(size int, padding Part) Parts {
}

func (parts Parts) Compare(other Part) int {
if other == nil {
return 1
} else if other.IsAny() {
return 0
}

var o Parts
switch t := other.(type) {
case InfinityType:
Expand All @@ -64,6 +58,11 @@ func (parts Parts) Compare(other Part) int {
case Parts:
o = t
default:
if other == nil {
return 1
} else if other.IsAny() {
return 0
}
return -1
}

Expand Down
50 changes: 45 additions & 5 deletions pkg/version/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import (
"regexp"
"strings"

"github.com/aquasecurity/go-version/pkg/part"
"golang.org/x/xerrors"
)

const cvRegex = `v?([0-9|x|X|\*]+(\.[0-9|x|X|\*]+)*)` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`

var (
constraintOperators = map[string]operatorFunc{
"": constraintEqual,
Expand All @@ -25,6 +30,7 @@ var (
"^": constraintCaret,
}
constraintRegexp *regexp.Regexp
constraintRangeRegexp *regexp.Regexp
validConstraintRegexp *regexp.Regexp
)

Expand All @@ -39,12 +45,16 @@ func init() {
constraintRegexp = regexp.MustCompile(fmt.Sprintf(
`(%s)\s*(%s)`,
strings.Join(ops, "|"),
regex))
cvRegex))

constraintRangeRegexp = regexp.MustCompile(fmt.Sprintf(
`(%s)\s+-\s+(%s)`,
cvRegex, 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.
Expand All @@ -61,6 +71,8 @@ type Constraint struct {

// NewConstraints parses a given constraint and returns a new instance of Constraints
func NewConstraints(v string) (Constraints, error) {
v = rewriteRange(v)

var css [][]Constraint
for _, vv := range strings.Split(v, "||") {
// Validate the segment
Expand Down Expand Up @@ -90,15 +102,43 @@ func NewConstraints(v string) (Constraints, error) {

}

func rewriteRange(i string) string {
m := constraintRangeRegexp.FindAllStringSubmatch(i, -1)
if m == nil {
return i
}
o := i
for _, v := range m {
t := fmt.Sprintf(">= %s, <= %s.*", v[1], v[11])
o = strings.Replace(o, v[0], t, 1)
}
return o
}

func newConstraint(c string) (Constraint, error) {
if c == "" {
return Constraint{
version: Version{
segments: part.NewParts("*"),
},
operatorFunc: constraintOperators[""],
}, nil
}

m := constraintRegexp.FindStringSubmatch(c)
if m == nil {
return Constraint{}, xerrors.Errorf("improper constraint: %s", c)
}

v, err := Parse(m[2])
if err != nil {
return Constraint{}, xerrors.Errorf("version parse error (%s): %w", m[2], err)
var segments []part.Part
for _, str := range strings.Split(m[3], ".") {
segments = append(segments, part.NewPart(str))
}

v := Version{
segments: segments,
preRelease: part.NewParts(m[6]),
original: c,
Comment on lines +133 to +141
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why Parse doesn't fit here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use Parse from version.go here that means a valid version would be 1.2.x. So instead it's necessary to parse the segments here to create a valid "constraint" version.

}

return Constraint{
Expand Down
44 changes: 44 additions & 0 deletions pkg/version/constraint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,17 @@ func TestVersion_Check(t *testing.T) {
{"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},
Expand All @@ -86,6 +91,10 @@ func TestVersion_Check(t *testing.T) {
{"<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},
Expand All @@ -98,6 +107,9 @@ func TestVersion_Check(t *testing.T) {
{"<=1.1", "0.1.0", true},
{"<=1.1", "1.1.0", true},
{"<=1.1", "1.1.1", false}, // different
{"<=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},
Expand All @@ -120,6 +132,8 @@ func TestVersion_Check(t *testing.T) {
{">11.1", "11.1.0", false},
{">11.1", "11.1.1", true}, // different
{">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},
Expand All @@ -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},
Expand All @@ -170,6 +186,10 @@ func TestVersion_Check(t *testing.T) {
{"~> 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},
Expand All @@ -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},
Expand Down Expand Up @@ -214,6 +236,28 @@ 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", false},
{"*", "1", true},
{"*", "4.5.6", true},
{"*", "1.2.3-alpha", false},
{"*-alpha", "1.2.3-alpha", 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},

// Ranges
{"1.1 - 2", "1.1.1", true},
{"1.1 - 3", "3.4.5", true},
{"1.1 - 3", "4.3.2", false},
{"1.5.0 - 4", "3.7.0", true},

// More than 3 numbers
{"< 1.0.0.1 || = 2.0.1.2.3", "2.0", false},
Expand Down
38 changes: 24 additions & 14 deletions pkg/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const (

// Version represents a single version.
type Version struct {
segments []part.Uint64
segments []part.Part
preRelease part.Parts
buildMetadata string
original string
Expand All @@ -44,7 +44,7 @@ func Parse(v string) (Version, error) {
return Version{}, xerrors.Errorf("malformed version: %s", v)
}

var segments []part.Uint64
var segments []part.Part
for _, str := range strings.Split(matches[1], ".") {
val, err := part.NewUint64(str)
if err != nil {
Expand Down Expand Up @@ -76,8 +76,8 @@ func (v Version) Compare(other Version) int {
return 0
}

p1 := part.Uint64SliceToParts(v.segments).Normalize()
p2 := part.Uint64SliceToParts(other.segments).Normalize()
p1 := part.Parts(v.segments).Normalize()
p2 := part.Parts(other.segments).Normalize()

p1 = p1.Padding(len(p2), part.Zero)
p2 = p2.Padding(len(p1), part.Zero)
Expand Down Expand Up @@ -139,6 +139,11 @@ func (v Version) Original() string {
return v.original
}

// Prerelease returns the pre-release version.
func (v Version) Prerelease() string {
return v.preRelease.String()
}

// 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
Expand All @@ -147,12 +152,12 @@ func (v Version) PessimisticBump() Version {

size := len(v.segments)
if size == 1 {
v.segments[0] += 1
v.segments[0] = v.segments[0].(part.Uint64) + 1
return v
}

v.segments[size-1] = 0
v.segments[size-2] += 1
v.segments[size-1] = part.Uint64(0)
v.segments[size-2] = v.segments[size-2].(part.Uint64) + 1

v.preRelease = part.Parts{}
v.buildMetadata = ""
Expand All @@ -166,7 +171,7 @@ func (v Version) TildeBump() Version {
v = v.copy()

if len(v.segments) == 2 {
v.segments[1] += 1
v.segments[1] = v.segments[1].(part.Uint64) + 1
return v
}

Expand All @@ -180,8 +185,8 @@ func (v Version) CaretBump() Version {

found := -1
for i, s := range v.segments {
if s != 0 {
v.segments[i] += 1
if s.(part.Uint64) != 0 {
v.segments[i] = v.segments[i].(part.Uint64) + 1
found = i
break
}
Expand All @@ -191,11 +196,11 @@ func (v Version) CaretBump() Version {
// zero padding
// ^1.2.3 => 2.0.0
for i := found + 1; i < len(v.segments); i++ {
v.segments[i] = 0
v.segments[i] = part.Uint64(0)
}
} else {
// ^0.0 => 0.1
v.segments[len(v.segments)-1] += 1
v.segments[len(v.segments)-1] = v.segments[len(v.segments)-1].(part.Uint64) + 1
}

v.preRelease = part.Parts{}
Expand All @@ -205,8 +210,13 @@ func (v Version) CaretBump() Version {
}

func (v Version) copy() Version {
segments := make([]part.Uint64, len(v.segments))
copy(segments, v.segments)
segments := make([]part.Part, 0, len(v.segments))
for _, segment := range v.segments {
if segment.IsAny() {
break
}
segments = append(segments, segment)
}

return Version{
segments: segments,
Expand Down