422 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package semver
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
)
 | 
						|
 | 
						|
// Constraints is one or more constraint that a semantic version can be
 | 
						|
// checked against.
 | 
						|
type Constraints struct {
 | 
						|
	constraints [][]*constraint
 | 
						|
}
 | 
						|
 | 
						|
// NewConstraint returns a Constraints instance that a Version instance can
 | 
						|
// be checked against. If there is a parse error it will be returned.
 | 
						|
func NewConstraint(c string) (*Constraints, error) {
 | 
						|
 | 
						|
	// Rewrite - ranges into a comparison operation.
 | 
						|
	c = rewriteRange(c)
 | 
						|
 | 
						|
	ors := strings.Split(c, "||")
 | 
						|
	or := make([][]*constraint, len(ors))
 | 
						|
	for k, v := range ors {
 | 
						|
		cs := strings.Split(v, ",")
 | 
						|
		result := make([]*constraint, len(cs))
 | 
						|
		for i, s := range cs {
 | 
						|
			pc, err := parseConstraint(s)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
 | 
						|
			result[i] = pc
 | 
						|
		}
 | 
						|
		or[k] = result
 | 
						|
	}
 | 
						|
 | 
						|
	o := &Constraints{constraints: or}
 | 
						|
	return o, nil
 | 
						|
}
 | 
						|
 | 
						|
// Check tests if a version satisfies the constraints.
 | 
						|
func (cs Constraints) Check(v *Version) bool {
 | 
						|
	// loop over the ORs and check the inner ANDs
 | 
						|
	for _, o := range cs.constraints {
 | 
						|
		joy := true
 | 
						|
		for _, c := range o {
 | 
						|
			if !c.check(v) {
 | 
						|
				joy = false
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if joy {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// Validate checks if a version satisfies a constraint. If not a slice of
 | 
						|
// reasons for the failure are returned in addition to a bool.
 | 
						|
func (cs Constraints) Validate(v *Version) (bool, []error) {
 | 
						|
	// loop over the ORs and check the inner ANDs
 | 
						|
	var e []error
 | 
						|
	for _, o := range cs.constraints {
 | 
						|
		joy := true
 | 
						|
		for _, c := range o {
 | 
						|
			if !c.check(v) {
 | 
						|
				em := fmt.Errorf(c.msg, v, c.orig)
 | 
						|
				e = append(e, em)
 | 
						|
				joy = false
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if joy {
 | 
						|
			return true, []error{}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false, e
 | 
						|
}
 | 
						|
 | 
						|
var constraintOps map[string]cfunc
 | 
						|
var constraintMsg map[string]string
 | 
						|
var constraintRegex *regexp.Regexp
 | 
						|
 | 
						|
func init() {
 | 
						|
	constraintOps = map[string]cfunc{
 | 
						|
		"":   constraintTildeOrEqual,
 | 
						|
		"=":  constraintTildeOrEqual,
 | 
						|
		"!=": constraintNotEqual,
 | 
						|
		">":  constraintGreaterThan,
 | 
						|
		"<":  constraintLessThan,
 | 
						|
		">=": constraintGreaterThanEqual,
 | 
						|
		"=>": constraintGreaterThanEqual,
 | 
						|
		"<=": constraintLessThanEqual,
 | 
						|
		"=<": constraintLessThanEqual,
 | 
						|
		"~":  constraintTilde,
 | 
						|
		"~>": constraintTilde,
 | 
						|
		"^":  constraintCaret,
 | 
						|
	}
 | 
						|
 | 
						|
	constraintMsg = map[string]string{
 | 
						|
		"":   "%s is not equal to %s",
 | 
						|
		"=":  "%s is not equal to %s",
 | 
						|
		"!=": "%s is equal to %s",
 | 
						|
		">":  "%s is less than or equal to %s",
 | 
						|
		"<":  "%s is greater than or equal to %s",
 | 
						|
		">=": "%s is less than %s",
 | 
						|
		"=>": "%s is less than %s",
 | 
						|
		"<=": "%s is greater than %s",
 | 
						|
		"=<": "%s is greater than %s",
 | 
						|
		"~":  "%s does not have same major and minor version as %s",
 | 
						|
		"~>": "%s does not have same major and minor version as %s",
 | 
						|
		"^":  "%s does not have same major version as %s",
 | 
						|
	}
 | 
						|
 | 
						|
	ops := make([]string, 0, len(constraintOps))
 | 
						|
	for k := range constraintOps {
 | 
						|
		ops = append(ops, regexp.QuoteMeta(k))
 | 
						|
	}
 | 
						|
 | 
						|
	constraintRegex = regexp.MustCompile(fmt.Sprintf(
 | 
						|
		`^\s*(%s)\s*(%s)\s*$`,
 | 
						|
		strings.Join(ops, "|"),
 | 
						|
		cvRegex))
 | 
						|
 | 
						|
	constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
 | 
						|
		`\s*(%s)\s+-\s+(%s)\s*`,
 | 
						|
		cvRegex, cvRegex))
 | 
						|
}
 | 
						|
 | 
						|
// An individual constraint
 | 
						|
type constraint struct {
 | 
						|
	// The callback function for the restraint. It performs the logic for
 | 
						|
	// the constraint.
 | 
						|
	function cfunc
 | 
						|
 | 
						|
	msg string
 | 
						|
 | 
						|
	// The version used in the constraint check. For example, if a constraint
 | 
						|
	// is '<= 2.0.0' the con a version instance representing 2.0.0.
 | 
						|
	con *Version
 | 
						|
 | 
						|
	// The original parsed version (e.g., 4.x from != 4.x)
 | 
						|
	orig string
 | 
						|
 | 
						|
	// When an x is used as part of the version (e.g., 1.x)
 | 
						|
	minorDirty bool
 | 
						|
	dirty      bool
 | 
						|
}
 | 
						|
 | 
						|
// Check if a version meets the constraint
 | 
						|
func (c *constraint) check(v *Version) bool {
 | 
						|
	return c.function(v, c)
 | 
						|
}
 | 
						|
 | 
						|
type cfunc func(v *Version, c *constraint) bool
 | 
						|
 | 
						|
func parseConstraint(c string) (*constraint, error) {
 | 
						|
	m := constraintRegex.FindStringSubmatch(c)
 | 
						|
	if m == nil {
 | 
						|
		return nil, fmt.Errorf("improper constraint: %s", c)
 | 
						|
	}
 | 
						|
 | 
						|
	ver := m[2]
 | 
						|
	orig := ver
 | 
						|
	minorDirty := false
 | 
						|
	dirty := false
 | 
						|
	if isX(m[3]) {
 | 
						|
		ver = "0.0.0"
 | 
						|
		dirty = true
 | 
						|
	} else if isX(strings.TrimPrefix(m[4], ".")) {
 | 
						|
		minorDirty = true
 | 
						|
		dirty = true
 | 
						|
		ver = fmt.Sprintf("%s.0.0%s", m[3], m[6])
 | 
						|
	} else if isX(strings.TrimPrefix(m[5], ".")) {
 | 
						|
		dirty = true
 | 
						|
		ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6])
 | 
						|
	}
 | 
						|
 | 
						|
	con, err := NewVersion(ver)
 | 
						|
	if err != nil {
 | 
						|
 | 
						|
		// The constraintRegex should catch any regex parsing errors. So,
 | 
						|
		// we should never get here.
 | 
						|
		return nil, errors.New("constraint Parser Error")
 | 
						|
	}
 | 
						|
 | 
						|
	cs := &constraint{
 | 
						|
		function:   constraintOps[m[1]],
 | 
						|
		msg:        constraintMsg[m[1]],
 | 
						|
		con:        con,
 | 
						|
		orig:       orig,
 | 
						|
		minorDirty: minorDirty,
 | 
						|
		dirty:      dirty,
 | 
						|
	}
 | 
						|
	return cs, nil
 | 
						|
}
 | 
						|
 | 
						|
// Constraint functions
 | 
						|
func constraintNotEqual(v *Version, c *constraint) bool {
 | 
						|
	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
 | 
						|
		}
 | 
						|
 | 
						|
		if c.con.Major() != v.Major() {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		if c.con.Minor() != v.Minor() && !c.minorDirty {
 | 
						|
			return true
 | 
						|
		} else if c.minorDirty {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return !v.Equal(c.con)
 | 
						|
}
 | 
						|
 | 
						|
func constraintGreaterThan(v *Version, c *constraint) bool {
 | 
						|
 | 
						|
	// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
 | 
						|
	// exists. This that case.
 | 
						|
	if !isNonZero(c.con) && isNonZero(v) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	return v.Compare(c.con) == 1
 | 
						|
}
 | 
						|
 | 
						|
func constraintLessThan(v *Version, c *constraint) bool {
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	if !c.dirty {
 | 
						|
		return v.Compare(c.con) < 0
 | 
						|
	}
 | 
						|
 | 
						|
	if v.Major() > c.con.Major() {
 | 
						|
		return false
 | 
						|
	} else if v.Minor() > c.con.Minor() && !c.minorDirty {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
 | 
						|
	// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
 | 
						|
	// exists. This that case.
 | 
						|
	if !isNonZero(c.con) && isNonZero(v) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	return v.Compare(c.con) >= 0
 | 
						|
}
 | 
						|
 | 
						|
func constraintLessThanEqual(v *Version, c *constraint) bool {
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	if !c.dirty {
 | 
						|
		return v.Compare(c.con) <= 0
 | 
						|
	}
 | 
						|
 | 
						|
	if v.Major() > c.con.Major() {
 | 
						|
		return false
 | 
						|
	} else if v.Minor() > c.con.Minor() && !c.minorDirty {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// ~*, ~>* --> >= 0.0.0 (any)
 | 
						|
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0
 | 
						|
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.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.0, ~>1.2.0 --> >=1.2.0, <1.3.0
 | 
						|
func constraintTilde(v *Version, c *constraint) bool {
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	if v.LessThan(c.con) {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	// ~0.0.0 is a special case where all constraints are accepted. It's
 | 
						|
	// equivalent to >= 0.0.0.
 | 
						|
	if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	if v.Major() != c.con.Major() {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	if v.Minor() != c.con.Minor() && !c.minorDirty {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
 | 
						|
// it's a straight =
 | 
						|
func constraintTildeOrEqual(v *Version, c *constraint) bool {
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	if c.dirty {
 | 
						|
		c.msg = constraintMsg["~"]
 | 
						|
		return constraintTilde(v, c)
 | 
						|
	}
 | 
						|
 | 
						|
	return v.Equal(c.con)
 | 
						|
}
 | 
						|
 | 
						|
// ^* --> (any)
 | 
						|
// ^2, ^2.x, ^2.x.x --> >=2.0.0, <3.0.0
 | 
						|
// ^2.0, ^2.0.x --> >=2.0.0, <3.0.0
 | 
						|
// ^1.2, ^1.2.x --> >=1.2.0, <2.0.0
 | 
						|
// ^1.2.3 --> >=1.2.3, <2.0.0
 | 
						|
// ^1.2.0 --> >=1.2.0, <2.0.0
 | 
						|
func constraintCaret(v *Version, c *constraint) bool {
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	if v.LessThan(c.con) {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	if v.Major() != c.con.Major() {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
var constraintRangeRegex *regexp.Regexp
 | 
						|
 | 
						|
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
 | 
						|
	`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
 | 
						|
	`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
 | 
						|
 | 
						|
func isX(x string) bool {
 | 
						|
	switch x {
 | 
						|
	case "x", "*", "X":
 | 
						|
		return true
 | 
						|
	default:
 | 
						|
		return false
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func rewriteRange(i string) string {
 | 
						|
	m := constraintRangeRegex.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
 | 
						|
}
 | 
						|
 | 
						|
// Detect if a version is not zero (0.0.0)
 | 
						|
func isNonZero(v *Version) bool {
 | 
						|
	if v.Major() != 0 || v.Minor() != 0 || v.Patch() != 0 || v.Prerelease() != "" {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 |