vendor: github.com/Masterminds/semver/v3 v3.4.0

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2025-07-15 15:36:39 +02:00
parent 92a4783070
commit 8c34ab52c0
8 changed files with 345 additions and 118 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/docker/buildx
go 1.23.0 go 1.23.0
require ( require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/semver/v3 v3.4.0
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51 // main github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51 // main
+2 -2
View File
@@ -3,8 +3,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
+55 -1
View File
@@ -1,5 +1,59 @@
# Changelog # Changelog
## 3.4.0 (2025-06-27)
### Added
- #268: Added property to Constraints to include prereleases for Check and Validate
### Changed
- #263: Updated Go testing for 1.24, 1.23, and 1.22
- #269: Updated the error message handling for message case and wrapping errors
- #266: Restore the ability to have leading 0's when parsing with NewVersion.
Opt-out of this by setting CoerceNewVersion to false.
### Fixed
- #257: Fixed the CodeQL link (thanks @dmitris)
- #262: Restored detailed errors when failed to parse with NewVersion. Opt-out
of this by setting DetailedNewVersionErrors to false for faster performance.
- #267: Handle pre-releases for an "and" group if one constraint includes them
## 3.3.1 (2024-11-19)
### Fixed
- #253: Fix for allowing some version that were invalid
## 3.3.0 (2024-08-27)
### Added
- #238: Add LessThanEqual and GreaterThanEqual functions (thanks @grosser)
- #213: nil version equality checking (thanks @KnutZuidema)
### Changed
- #241: Simplify StrictNewVersion parsing (thanks @grosser)
- Testing support up through Go 1.23
- Minimum version set to 1.21 as this is what's tested now
- Fuzz testing now supports caching
## 3.2.1 (2023-04-10)
### Changed
- #198: Improved testing around pre-release names
- #200: Improved code scanning with addition of CodeQL
- #201: Testing now includes Go 1.20. Go 1.17 has been dropped
- #202: Migrated Fuzz testing to Go built-in Fuzzing. CI runs daily
- #203: Docs updated for security details
### Fixed
- #199: Fixed issue with range transformations
## 3.2.0 (2022-11-28) ## 3.2.0 (2022-11-28)
### Added ### Added
@@ -109,7 +163,7 @@ functions. These are described in the added and changed sections below.
- #78: Fix unchecked error in example code (thanks @ravron) - #78: Fix unchecked error in example code (thanks @ravron)
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case - #70: Fix the handling of pre-releases and the 0.0.0 release edge case
- #97: Fixed copyright file for proper display on GitHub - #97: Fixed copyright file for proper display on GitHub
- #107: Fix handling prerelease when sorting alphanum and num - #107: Fix handling prerelease when sorting alphanum and num
- #109: Fixed where Validate sometimes returns wrong message on error - #109: Fixed where Validate sometimes returns wrong message on error
## 1.4.2 (2018-04-10) ## 1.4.2 (2018-04-10)
+2 -1
View File
@@ -19,6 +19,7 @@ test-cover:
.PHONY: fuzz .PHONY: fuzz
fuzz: fuzz:
@echo "==> Running Fuzz Tests" @echo "==> Running Fuzz Tests"
go env GOCACHE
go test -fuzz=FuzzNewVersion -fuzztime=15s . go test -fuzz=FuzzNewVersion -fuzztime=15s .
go test -fuzz=FuzzStrictNewVersion -fuzztime=15s . go test -fuzz=FuzzStrictNewVersion -fuzztime=15s .
go test -fuzz=FuzzNewConstraint -fuzztime=15s . go test -fuzz=FuzzNewConstraint -fuzztime=15s .
@@ -27,4 +28,4 @@ $(GOLANGCI_LINT):
# Install golangci-lint. The configuration for it is in the .golangci.yml # Install golangci-lint. The configuration for it is in the .golangci.yml
# file in the root of the repository # file in the root of the repository
echo ${GOPATH} echo ${GOPATH}
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.17.1 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.56.2
+31 -15
View File
@@ -13,12 +13,9 @@ Active](https://masterminds.github.io/stability/active.svg)](https://masterminds
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/semver/v3) [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/semver/v3)
[![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver) [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver)
If you are looking for a command line tool for version comparisons please see
[vert](https://github.com/Masterminds/vert) which uses this library.
## Package Versions ## Package Versions
Note, import `github.com/github.com/Masterminds/semver/v3` to use the latest version. Note, import `github.com/Masterminds/semver/v3` to use the latest version.
There are three major versions fo the `semver` package. There are three major versions fo the `semver` package.
@@ -53,6 +50,18 @@ other versions, convert the version back into a string, and get the original
string. Getting the original string is useful if the semantic version was coerced string. Getting the original string is useful if the semantic version was coerced
into a valid form. into a valid form.
There are package level variables that affect how `NewVersion` handles parsing.
- `CoerceNewVersion` is `true` by default. When set to `true` it coerces non-compliant
versions into SemVer. For example, allowing a leading 0 in a major, minor, or patch
part. This enables the use of CalVer in versions even when not compliant with SemVer.
When set to `false` less coercion work is done.
- `DetailedNewVersionErrors` provides more detailed errors. It only has an affect when
`CoerceNewVersion` is set to `false`. When `DetailedNewVersionErrors` is set to `true`
it can provide some more insight into why a version is invalid. Setting
`DetailedNewVersionErrors` to `false` is faster on performance but provides less
detailed error messages if a version fails to parse.
## Sorting Semantic Versions ## Sorting Semantic Versions
A set of versions can be sorted using the `sort` package from the standard library. A set of versions can be sorted using the `sort` package from the standard library.
@@ -80,12 +89,12 @@ There are two methods for comparing versions. One uses comparison methods on
differences to notes between these two methods of comparison. differences to notes between these two methods of comparison.
1. When two versions are compared using functions such as `Compare`, `LessThan`, 1. When two versions are compared using functions such as `Compare`, `LessThan`,
and others it will follow the specification and always include prereleases and others it will follow the specification and always include pre-releases
within the comparison. It will provide an answer that is valid with the within the comparison. It will provide an answer that is valid with the
comparison section of the spec at https://semver.org/#spec-item-11 comparison section of the spec at https://semver.org/#spec-item-11
2. When constraint checking is used for checks or validation it will follow a 2. When constraint checking is used for checks or validation it will follow a
different set of rules that are common for ranges with tools like npm/js different set of rules that are common for ranges with tools like npm/js
and Rust/Cargo. This includes considering prereleases to be invalid if the and Rust/Cargo. This includes considering pre-releases to be invalid if the
ranges does not include one. If you want to have it include pre-releases a ranges does not include one. If you want to have it include pre-releases a
simple solution is to include `-0` in your range. simple solution is to include `-0` in your range.
3. Constraint ranges can have some complex rules including the shorthand use of 3. Constraint ranges can have some complex rules including the shorthand use of
@@ -113,7 +122,7 @@ v, err := semver.NewVersion("1.3")
if err != nil { if err != nil {
// Handle version not being parsable. // Handle version not being parsable.
} }
// Check if the version meets the constraints. The a variable will be true. // Check if the version meets the constraints. The variable a will be true.
a := c.Check(v) a := c.Check(v)
``` ```
@@ -137,20 +146,20 @@ The basic comparisons are:
### Working With Prerelease Versions ### Working With Prerelease Versions
Pre-releases, for those not familiar with them, are used for software releases Pre-releases, for those not familiar with them, are used for software releases
prior to stable or generally available releases. Examples of prereleases include prior to stable or generally available releases. Examples of pre-releases include
development, alpha, beta, and release candidate releases. A prerelease may be development, alpha, beta, and release candidate releases. A pre-release may be
a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the
order of precedence, prereleases come before their associated releases. In this order of precedence, pre-releases come before their associated releases. In this
example `1.2.3-beta.1 < 1.2.3`. example `1.2.3-beta.1 < 1.2.3`.
According to the Semantic Version specification prereleases may not be According to the Semantic Version specification, pre-releases may not be
API compliant with their release counterpart. It says, API compliant with their release counterpart. It says,
> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. > A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.
SemVer comparisons using constraints without a prerelease comparator will skip SemVer's comparisons using constraints without a pre-release comparator will skip
prerelease versions. For example, `>=1.2.3` will skip prereleases when looking pre-release versions. For example, `>=1.2.3` will skip pre-releases when looking
at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. at a list of releases while `>=1.2.3-0` will evaluate and find pre-releases.
The reason for the `0` as a pre-release version in the example comparison is The reason for the `0` as a pre-release version in the example comparison is
because pre-releases can only contain ASCII alphanumerics and hyphens (along with because pre-releases can only contain ASCII alphanumerics and hyphens (along with
@@ -163,6 +172,10 @@ means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case
sensitivity doesn't apply here. This is due to ASCII sort ordering which is what sensitivity doesn't apply here. This is due to ASCII sort ordering which is what
the spec specifies. the spec specifies.
The `Constraints` instance returned from `semver.NewConstraint()` has a property
`IncludePrerelease` that, when set to true, will return prerelease versions when calls
to `Check()` and `Validate()` are made.
### Hyphen Range Comparisons ### Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges. There are multiple methods to handle ranges and the first is hyphens ranges.
@@ -171,6 +184,9 @@ These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` * `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` * `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5`
Note that `1.2-1.4.5` without whitespace is parsed completely differently; it's
parsed as a single constraint `1.2.0` with _prerelease_ `1.4.5`.
### Wildcards In Comparisons ### Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works The `x`, `X`, and `*` characters can be used as a wildcard character. This works
@@ -250,7 +266,7 @@ or [create a pull request](https://github.com/Masterminds/semver/pulls).
Security is an important consideration for this project. The project currently Security is an important consideration for this project. The project currently
uses the following tools to help discover security issues: uses the following tools to help discover security issues:
* [CodeQL](https://github.com/Masterminds/semver) * [CodeQL](https://codeql.github.com)
* [gosec](https://github.com/securego/gosec) * [gosec](https://github.com/securego/gosec)
* Daily Fuzz testing * Daily Fuzz testing
+67 -60
View File
@@ -12,6 +12,13 @@ import (
// checked against. // checked against.
type Constraints struct { type Constraints struct {
constraints [][]*constraint constraints [][]*constraint
containsPre []bool
// IncludePrerelease specifies if pre-releases should be included in
// the results. Note, if a constraint range has a prerelease than
// prereleases will be included for that AND group even if this is
// set to false.
IncludePrerelease bool
} }
// NewConstraint returns a Constraints instance that a Version instance can // NewConstraint returns a Constraints instance that a Version instance can
@@ -22,11 +29,10 @@ func NewConstraint(c string) (*Constraints, error) {
c = rewriteRange(c) c = rewriteRange(c)
ors := strings.Split(c, "||") ors := strings.Split(c, "||")
or := make([][]*constraint, len(ors)) lenors := len(ors)
or := make([][]*constraint, lenors)
hasPre := make([]bool, lenors)
for k, v := range ors { for k, v := range ors {
// TODO: Find a way to validate and fetch all the constraints in a simpler form
// Validate the segment // Validate the segment
if !validConstraintRegex.MatchString(v) { if !validConstraintRegex.MatchString(v) {
return nil, fmt.Errorf("improper constraint: %s", v) return nil, fmt.Errorf("improper constraint: %s", v)
@@ -43,12 +49,22 @@ func NewConstraint(c string) (*Constraints, error) {
return nil, err return nil, err
} }
// If one of the constraints has a prerelease record this.
// This information is used when checking all in an "and"
// group to ensure they all check for prereleases.
if pc.con.pre != "" {
hasPre[k] = true
}
result[i] = pc result[i] = pc
} }
or[k] = result or[k] = result
} }
o := &Constraints{constraints: or} o := &Constraints{
constraints: or,
containsPre: hasPre,
}
return o, nil return o, nil
} }
@@ -57,10 +73,10 @@ func (cs Constraints) Check(v *Version) bool {
// TODO(mattfarina): For v4 of this library consolidate the Check and Validate // TODO(mattfarina): For v4 of this library consolidate the Check and Validate
// functions as the underlying functions make that possible now. // functions as the underlying functions make that possible now.
// loop over the ORs and check the inner ANDs // loop over the ORs and check the inner ANDs
for _, o := range cs.constraints { for i, o := range cs.constraints {
joy := true joy := true
for _, c := range o { for _, c := range o {
if check, _ := c.check(v); !check { if check, _ := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); !check {
joy = false joy = false
break break
} }
@@ -83,12 +99,12 @@ func (cs Constraints) Validate(v *Version) (bool, []error) {
// Capture the prerelease message only once. When it happens the first time // Capture the prerelease message only once. When it happens the first time
// this var is marked // this var is marked
var prerelesase bool var prerelesase bool
for _, o := range cs.constraints { for i, o := range cs.constraints {
joy := true joy := true
for _, c := range o { for _, c := range o {
// Before running the check handle the case there the version is // Before running the check handle the case there the version is
// a prerelease and the check is not searching for prereleases. // a prerelease and the check is not searching for prereleases.
if c.con.pre == "" && v.pre != "" { if !(cs.IncludePrerelease || cs.containsPre[i]) && v.pre != "" {
if !prerelesase { if !prerelesase {
em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
e = append(e, em) e = append(e, em)
@@ -98,7 +114,7 @@ func (cs Constraints) Validate(v *Version) (bool, []error) {
} else { } else {
if _, err := c.check(v); err != nil { if _, err := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); err != nil {
e = append(e, err) e = append(e, err)
joy = false joy = false
} }
@@ -227,8 +243,8 @@ type constraint struct {
} }
// Check if a version meets the constraint // Check if a version meets the constraint
func (c *constraint) check(v *Version) (bool, error) { func (c *constraint) check(v *Version, includePre bool) (bool, error) {
return constraintOps[c.origfunc](v, c) return constraintOps[c.origfunc](v, c, includePre)
} }
// String prints an individual constraint into a string // String prints an individual constraint into a string
@@ -236,7 +252,7 @@ func (c *constraint) string() string {
return c.origfunc + c.orig return c.origfunc + c.orig
} }
type cfunc func(v *Version, c *constraint) (bool, error) type cfunc func(v *Version, c *constraint, includePre bool) (bool, error)
func parseConstraint(c string) (*constraint, error) { func parseConstraint(c string) (*constraint, error) {
if len(c) > 0 { if len(c) > 0 {
@@ -272,7 +288,7 @@ func parseConstraint(c string) (*constraint, error) {
// The constraintRegex should catch any regex parsing errors. So, // The constraintRegex should catch any regex parsing errors. So,
// we should never get here. // we should never get here.
return nil, errors.New("constraint Parser Error") return nil, errors.New("constraint parser error")
} }
cs.con = con cs.con = con
@@ -290,7 +306,7 @@ func parseConstraint(c string) (*constraint, error) {
// The constraintRegex should catch any regex parsing errors. So, // The constraintRegex should catch any regex parsing errors. So,
// we should never get here. // we should never get here.
return nil, errors.New("constraint Parser Error") return nil, errors.New("constraint parser error")
} }
cs := &constraint{ cs := &constraint{
@@ -305,16 +321,14 @@ func parseConstraint(c string) (*constraint, error) {
} }
// Constraint functions // Constraint functions
func constraintNotEqual(v *Version, c *constraint) (bool, error) { func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
}
if c.dirty { if c.dirty {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
}
if c.con.Major() != v.Major() { if c.con.Major() != v.Major() {
return true, nil return true, nil
} }
@@ -345,12 +359,11 @@ func constraintNotEqual(v *Version, c *constraint) (bool, error) {
return true, nil return true, nil
} }
func constraintGreaterThan(v *Version, c *constraint) (bool, error) { func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
@@ -391,11 +404,10 @@ func constraintGreaterThan(v *Version, c *constraint) (bool, error) {
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
} }
func constraintLessThan(v *Version, c *constraint) (bool, error) { func constraintLessThan(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
@@ -406,12 +418,11 @@ func constraintLessThan(v *Version, c *constraint) (bool, error) {
return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig) return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig)
} }
func constraintGreaterThanEqual(v *Version, c *constraint) (bool, error) { func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
@@ -422,11 +433,10 @@ func constraintGreaterThanEqual(v *Version, c *constraint) (bool, error) {
return false, fmt.Errorf("%s is less than %s", v, c.orig) return false, fmt.Errorf("%s is less than %s", v, c.orig)
} }
func constraintLessThanEqual(v *Version, c *constraint) (bool, error) { func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
@@ -455,11 +465,10 @@ func constraintLessThanEqual(v *Version, c *constraint) (bool, error) {
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0 // ~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.3, ~>1.2.3 --> >=1.2.3, <1.3.0
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0 // ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
func constraintTilde(v *Version, c *constraint) (bool, error) { func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
@@ -487,16 +496,15 @@ func constraintTilde(v *Version, c *constraint) (bool, error) {
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise // When there is a .x (dirty) status it automatically opts in to ~. Otherwise
// it's a straight = // it's a straight =
func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) { func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
if c.dirty { if c.dirty {
return constraintTilde(v, c) return constraintTilde(v, c, includePre)
} }
eq := v.Equal(c.con) eq := v.Equal(c.con)
@@ -516,11 +524,10 @@ func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) {
// ^0.0.3 --> >=0.0.3 <0.0.4 // ^0.0.3 --> >=0.0.3 <0.0.4
// ^0.0 --> >=0.0.0 <0.1.0 // ^0.0 --> >=0.0.0 <0.1.0
// ^0 --> >=0.0.0 <1.0.0 // ^0 --> >=0.0.0 <1.0.0
func constraintCaret(v *Version, c *constraint) (bool, error) { func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
// If there is a pre-release on the version but the constraint isn't looking // The existence of prereleases is checked at the group level and passed in.
// for them assume that pre-releases are not compatible. See issue 21 for // Exit early if the version has a prerelease but those are to be ignored.
// more details. if v.Prerelease() != "" && !includePre {
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
} }
+185 -36
View File
@@ -14,32 +14,52 @@ import (
// The compiled version of the regex created at init() is cached here so it // The compiled version of the regex created at init() is cached here so it
// only needs to be created once. // only needs to be created once.
var versionRegex *regexp.Regexp var versionRegex *regexp.Regexp
var looseVersionRegex *regexp.Regexp
// CoerceNewVersion sets if leading 0's are allowd in the version part. Leading 0's are
// not allowed in a valid semantic version. When set to true, NewVersion will coerce
// leading 0's into a valid version.
var CoerceNewVersion = true
// DetailedNewVersionErrors specifies if detailed errors are returned from the NewVersion
// function. This is used when CoerceNewVersion is set to false. If set to false
// ErrInvalidSemVer is returned for an invalid version. This does not apply to
// StrictNewVersion. Setting this function to false returns errors more quickly.
var DetailedNewVersionErrors = true
var ( var (
// ErrInvalidSemVer is returned a version is found to be invalid when // ErrInvalidSemVer is returned a version is found to be invalid when
// being parsed. // being parsed.
ErrInvalidSemVer = errors.New("Invalid Semantic Version") ErrInvalidSemVer = errors.New("invalid semantic version")
// ErrEmptyString is returned when an empty string is passed in for parsing. // ErrEmptyString is returned when an empty string is passed in for parsing.
ErrEmptyString = errors.New("Version string empty") ErrEmptyString = errors.New("version string empty")
// ErrInvalidCharacters is returned when invalid characters are found as // ErrInvalidCharacters is returned when invalid characters are found as
// part of a version // part of a version
ErrInvalidCharacters = errors.New("Invalid characters in version") ErrInvalidCharacters = errors.New("invalid characters in version")
// ErrSegmentStartsZero is returned when a version segment starts with 0. // ErrSegmentStartsZero is returned when a version segment starts with 0.
// This is invalid in SemVer. // This is invalid in SemVer.
ErrSegmentStartsZero = errors.New("Version segment starts with 0") ErrSegmentStartsZero = errors.New("version segment starts with 0")
// ErrInvalidMetadata is returned when the metadata is an invalid format // ErrInvalidMetadata is returned when the metadata is an invalid format
ErrInvalidMetadata = errors.New("Invalid Metadata string") ErrInvalidMetadata = errors.New("invalid metadata string")
// ErrInvalidPrerelease is returned when the pre-release is an invalid format // ErrInvalidPrerelease is returned when the pre-release is an invalid format
ErrInvalidPrerelease = errors.New("Invalid Prerelease string") ErrInvalidPrerelease = errors.New("invalid prerelease string")
) )
// semVerRegex is the regular expression used to parse a semantic version. // semVerRegex is the regular expression used to parse a semantic version.
const semVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` + // This is not the official regex from the semver spec. It has been modified to allow for loose handling
// where versions like 2.1 are detected.
const semVerRegex string = `v?(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?` +
`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`
// looseSemVerRegex is a regular expression that lets invalid semver expressions through
// with enough detail that certain errors can be checked for.
const looseSemVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
@@ -53,6 +73,7 @@ type Version struct {
func init() { func init() {
versionRegex = regexp.MustCompile("^" + semVerRegex + "$") versionRegex = regexp.MustCompile("^" + semVerRegex + "$")
looseVersionRegex = regexp.MustCompile("^" + looseSemVerRegex + "$")
} }
const ( const (
@@ -83,22 +104,23 @@ func StrictNewVersion(v string) (*Version, error) {
original: v, original: v,
} }
// check for prerelease or build metadata // Extract build metadata
var extra []string if strings.Contains(parts[2], "+") {
if strings.ContainsAny(parts[2], "-+") { extra := strings.SplitN(parts[2], "+", 2)
// Start with the build metadata first as it needs to be on the right sv.metadata = extra[1]
extra = strings.SplitN(parts[2], "+", 2) parts[2] = extra[0]
if len(extra) > 1 { if err := validateMetadata(sv.metadata); err != nil {
// build metadata found return nil, err
sv.metadata = extra[1]
parts[2] = extra[0]
} }
}
extra = strings.SplitN(parts[2], "-", 2) // Extract build prerelease
if len(extra) > 1 { if strings.Contains(parts[2], "-") {
// prerelease found extra := strings.SplitN(parts[2], "-", 2)
sv.pre = extra[1] sv.pre = extra[1]
parts[2] = extra[0] parts[2] = extra[0]
if err := validatePrerelease(sv.pre); err != nil {
return nil, err
} }
} }
@@ -114,7 +136,7 @@ func StrictNewVersion(v string) (*Version, error) {
} }
} }
// Extract the major, minor, and patch elements onto the returned Version // Extract major, minor, and patch
var err error var err error
sv.major, err = strconv.ParseUint(parts[0], 10, 64) sv.major, err = strconv.ParseUint(parts[0], 10, 64)
if err != nil { if err != nil {
@@ -131,10 +153,70 @@ func StrictNewVersion(v string) (*Version, error) {
return nil, err return nil, err
} }
// No prerelease or build metadata found so returning now as a fastpath. return sv, nil
if sv.pre == "" && sv.metadata == "" { }
return sv, nil
// NewVersion parses a given version and returns an instance of Version or
// an error if unable to parse the version. If the version is SemVer-ish it
// attempts to convert it to SemVer. If you want to validate it was a strict
// semantic version at parse time see StrictNewVersion().
func NewVersion(v string) (*Version, error) {
if CoerceNewVersion {
return coerceNewVersion(v)
} }
m := versionRegex.FindStringSubmatch(v)
if m == nil {
// Disabling detailed errors is first so that it is in the fast path.
if !DetailedNewVersionErrors {
return nil, ErrInvalidSemVer
}
// Check for specific errors with the semver string and return a more detailed
// error.
m = looseVersionRegex.FindStringSubmatch(v)
if m == nil {
return nil, ErrInvalidSemVer
}
err := validateVersion(m)
if err != nil {
return nil, err
}
return nil, ErrInvalidSemVer
}
sv := &Version{
metadata: m[5],
pre: m[4],
original: v,
}
var err error
sv.major, err = strconv.ParseUint(m[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("error parsing version segment: %w", err)
}
if m[2] != "" {
sv.minor, err = strconv.ParseUint(m[2], 10, 64)
if err != nil {
return nil, fmt.Errorf("error parsing version segment: %w", err)
}
} else {
sv.minor = 0
}
if m[3] != "" {
sv.patch, err = strconv.ParseUint(m[3], 10, 64)
if err != nil {
return nil, fmt.Errorf("error parsing version segment: %w", err)
}
} else {
sv.patch = 0
}
// Perform some basic due diligence on the extra parts to ensure they are
// valid.
if sv.pre != "" { if sv.pre != "" {
if err = validatePrerelease(sv.pre); err != nil { if err = validatePrerelease(sv.pre); err != nil {
@@ -151,12 +233,8 @@ func StrictNewVersion(v string) (*Version, error) {
return sv, nil return sv, nil
} }
// NewVersion parses a given version and returns an instance of Version or func coerceNewVersion(v string) (*Version, error) {
// an error if unable to parse the version. If the version is SemVer-ish it m := looseVersionRegex.FindStringSubmatch(v)
// attempts to convert it to SemVer. If you want to validate it was a strict
// semantic version at parse time see StrictNewVersion().
func NewVersion(v string) (*Version, error) {
m := versionRegex.FindStringSubmatch(v)
if m == nil { if m == nil {
return nil, ErrInvalidSemVer return nil, ErrInvalidSemVer
} }
@@ -170,13 +248,13 @@ func NewVersion(v string) (*Version, error) {
var err error var err error
sv.major, err = strconv.ParseUint(m[1], 10, 64) sv.major, err = strconv.ParseUint(m[1], 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err) return nil, fmt.Errorf("error parsing version segment: %w", err)
} }
if m[2] != "" { if m[2] != "" {
sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64) sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err) return nil, fmt.Errorf("error parsing version segment: %w", err)
} }
} else { } else {
sv.minor = 0 sv.minor = 0
@@ -185,7 +263,7 @@ func NewVersion(v string) (*Version, error) {
if m[3] != "" { if m[3] != "" {
sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64) sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err) return nil, fmt.Errorf("error parsing version segment: %w", err)
} }
} else { } else {
sv.patch = 0 sv.patch = 0
@@ -381,15 +459,31 @@ func (v *Version) LessThan(o *Version) bool {
return v.Compare(o) < 0 return v.Compare(o) < 0
} }
// LessThanEqual tests if one version is less or equal than another one.
func (v *Version) LessThanEqual(o *Version) bool {
return v.Compare(o) <= 0
}
// GreaterThan tests if one version is greater than another one. // GreaterThan tests if one version is greater than another one.
func (v *Version) GreaterThan(o *Version) bool { func (v *Version) GreaterThan(o *Version) bool {
return v.Compare(o) > 0 return v.Compare(o) > 0
} }
// GreaterThanEqual tests if one version is greater or equal than another one.
func (v *Version) GreaterThanEqual(o *Version) bool {
return v.Compare(o) >= 0
}
// Equal tests if two versions are equal to each other. // Equal tests if two versions are equal to each other.
// Note, versions can be equal with different metadata since metadata // Note, versions can be equal with different metadata since metadata
// is not considered part of the comparable version. // is not considered part of the comparable version.
func (v *Version) Equal(o *Version) bool { func (v *Version) Equal(o *Version) bool {
if v == o {
return true
}
if v == nil || o == nil {
return false
}
return v.Compare(o) == 0 return v.Compare(o) == 0
} }
@@ -612,7 +706,9 @@ func containsOnly(s string, comp string) bool {
func validatePrerelease(p string) error { func validatePrerelease(p string) error {
eparts := strings.Split(p, ".") eparts := strings.Split(p, ".")
for _, p := range eparts { for _, p := range eparts {
if containsOnly(p, num) { if p == "" {
return ErrInvalidPrerelease
} else if containsOnly(p, num) {
if len(p) > 1 && p[0] == '0' { if len(p) > 1 && p[0] == '0' {
return ErrSegmentStartsZero return ErrSegmentStartsZero
} }
@@ -631,9 +727,62 @@ func validatePrerelease(p string) error {
func validateMetadata(m string) error { func validateMetadata(m string) error {
eparts := strings.Split(m, ".") eparts := strings.Split(m, ".")
for _, p := range eparts { for _, p := range eparts {
if !containsOnly(p, allowed) { if p == "" {
return ErrInvalidMetadata
} else if !containsOnly(p, allowed) {
return ErrInvalidMetadata return ErrInvalidMetadata
} }
} }
return nil return nil
} }
// validateVersion checks for common validation issues but may not catch all errors
func validateVersion(m []string) error {
var err error
var v string
if m[1] != "" {
if len(m[1]) > 1 && m[1][0] == '0' {
return ErrSegmentStartsZero
}
_, err = strconv.ParseUint(m[1], 10, 64)
if err != nil {
return fmt.Errorf("error parsing version segment: %w", err)
}
}
if m[2] != "" {
v = strings.TrimPrefix(m[2], ".")
if len(v) > 1 && v[0] == '0' {
return ErrSegmentStartsZero
}
_, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return fmt.Errorf("error parsing version segment: %w", err)
}
}
if m[3] != "" {
v = strings.TrimPrefix(m[3], ".")
if len(v) > 1 && v[0] == '0' {
return ErrSegmentStartsZero
}
_, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return fmt.Errorf("error parsing version segment: %w", err)
}
}
if m[5] != "" {
if err = validatePrerelease(m[5]); err != nil {
return err
}
}
if m[8] != "" {
if err = validateMetadata(m[8]); err != nil {
return err
}
}
return nil
}
+2 -2
View File
@@ -2,8 +2,8 @@
## explicit; go 1.16 ## explicit; go 1.16
github.com/Azure/go-ansiterm github.com/Azure/go-ansiterm
github.com/Azure/go-ansiterm/winterm github.com/Azure/go-ansiterm/winterm
# github.com/Masterminds/semver/v3 v3.2.1 # github.com/Masterminds/semver/v3 v3.4.0
## explicit; go 1.18 ## explicit; go 1.21
github.com/Masterminds/semver/v3 github.com/Masterminds/semver/v3
# github.com/Microsoft/go-winio v0.6.2 # github.com/Microsoft/go-winio v0.6.2
## explicit; go 1.21 ## explicit; go 1.21