417 lines
9.9 KiB
Go
417 lines
9.9 KiB
Go
package semver
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
type wildcardType int
|
|
|
|
const (
|
|
noneWildcard wildcardType = iota
|
|
majorWildcard wildcardType = 1
|
|
minorWildcard wildcardType = 2
|
|
patchWildcard wildcardType = 3
|
|
)
|
|
|
|
func wildcardTypefromInt(i int) wildcardType {
|
|
switch i {
|
|
case 1:
|
|
return majorWildcard
|
|
case 2:
|
|
return minorWildcard
|
|
case 3:
|
|
return patchWildcard
|
|
default:
|
|
return noneWildcard
|
|
}
|
|
}
|
|
|
|
type comparator func(Version, Version) bool
|
|
|
|
var (
|
|
compEQ comparator = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) == 0
|
|
}
|
|
compNE = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) != 0
|
|
}
|
|
compGT = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) == 1
|
|
}
|
|
compGE = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) >= 0
|
|
}
|
|
compLT = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) == -1
|
|
}
|
|
compLE = func(v1 Version, v2 Version) bool {
|
|
return v1.Compare(v2) <= 0
|
|
}
|
|
)
|
|
|
|
type versionRange struct {
|
|
v Version
|
|
c comparator
|
|
}
|
|
|
|
// rangeFunc creates a Range from the given versionRange.
|
|
func (vr *versionRange) rangeFunc() Range {
|
|
return Range(func(v Version) bool {
|
|
return vr.c(v, vr.v)
|
|
})
|
|
}
|
|
|
|
// Range represents a range of versions.
|
|
// A Range can be used to check if a Version satisfies it:
|
|
//
|
|
// range, err := semver.ParseRange(">1.0.0 <2.0.0")
|
|
// range(semver.MustParse("1.1.1") // returns true
|
|
type Range func(Version) bool
|
|
|
|
// OR combines the existing Range with another Range using logical OR.
|
|
func (rf Range) OR(f Range) Range {
|
|
return Range(func(v Version) bool {
|
|
return rf(v) || f(v)
|
|
})
|
|
}
|
|
|
|
// AND combines the existing Range with another Range using logical AND.
|
|
func (rf Range) AND(f Range) Range {
|
|
return Range(func(v Version) bool {
|
|
return rf(v) && f(v)
|
|
})
|
|
}
|
|
|
|
// ParseRange parses a range and returns a Range.
|
|
// If the range could not be parsed an error is returned.
|
|
//
|
|
// Valid ranges are:
|
|
// - "<1.0.0"
|
|
// - "<=1.0.0"
|
|
// - ">1.0.0"
|
|
// - ">=1.0.0"
|
|
// - "1.0.0", "=1.0.0", "==1.0.0"
|
|
// - "!1.0.0", "!=1.0.0"
|
|
//
|
|
// A Range can consist of multiple ranges separated by space:
|
|
// Ranges can be linked by logical AND:
|
|
// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0"
|
|
// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2
|
|
//
|
|
// Ranges can also be linked by logical OR:
|
|
// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
|
|
//
|
|
// AND has a higher precedence than OR. It's not possible to use brackets.
|
|
//
|
|
// Ranges can be combined by both AND and OR
|
|
//
|
|
// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1`
|
|
func ParseRange(s string) (Range, error) {
|
|
parts := splitAndTrim(s)
|
|
orParts, err := splitORParts(parts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expandedParts, err := expandWildcardVersion(orParts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var orFn Range
|
|
for _, p := range expandedParts {
|
|
var andFn Range
|
|
for _, ap := range p {
|
|
opStr, vStr, err := splitComparatorVersion(ap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vr, err := buildVersionRange(opStr, vStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err)
|
|
}
|
|
rf := vr.rangeFunc()
|
|
|
|
// Set function
|
|
if andFn == nil {
|
|
andFn = rf
|
|
} else { // Combine with existing function
|
|
andFn = andFn.AND(rf)
|
|
}
|
|
}
|
|
if orFn == nil {
|
|
orFn = andFn
|
|
} else {
|
|
orFn = orFn.OR(andFn)
|
|
}
|
|
|
|
}
|
|
return orFn, nil
|
|
}
|
|
|
|
// splitORParts splits the already cleaned parts by '||'.
|
|
// Checks for invalid positions of the operator and returns an
|
|
// error if found.
|
|
func splitORParts(parts []string) ([][]string, error) {
|
|
var ORparts [][]string
|
|
last := 0
|
|
for i, p := range parts {
|
|
if p == "||" {
|
|
if i == 0 {
|
|
return nil, fmt.Errorf("First element in range is '||'")
|
|
}
|
|
ORparts = append(ORparts, parts[last:i])
|
|
last = i + 1
|
|
}
|
|
}
|
|
if last == len(parts) {
|
|
return nil, fmt.Errorf("Last element in range is '||'")
|
|
}
|
|
ORparts = append(ORparts, parts[last:])
|
|
return ORparts, nil
|
|
}
|
|
|
|
// buildVersionRange takes a slice of 2: operator and version
|
|
// and builds a versionRange, otherwise an error.
|
|
func buildVersionRange(opStr, vStr string) (*versionRange, error) {
|
|
c := parseComparator(opStr)
|
|
if c == nil {
|
|
return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, ""))
|
|
}
|
|
v, err := Parse(vStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err)
|
|
}
|
|
|
|
return &versionRange{
|
|
v: v,
|
|
c: c,
|
|
}, nil
|
|
|
|
}
|
|
|
|
// inArray checks if a byte is contained in an array of bytes
|
|
func inArray(s byte, list []byte) bool {
|
|
for _, el := range list {
|
|
if el == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// splitAndTrim splits a range string by spaces and cleans whitespaces
|
|
func splitAndTrim(s string) (result []string) {
|
|
last := 0
|
|
var lastChar byte
|
|
excludeFromSplit := []byte{'>', '<', '='}
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) {
|
|
if last < i-1 {
|
|
result = append(result, s[last:i])
|
|
}
|
|
last = i + 1
|
|
} else if s[i] != ' ' {
|
|
lastChar = s[i]
|
|
}
|
|
}
|
|
if last < len(s)-1 {
|
|
result = append(result, s[last:])
|
|
}
|
|
|
|
for i, v := range result {
|
|
result[i] = strings.Replace(v, " ", "", -1)
|
|
}
|
|
|
|
// parts := strings.Split(s, " ")
|
|
// for _, x := range parts {
|
|
// if s := strings.TrimSpace(x); len(s) != 0 {
|
|
// result = append(result, s)
|
|
// }
|
|
// }
|
|
return
|
|
}
|
|
|
|
// splitComparatorVersion splits the comparator from the version.
|
|
// Input must be free of leading or trailing spaces.
|
|
func splitComparatorVersion(s string) (string, string, error) {
|
|
i := strings.IndexFunc(s, unicode.IsDigit)
|
|
if i == -1 {
|
|
return "", "", fmt.Errorf("Could not get version from string: %q", s)
|
|
}
|
|
return strings.TrimSpace(s[0:i]), s[i:], nil
|
|
}
|
|
|
|
// getWildcardType will return the type of wildcard that the
|
|
// passed version contains
|
|
func getWildcardType(vStr string) wildcardType {
|
|
parts := strings.Split(vStr, ".")
|
|
nparts := len(parts)
|
|
wildcard := parts[nparts-1]
|
|
|
|
possibleWildcardType := wildcardTypefromInt(nparts)
|
|
if wildcard == "x" {
|
|
return possibleWildcardType
|
|
}
|
|
|
|
return noneWildcard
|
|
}
|
|
|
|
// createVersionFromWildcard will convert a wildcard version
|
|
// into a regular version, replacing 'x's with '0's, handling
|
|
// special cases like '1.x.x' and '1.x'
|
|
func createVersionFromWildcard(vStr string) string {
|
|
// handle 1.x.x
|
|
vStr2 := strings.Replace(vStr, ".x.x", ".x", 1)
|
|
vStr2 = strings.Replace(vStr2, ".x", ".0", 1)
|
|
parts := strings.Split(vStr2, ".")
|
|
|
|
// handle 1.x
|
|
if len(parts) == 2 {
|
|
return vStr2 + ".0"
|
|
}
|
|
|
|
return vStr2
|
|
}
|
|
|
|
// incrementMajorVersion will increment the major version
|
|
// of the passed version
|
|
func incrementMajorVersion(vStr string) (string, error) {
|
|
parts := strings.Split(vStr, ".")
|
|
i, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts[0] = strconv.Itoa(i + 1)
|
|
|
|
return strings.Join(parts, "."), nil
|
|
}
|
|
|
|
// incrementMajorVersion will increment the minor version
|
|
// of the passed version
|
|
func incrementMinorVersion(vStr string) (string, error) {
|
|
parts := strings.Split(vStr, ".")
|
|
i, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts[1] = strconv.Itoa(i + 1)
|
|
|
|
return strings.Join(parts, "."), nil
|
|
}
|
|
|
|
// expandWildcardVersion will expand wildcards inside versions
|
|
// following these rules:
|
|
//
|
|
// * when dealing with patch wildcards:
|
|
// >= 1.2.x will become >= 1.2.0
|
|
// <= 1.2.x will become < 1.3.0
|
|
// > 1.2.x will become >= 1.3.0
|
|
// < 1.2.x will become < 1.2.0
|
|
// != 1.2.x will become < 1.2.0 >= 1.3.0
|
|
//
|
|
// * when dealing with minor wildcards:
|
|
// >= 1.x will become >= 1.0.0
|
|
// <= 1.x will become < 2.0.0
|
|
// > 1.x will become >= 2.0.0
|
|
// < 1.0 will become < 1.0.0
|
|
// != 1.x will become < 1.0.0 >= 2.0.0
|
|
//
|
|
// * when dealing with wildcards without
|
|
// version operator:
|
|
// 1.2.x will become >= 1.2.0 < 1.3.0
|
|
// 1.x will become >= 1.0.0 < 2.0.0
|
|
func expandWildcardVersion(parts [][]string) ([][]string, error) {
|
|
var expandedParts [][]string
|
|
for _, p := range parts {
|
|
var newParts []string
|
|
for _, ap := range p {
|
|
if strings.Index(ap, "x") != -1 {
|
|
opStr, vStr, err := splitComparatorVersion(ap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
versionWildcardType := getWildcardType(vStr)
|
|
flatVersion := createVersionFromWildcard(vStr)
|
|
|
|
var resultOperator string
|
|
var shouldIncrementVersion bool
|
|
switch opStr {
|
|
case ">":
|
|
resultOperator = ">="
|
|
shouldIncrementVersion = true
|
|
case ">=":
|
|
resultOperator = ">="
|
|
case "<":
|
|
resultOperator = "<"
|
|
case "<=":
|
|
resultOperator = "<"
|
|
shouldIncrementVersion = true
|
|
case "", "=", "==":
|
|
newParts = append(newParts, ">="+flatVersion)
|
|
resultOperator = "<"
|
|
shouldIncrementVersion = true
|
|
case "!=", "!":
|
|
newParts = append(newParts, "<"+flatVersion)
|
|
resultOperator = ">="
|
|
shouldIncrementVersion = true
|
|
}
|
|
|
|
var resultVersion string
|
|
if shouldIncrementVersion {
|
|
switch versionWildcardType {
|
|
case patchWildcard:
|
|
resultVersion, _ = incrementMinorVersion(flatVersion)
|
|
case minorWildcard:
|
|
resultVersion, _ = incrementMajorVersion(flatVersion)
|
|
}
|
|
} else {
|
|
resultVersion = flatVersion
|
|
}
|
|
|
|
ap = resultOperator + resultVersion
|
|
}
|
|
newParts = append(newParts, ap)
|
|
}
|
|
expandedParts = append(expandedParts, newParts)
|
|
}
|
|
|
|
return expandedParts, nil
|
|
}
|
|
|
|
func parseComparator(s string) comparator {
|
|
switch s {
|
|
case "==":
|
|
fallthrough
|
|
case "":
|
|
fallthrough
|
|
case "=":
|
|
return compEQ
|
|
case ">":
|
|
return compGT
|
|
case ">=":
|
|
return compGE
|
|
case "<":
|
|
return compLT
|
|
case "<=":
|
|
return compLE
|
|
case "!":
|
|
fallthrough
|
|
case "!=":
|
|
return compNE
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MustParseRange is like ParseRange but panics if the range cannot be parsed.
|
|
func MustParseRange(s string) Range {
|
|
r, err := ParseRange(s)
|
|
if err != nil {
|
|
panic(`semver: ParseRange(` + s + `): ` + err.Error())
|
|
}
|
|
return r
|
|
}
|