Skip to content
Merged
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
81 changes: 81 additions & 0 deletions version.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import (
// The compiled version of the regex created at init() is cached here so it
// only needs to be created once.
var versionRegex *regexp.Regexp
var looseVersionRegex *regexp.Regexp

// DetailedNewVersionErrors specifies if detailed errors are returned from the NewVersion
// function. 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 (
// ErrInvalidSemVer is returned a version is found to be invalid when
Expand Down Expand Up @@ -45,6 +52,12 @@ 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\-]+)*))?`

// Version represents a single semantic version.
type Version struct {
major, minor, patch uint64
Expand All @@ -55,6 +68,7 @@ type Version struct {

func init() {
versionRegex = regexp.MustCompile("^" + semVerRegex + "$")
looseVersionRegex = regexp.MustCompile("^" + looseSemVerRegex + "$")
}

const (
Expand Down Expand Up @@ -144,6 +158,22 @@ func StrictNewVersion(v string) (*Version, error) {
func NewVersion(v string) (*Version, error) {
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
}

Expand Down Expand Up @@ -643,3 +673,54 @@ func validateMetadata(m string) error {
}
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: %s", 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: %s", 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: %s", 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
}
134 changes: 134 additions & 0 deletions version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,140 @@ func TestNewVersion(t *testing.T) {
}
}

// TestNewVersionCheckError checks the returned error for compatibility
func TestNewVersionCheckError(t *testing.T) {
t.Run("With detailed errors", func(t *testing.T) {
tests := []struct {
version string
wantErr error
}{
{
"1.2.3",
nil,
},
{
"1.2.3-dev.1",
nil,
},
{
"1.2.3+meta",
nil,
},
{
"1.2.3-dev.1+meta",
nil,
},
{
"1.2.3-dev.1+meta.01",
nil,
},
{
"1.2.3-dev..1",
ErrInvalidSemVer,
},
{
"1.2.3-dev.01",
ErrSegmentStartsZero,
},
{
"1.2.3-de-v.1",
nil,
},
{
"1.02.3",
ErrSegmentStartsZero,
},
{
"01.2.3",
ErrSegmentStartsZero,
},
{
"1.2.03",
ErrSegmentStartsZero,
},
{
"1.2.3-023658",
ErrSegmentStartsZero,
},
}

for _, tc := range tests {
_, err := NewVersion(tc.version)
if err != tc.wantErr {
t.Errorf("expected error %q but got %q for version %s", tc.wantErr, err, tc.version)
}
}
})

t.Run("Without detailed errors", func(t *testing.T) {
DetailedNewVersionErrors = false
defer func() {
DetailedNewVersionErrors = true
}()

tests := []struct {
version string
wantErr error
}{
{
"1.2.3",
nil,
},
{
"1.2.3-dev.1",
nil,
},
{
"1.2.3+meta",
nil,
},
{
"1.2.3-dev.1+meta",
nil,
},
{
"1.2.3-dev.1+meta.01",
nil,
},
{
"1.2.3-dev..1",
ErrInvalidSemVer,
},
{
"1.2.3-dev.01",
ErrInvalidSemVer,
},
{
"1.2.3-de-v.1",
nil,
},
{
"1.02.3",
ErrInvalidSemVer,
},
{
"01.2.3",
ErrInvalidSemVer,
},
{
"1.2.03",
ErrInvalidSemVer,
},
{
"1.2.3-023658",
ErrInvalidSemVer,
},
}

for _, tc := range tests {
_, err := NewVersion(tc.version)
if err != tc.wantErr {
t.Errorf("expected error %q but got %q for version %s", tc.wantErr, err, tc.version)
}
}
})
}

func TestNew(t *testing.T) {
// v0.1.2
v := New(0, 1, 2, "", "")
Expand Down
Loading