package semver import ( "regexp" "strings" ) // https://github.com/npm/node-semver#range-grammar // // range-set ::= range ( logical-or range ) * // range ::= hyphen | simple ( ' ' simple ) * | ” // logical-or ::= ( ' ' ) * '||' ( ' ' ) * var ( logicalOrRegExp = regexp.MustCompile(`\|\|`) whitespaceRegExp = regexp.MustCompile(`\s+`) ) // https://github.com/npm/node-semver#range-grammar // // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? // xr ::= 'x' | 'X' | '*' | nr // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * // qualifier ::= ( '-' pre )? ( '+' build )? // pre ::= parts // build ::= parts // parts ::= part ( '.' part ) * // part ::= nr | [-0-9A-Za-z]+ var partialRegExp = regexp.MustCompile(`(?i)^([x*0]|[1-9]\d*)(?:\.([x*0]|[1-9]\d*)(?:\.([x*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$`) // https://github.com/npm/node-semver#range-grammar // // hyphen ::= partial ' - ' partial var hyphenRegExp = regexp.MustCompile(`(?i)^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$`) // https://github.com/npm/node-semver#range-grammar // // simple ::= primitive | partial | tilde | caret // primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial // tilde ::= '~' partial // caret ::= '^' partial var rangeRegExp = regexp.MustCompile(`(?i)^([~^<>=]|<=|>=)?\s*([a-z0-9-+.*]+)$`) type VersionRange struct { alternatives [][]versionComparator } type versionComparator struct { operator comparatorOperator operand Version } type comparatorOperator string const ( rangeLessThan comparatorOperator = "<" rangeLessThanEqual comparatorOperator = "<=" rangeEqual comparatorOperator = "=" rangeGreaterThanEqual comparatorOperator = ">=" rangeGreaterThan comparatorOperator = ">" ) func (v *VersionRange) String() string { var sb strings.Builder formatDisjunction(&sb, v.alternatives) return sb.String() } func formatDisjunction(sb *strings.Builder, alternatives [][]versionComparator) { origLen := sb.Len() for i, alternative := range alternatives { if i > 0 { sb.WriteString(" || ") } formatAlternative(sb, alternative) } if sb.Len() == origLen { sb.WriteString("*") } } func formatAlternative(sb *strings.Builder, comparators []versionComparator) { for i, comparator := range comparators { if i > 0 { sb.WriteByte(' ') } formatComparator(sb, comparator) } } func formatComparator(sb *strings.Builder, comparator versionComparator) { sb.WriteString(string(comparator.operator)) sb.WriteString(comparator.operand.String()) } func (v *VersionRange) Test(version *Version) bool { return testDisjunction(v.alternatives, version) } func testDisjunction(alternatives [][]versionComparator, version *Version) bool { // an empty disjunction is treated as "*" (all versions) if len(alternatives) == 0 { return true } for _, alternative := range alternatives { if testAlternative(alternative, version) { return true } } return false } func testAlternative(alternative []versionComparator, version *Version) bool { for _, comparator := range alternative { if !testComparator(comparator, version) { return false } } return true } func testComparator(comparator versionComparator, version *Version) bool { cmp := version.Compare(&comparator.operand) switch comparator.operator { case rangeLessThan: return cmp < 0 case rangeLessThanEqual: return cmp <= 0 case rangeEqual: return cmp == 0 case rangeGreaterThanEqual: return cmp >= 0 case rangeGreaterThan: return cmp > 0 default: panic("Unexpected operator: " + comparator.operator) } } func TryParseVersionRange(text string) (VersionRange, bool) { alternatives, ok := parseAlternatives(text) return VersionRange{alternatives: alternatives}, ok } func parseAlternatives(text string) ([][]versionComparator, bool) { var alternatives [][]versionComparator text = strings.TrimSpace(text) ranges := logicalOrRegExp.Split(text, -1) for _, r := range ranges { r = strings.TrimSpace(r) if r == "" { continue } var comparators []versionComparator if hyphenMatch := hyphenRegExp.FindStringSubmatch(r); hyphenMatch != nil { if parsedComparators, ok := parseHyphen(hyphenMatch[1], hyphenMatch[2]); ok { comparators = append(comparators, parsedComparators...) } else { return nil, false } } else { for _, simple := range whitespaceRegExp.Split(r, -1) { match := rangeRegExp.FindStringSubmatch(strings.TrimSpace(simple)) if match == nil { return nil, false } if parsedComparators, ok := parseComparator(match[1], match[2]); ok { comparators = append(comparators, parsedComparators...) } else { return nil, false } } } alternatives = append(alternatives, comparators) } return alternatives, true } func parseHyphen(left, right string) ([]versionComparator, bool) { leftResult, leftOk := parsePartial(left) if !leftOk { return nil, false } rightResult, rightOk := parsePartial(right) if !rightOk { return nil, false } var comparators []versionComparator if !isWildcard(leftResult.majorStr) { // `MAJOR.*.*-...` gives us `>=MAJOR.0.0 ...` comparators = append(comparators, versionComparator{ operator: rangeGreaterThanEqual, operand: leftResult.version, }) } if !isWildcard(rightResult.majorStr) { var operator comparatorOperator operand := rightResult.version switch { case isWildcard(rightResult.minorStr): // `...-MAJOR.*.*` gives us `... <(MAJOR+1).0.0` operand = operand.incrementMajor() operator = rangeLessThan case isWildcard(rightResult.patchStr): // `...-MAJOR.MINOR.*` gives us `... 0 || isWildcard(result.minorStr) { secondVersion = result.version.incrementMajor() } else if result.version.minor > 0 || isWildcard(result.patchStr) { secondVersion = result.version.incrementMinor() } else { secondVersion = result.version.incrementPatch() } second := versionComparator{rangeLessThan, secondVersion} comparatorsResult = []versionComparator{first, second} case "<", ">=": version := result.version if isWildcard(result.minorStr) || isWildcard(result.patchStr) { version.prerelease = []string{"0"} } comparatorsResult = []versionComparator{ {operator, version}, } case "<=", ">": version := result.version if isWildcard(result.minorStr) { if operator == rangeLessThanEqual { operator = rangeLessThan } else { operator = rangeGreaterThanEqual } version = version.incrementMajor() version.prerelease = []string{"0"} } else if isWildcard(result.patchStr) { if operator == rangeLessThanEqual { operator = rangeLessThan } else { operator = rangeGreaterThanEqual } version = version.incrementMinor() version.prerelease = []string{"0"} } comparatorsResult = []versionComparator{ {operator, version}, } case "=", "": // normalize empty string to `=` operator = rangeEqual if isWildcard(result.minorStr) || isWildcard(result.patchStr) { originalVersion := result.version firstVersion := originalVersion firstVersion.prerelease = []string{"0"} var secondVersion Version if isWildcard(result.minorStr) { secondVersion = originalVersion.incrementMajor() } else { secondVersion = originalVersion.incrementMinor() } secondVersion.prerelease = []string{"0"} comparatorsResult = []versionComparator{ {rangeGreaterThanEqual, firstVersion}, {rangeLessThan, secondVersion}, } } else { comparatorsResult = []versionComparator{ {operator, result.version}, } } default: panic("Unexpected operator: " + operator) } } else { if operator == "<" || operator == ">" { comparatorsResult = []versionComparator{ // < 0.0.0-0 {rangeLessThan, versionZero}, } } } return comparatorsResult, true } func isWildcard(text string) bool { return text == "*" || text == "x" || text == "X" }