diff --git a/constraints.go b/constraints.go index e8353bc..94fb78d 100644 --- a/constraints.go +++ b/constraints.go @@ -98,7 +98,7 @@ func (cs Constraints) Check(v *Version) bool { for i, o := range cs.constraints { joy := true for _, c := range o { - if check, _ := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); !check { + if check, _ := c.check(v, cs.IncludePrerelease || cs.containsPre[i], false); !check { joy = false break } @@ -136,7 +136,7 @@ func (cs Constraints) Validate(v *Version) (bool, []error) { } else { - if _, err := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); err != nil { + if _, err := c.check(v, cs.IncludePrerelease || cs.containsPre[i], true); err != nil { e = append(e, err) joy = false } @@ -265,8 +265,8 @@ type constraint struct { } // Check if a version meets the constraint -func (c *constraint) check(v *Version, includePre bool) (bool, error) { - return constraintOps[c.origfunc](v, c, includePre) +func (c *constraint) check(v *Version, includePre, reportErr bool) (bool, error) { + return constraintOps[c.origfunc](v, c, includePre, reportErr) } // String prints an individual constraint into a string @@ -274,7 +274,20 @@ func (c *constraint) string() string { return c.origfunc + c.orig } -type cfunc func(v *Version, c *constraint, includePre bool) (bool, error) +type cfunc func(v *Version, c *constraint, includePre, reportErr bool) (bool, error) + +// cerr builds the error explaining why a constraint check failed. When +// reportErr is false the caller (Check) discards the error, so nothing is +// formatted and no allocation happens. +func cerr(reportErr bool, format string, v *Version, orig string) error { + if !reportErr { + return nil + } + if orig == "" { + return fmt.Errorf(format, v) + } + return fmt.Errorf(format, v, orig) +} func parseConstraint(c string) (*constraint, error) { if len(c) > 0 { @@ -343,11 +356,11 @@ func parseConstraint(c string) (*constraint, error) { } // Constraint functions -func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintNotEqual(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } if c.dirty { @@ -357,7 +370,7 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error if c.con.Minor() != v.Minor() && !c.minorDirty { return true, nil } else if c.minorDirty { - return false, fmt.Errorf("%q is equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is equal to %q", v, c.orig) } else if c.con.Patch() != v.Patch() && !c.patchDirty { return true, nil } else if c.patchDirty { @@ -367,26 +380,26 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error if eq { return true, nil } - return false, fmt.Errorf("%q is equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is equal to %q", v, c.orig) } - return false, fmt.Errorf("%q is equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is equal to %q", v, c.orig) } } eq := v.Equal(c.con) if eq { - return false, fmt.Errorf("%q is equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is equal to %q", v, c.orig) } return true, nil } -func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintGreaterThan(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } var eq bool @@ -396,17 +409,17 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er if eq { return true, nil } - return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is less than or equal to %q", v, c.orig) } if v.Major() > c.con.Major() { return true, nil } else if v.Major() < c.con.Major() { - return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is less than or equal to %q", v, c.orig) } else if c.minorDirty { // This is a range case such as >11. When the version is something like // 11.1.0 is it not > 11. For that we would need 12 or higher - return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is less than or equal to %q", v, c.orig) } else if c.patchDirty { // This is for ranges such as >11.1. A version of 11.1.1 is not greater // which one of 11.2.1 is greater @@ -414,7 +427,7 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er if eq { return true, nil } - return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is less than or equal to %q", v, c.orig) } // If we have gotten here we are not comparing pre-preleases and can use the @@ -423,43 +436,43 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er if eq { return true, nil } - return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is less than or equal to %q", v, c.orig) } -func constraintLessThan(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintLessThan(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } eq := v.Compare(c.con) < 0 if eq { return true, nil } - return false, fmt.Errorf("%q is greater than or equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is greater than or equal to %q", v, c.orig) } -func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintGreaterThanEqual(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } eq := v.Compare(c.con) >= 0 if eq { return true, nil } - return false, fmt.Errorf("%q is less than %q", v, c.orig) + return false, cerr(reportErr, "%q is less than %q", v, c.orig) } -func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintLessThanEqual(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } var eq bool @@ -469,13 +482,13 @@ func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, if eq { return true, nil } - return false, fmt.Errorf("%q is greater than %q", v, c.orig) + return false, cerr(reportErr, "%q is greater than %q", v, c.orig) } if v.Major() > c.con.Major() { - return false, fmt.Errorf("%q is greater than %q", v, c.orig) + return false, cerr(reportErr, "%q is greater than %q", v, c.orig) } else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty { - return false, fmt.Errorf("%q is greater than %q", v, c.orig) + return false, cerr(reportErr, "%q is greater than %q", v, c.orig) } return true, nil @@ -487,15 +500,15 @@ func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (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 -func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintTilde(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } if v.LessThan(c.con) { - return false, fmt.Errorf("%q is less than %q", v, c.orig) + return false, cerr(reportErr, "%q is less than %q", v, c.orig) } // ~0.0.0 is a special case where all constraints are accepted. It's @@ -506,11 +519,11 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) { } if v.Major() != c.con.Major() { - return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig) + return false, cerr(reportErr, "%q does not have same major version as %q", v, c.orig) } if v.Minor() != c.con.Minor() && !c.minorDirty { - return false, fmt.Errorf("%q does not have same major and minor version as %q", v, c.orig) + return false, cerr(reportErr, "%q does not have same major and minor version as %q", v, c.orig) } return true, nil @@ -518,15 +531,15 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) { // When there is a .x (dirty) status it automatically opts in to ~. Otherwise // it's a straight = -func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintTildeOrEqual(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } if c.dirty { - return constraintTilde(v, c, includePre) + return constraintTilde(v, c, includePre, reportErr) } eq := v.Equal(c.con) @@ -534,7 +547,7 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e return true, nil } - return false, fmt.Errorf("%q is not equal to %q", v, c.orig) + return false, cerr(reportErr, "%q is not equal to %q", v, c.orig) } // ^* --> (any) @@ -546,16 +559,16 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e // ^0.0.3 --> >=0.0.3 <0.0.4 // ^0.0 --> >=0.0.0 <0.1.0 // ^0 --> >=0.0.0 <1.0.0 -func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) { +func constraintCaret(v *Version, c *constraint, includePre, reportErr 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("%q is a prerelease version and the constraint is only looking for release versions", v) + return false, cerr(reportErr, "%q is a prerelease version and the constraint is only looking for release versions", v, "") } // This less than handles prereleases if v.LessThan(c.con) { - return false, fmt.Errorf("%q is less than %q", v, c.orig) + return false, cerr(reportErr, "%q is less than %q", v, c.orig) } var eq bool @@ -570,12 +583,12 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) { if eq { return true, nil } - return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig) + return false, cerr(reportErr, "%q does not have same major version as %q", v, c.orig) } // ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1 if c.con.Major() == 0 && v.Major() > 0 { - return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig) + return false, cerr(reportErr, "%q does not have same major version as %q", v, c.orig) } // If the con Minor is > 0 it is not dirty if c.con.Minor() > 0 || c.patchDirty { @@ -583,11 +596,11 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) { if eq { return true, nil } - return false, fmt.Errorf("%q does not have same minor version as %q. Expected minor versions to match when constraint major version is 0", v, c.orig) + return false, cerr(reportErr, "%q does not have same minor version as %q. Expected minor versions to match when constraint major version is 0", v, c.orig) } // ^ when the minor is 0 and minor > 0 is =0.0.z if c.con.Minor() == 0 && v.Minor() > 0 { - return false, fmt.Errorf("%q does not have same minor version as %q", v, c.orig) + return false, cerr(reportErr, "%q does not have same minor version as %q", v, c.orig) } // At this point the major is 0 and the minor is 0 and not dirty. The patch @@ -596,7 +609,7 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) { if eq { return true, nil } - return false, fmt.Errorf("%q does not equal %q. Expect version and constraint to equal when major and minor versions are 0", v, c.orig) + return false, cerr(reportErr, "%q does not equal %q. Expect version and constraint to equal when major and minor versions are 0", v, c.orig) } func isX(x string) bool { @@ -609,6 +622,11 @@ func isX(x string) bool { } func rewriteRange(i string) string { + // A range requires a hyphen. Skip the regex when there is not one. + if !strings.Contains(i, "-") { + return i + } + m := constraintRangeRegex.FindAllStringSubmatch(i, -1) if m == nil { return i diff --git a/constraints_test.go b/constraints_test.go index fe2c14b..a5850db 100644 --- a/constraints_test.go +++ b/constraints_test.go @@ -210,7 +210,7 @@ func TestConstraintCheck(t *testing.T) { hasPre = true } - a, _ := c.check(v, hasPre) + a, _ := c.check(v, hasPre, true) if a != tc.check { t.Errorf("Constraint %q failing with %q", tc.constraint, tc.version) } diff --git a/version.go b/version.go index 84544f4..3bba645 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,6 @@ package semver import ( - "bytes" "database/sql/driver" "encoding/json" "errors" @@ -114,30 +113,34 @@ func StrictNewVersion(v string) (*Version, error) { } // Split the parts into [0]major, [1]minor, and [2]patch,prerelease,build - parts := strings.SplitN(v, ".", 3) - if len(parts) != 3 { - return nil, ErrInvalidSemVer + var parts [3]string + rest := v + for i := 0; i < 2; i++ { + j := strings.IndexByte(rest, '.') + if j < 0 { + return nil, ErrInvalidSemVer + } + parts[i], rest = rest[:j], rest[j+1:] } + parts[2] = rest sv := &Version{ original: v, } // Extract build metadata - if strings.Contains(parts[2], "+") { - extra := strings.SplitN(parts[2], "+", 2) - sv.metadata = extra[1] - parts[2] = extra[0] + if i := strings.IndexByte(parts[2], '+'); i >= 0 { + sv.metadata = parts[2][i+1:] + parts[2] = parts[2][:i] if err := validateMetadata(sv.metadata); err != nil { return nil, err } } // Extract build prerelease - if strings.Contains(parts[2], "-") { - extra := strings.SplitN(parts[2], "-", 2) - sv.pre = extra[1] - parts[2] = extra[0] + if i := strings.IndexByte(parts[2], '-'); i >= 0 { + sv.pre = parts[2][i+1:] + parts[2] = parts[2][:i] if err := validatePrerelease(sv.pre); err != nil { return nil, err } @@ -146,7 +149,7 @@ func StrictNewVersion(v string) (*Version, error) { // Validate the number segments are valid. This includes only having positive // numbers and no leading 0's. for _, p := range parts { - if !containsOnly(p, num) { + if !containsOnlyNum(p) { return nil, ErrInvalidCharacters } @@ -255,58 +258,113 @@ func NewVersion(v string) (*Version, error) { return sv, nil } +// coerceNewVersion parses a SemVer-ish version without using a regular +// expression. Versions such as 1 or 1.2 are coerced into a full version. func coerceNewVersion(v string) (*Version, error) { - m := looseVersionRegex.FindStringSubmatch(v) - if m == nil { - return nil, ErrInvalidSemVer + s := v + if len(s) > 0 && s[0] == 'v' { + s = s[1:] } sv := &Version{ - metadata: m[8], - pre: m[5], original: v, } + // Metadata is everything following the first +. It is separated first + // because a - is a valid character within metadata. + if i := strings.IndexByte(s, '+'); i >= 0 { + sv.metadata = s[i+1:] + s = s[:i] + if !validIdentifiers(sv.metadata) { + return nil, ErrInvalidSemVer + } + } + + // The prerelease is everything following the first - that remains after + // the metadata has been removed. + if i := strings.IndexByte(s, '-'); i >= 0 { + sv.pre = s[i+1:] + s = s[:i] + if !validIdentifiers(sv.pre) { + return nil, ErrInvalidSemVer + } + } + + // What remains are the major, minor, and patch segments. Missing minor and + // patch segments are coerced to 0. The segments are checked before any are + // parsed so that an invalid segment is reported ahead of a numeric one + // that is out of range. + var segs [3]string + n := 0 + for { + var seg string + more := false + if i := strings.IndexByte(s, '.'); i >= 0 { + seg, s, more = s[:i], s[i+1:], true + } else { + seg, s = s, "" + } + + if n > 2 || seg == "" || !containsOnlyNum(seg) { + return nil, ErrInvalidSemVer + } + segs[n] = seg + n++ + + if !more { + break + } + } + var err error - sv.major, err = strconv.ParseUint(m[1], 10, 64) - if err != nil { + if sv.major, err = strconv.ParseUint(segs[0], 10, 64); err != nil { return nil, fmt.Errorf("error parsing version segment: %w", err) } - if m[2] != "" { - sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64) - if err != nil { + if n > 1 { + if sv.minor, err = strconv.ParseUint(segs[1], 10, 64); err != nil { return nil, fmt.Errorf("error parsing version segment: %w", err) } - } else { - sv.minor = 0 } - if m[3] != "" { - sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64) - if err != nil { + if n > 2 { + if sv.patch, err = strconv.ParseUint(segs[2], 10, 64); 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. - + // The characters in the prerelease are already known to be valid. This + // catches the numeric segments that have a leading 0. if sv.pre != "" { - if err = validatePrerelease(sv.pre); err != nil { + if err := validatePrerelease(sv.pre); err != nil { return nil, err } } - if sv.metadata != "" { - if err = validateMetadata(sv.metadata); err != nil { - return nil, err + return sv, nil +} + +// validIdentifiers reports if s is a series of dot separated identifiers made +// up of the characters allowed in a prerelease or metadata string. Identifiers +// must not be empty. +func validIdentifiers(s string) bool { + for { + var part string + more := false + if i := strings.IndexByte(s, '.'); i >= 0 { + part, s, more = s[:i], s[i+1:], true + } else { + part, s = s, "" } - } - return sv, nil + if part == "" || !containsOnlyAllowed(part) { + return false + } + + if !more { + return true + } + } } // New creates a new instance of Version with each of the parts passed in as @@ -344,17 +402,24 @@ func MustParse(v string) *Version { // don't contain a leading v per the spec. Instead it's optional on // implementation. func (v Version) String() string { - var buf bytes.Buffer - - fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch) + var b [64]byte + buf := b[:0] + + buf = strconv.AppendUint(buf, v.major, 10) + buf = append(buf, '.') + buf = strconv.AppendUint(buf, v.minor, 10) + buf = append(buf, '.') + buf = strconv.AppendUint(buf, v.patch, 10) if v.pre != "" { - fmt.Fprintf(&buf, "-%s", v.pre) + buf = append(buf, '-') + buf = append(buf, v.pre...) } if v.metadata != "" { - fmt.Fprintf(&buf, "+%s", v.metadata) + buf = append(buf, '+') + buf = append(buf, v.metadata...) } - return buf.String() + return string(buf) } // Original returns the original value passed in to be parsed. @@ -690,37 +755,14 @@ func compareSegment(v, o uint64) int { } func comparePrerelease(v, o string) int { - // split the prelease versions by their part. The separator, per the spec, - // is a . - sparts := strings.Split(v, ".") - oparts := strings.Split(o, ".") - - // Find the longer length of the parts to know how many loop iterations to - // go through. - slen := len(sparts) - olen := len(oparts) - - l := slen - if olen > slen { - l = olen - } - - // Iterate over each part of the prereleases to compare the differences. - for i := 0; i < l; i++ { - // Since the lentgh of the parts can be different we need to create - // a placeholder. This is to avoid out of bounds issues. - stemp := "" - if i < slen { - stemp = sparts[i] - } - - otemp := "" - if i < olen { - otemp = oparts[i] - } - - d := comparePrePart(stemp, otemp) - if d != 0 { + // Walk the dot separated parts of both prereleases without allocating + // slices for the parts. + for v != "" || o != "" { + var sp, op string + sp, v = nextPart(v) + op, o = nextPart(o) + + if d := comparePrePart(sp, op); d != 0 { return d } } @@ -731,6 +773,15 @@ func comparePrerelease(v, o string) int { return 0 } +// nextPart returns the leading dot separated segment of s along with the +// remainder of s following the dot. +func nextPart(s string) (part, rest string) { + if i := strings.IndexByte(s, '.'); i >= 0 { + return s[:i], s[i+1:] + } + return s, "" +} + func comparePrePart(s, o string) int { // Fastpath if they are equal if s == o { @@ -783,11 +834,38 @@ func comparePrePart(s, o string) int { return -1 } -// Like strings.ContainsAny but does an only instead of any. -func containsOnly(s string, comp string) bool { - return strings.IndexFunc(s, func(r rune) bool { - return !strings.ContainsRune(comp, r) - }) == -1 +// allowedChars and numChars are lookup tables for the characters allowed in +// the identifier and numeric portions of a version. +var allowedChars, numChars [256]bool + +func init() { + for i := 0; i < len(allowed); i++ { + allowedChars[allowed[i]] = true + } + for i := 0; i < len(num); i++ { + numChars[num[i]] = true + } +} + +// containsOnlyNum reports if s is made up only of the digits 0-9. +func containsOnlyNum(s string) bool { + for i := 0; i < len(s); i++ { + if !numChars[s[i]] { + return false + } + } + return true +} + +// containsOnlyAllowed reports if s is made up only of the characters valid in +// a prerelease or metadata identifier. +func containsOnlyAllowed(s string) bool { + for i := 0; i < len(s); i++ { + if !allowedChars[s[i]] { + return false + } + } + return true } // From the spec, "Identifiers MUST comprise only @@ -795,16 +873,17 @@ func containsOnly(s string, comp string) bool { // Numeric identifiers MUST NOT include leading zeroes.". These segments can // be dot separated. func validatePrerelease(p string) error { - eparts := strings.Split(p, ".") - for _, p := range eparts { - if p == "" { - return ErrInvalidPrerelease - } else if containsOnly(p, num) { - if len(p) > 1 && p[0] == '0' { - return ErrSegmentStartsZero - } - } else if !containsOnly(p, allowed) { - return ErrInvalidPrerelease + if !validIdentifiers(p) { + return ErrInvalidPrerelease + } + + // The identifiers are known to be valid and non-empty. Numeric identifiers + // must not have a leading 0. + for p != "" { + var part string + part, p = nextPart(p) + if len(part) > 1 && part[0] == '0' && containsOnlyNum(part) { + return ErrSegmentStartsZero } } @@ -816,13 +895,8 @@ func validatePrerelease(p string) error { // following the patch or pre-release version. Identifiers MUST comprise only // ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty." func validateMetadata(m string) error { - eparts := strings.Split(m, ".") - for _, p := range eparts { - if p == "" { - return ErrInvalidMetadata - } else if !containsOnly(p, allowed) { - return ErrInvalidMetadata - } + if !validIdentifiers(m) { + return ErrInvalidMetadata } return nil } diff --git a/version_test.go b/version_test.go index 5ebe9e0..6d35697 100644 --- a/version_test.go +++ b/version_test.go @@ -70,6 +70,11 @@ func TestStrictNewVersion(t *testing.T) { {"alpha+beta", true}, {"1.2.3-alpha_beta+foo", true}, {"1.0.0-alpha..1", true}, + {"1.0.0-", true}, // An empty pre-release + {"1.0.0+", true}, // An empty metadata + {"1.0.0-alpha+", true}, // An empty metadata following a pre-release + {"1.0.0-alpha.", true}, // A trailing empty pre-release segment + {"1.0.0+meta.", true}, // A trailing empty metadata segment } for _, tc := range tests { @@ -137,6 +142,13 @@ func TestNewVersion(t *testing.T) { {"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value {"9.8.7+meta+meta", true}, // Multiple metadata parts {"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment + {"1.0.0-", true}, // An empty pre-release + {"1.0.0+", true}, // An empty metadata + {"1.0.0-alpha+", true}, // An empty metadata following a pre-release + {"1.0.0-alpha.", true}, // A trailing empty pre-release segment + {"1.0.0+meta.", true}, // A trailing empty metadata segment + {"1.2.", true}, // A trailing empty number segment + {"1.", true}, // A trailing empty number segment // Versions that are invalid but in loose mode are handled. // This enables a calver-ish style. This pattern has long