package main import ( "fmt" "regexp" "strconv" "strings" ) // Versions and ranges as npm reads them (node-semver), small enough to read whole: a version, the // order of two, and whether a version satisfies a range. Only what retention needs — which published // version a range resolves to — and nothing npm does beyond that (no coercion, no loose mode). // Version is one semantic version. type Version struct { Major, Minor, Patch int Pre []string // the pre-release identifiers; none for a release raw string } func (v Version) String() string { return v.raw } var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$`) // ParseVersion reads a full version; a partial one (1.2) is not a version. func ParseVersion(s string) (Version, error) { s = strings.TrimSpace(s) m := versionPattern.FindStringSubmatch(s) if m == nil { return Version{}, fmt.Errorf("%q is not a version", s) } v := Version{raw: strings.TrimPrefix(s, "v")} v.Major, _ = strconv.Atoi(m[1]) v.Minor, _ = strconv.Atoi(m[2]) v.Patch, _ = strconv.Atoi(m[3]) if m[4] != "" { v.Pre = strings.Split(m[4], ".") } return v, nil } // Compare is -1, 0 or 1 as a is lower than, equal to or higher than b, by semver precedence. func Compare(a, b Version) int { for _, d := range [][2]int{{a.Major, b.Major}, {a.Minor, b.Minor}, {a.Patch, b.Patch}} { if d[0] != d[1] { if d[0] < d[1] { return -1 } return 1 } } switch { case len(a.Pre) == 0 && len(b.Pre) == 0: return 0 case len(a.Pre) == 0: return 1 // a release is higher than any of its pre-releases case len(b.Pre) == 0: return -1 } for i := 0; i < len(a.Pre) && i < len(b.Pre); i++ { if c := compareIdentifier(a.Pre[i], b.Pre[i]); c != 0 { return c } } switch { case len(a.Pre) < len(b.Pre): return -1 case len(a.Pre) > len(b.Pre): return 1 } return 0 } func compareIdentifier(a, b string) int { an, aerr := strconv.Atoi(a) bn, berr := strconv.Atoi(b) switch { case aerr == nil && berr == nil: switch { case an < bn: return -1 case an > bn: return 1 } return 0 case aerr == nil: return -1 // a numeric identifier is lower than an alphanumeric one case berr == nil: return 1 } return strings.Compare(a, b) } // comparator is one ` `; ANY when the version is the zero bound of `*`. type comparator struct { op string // ">", ">=", "<", "<=", "=" v Version } func (c comparator) test(v Version) bool { d := Compare(v, c.v) switch c.op { case ">": return d > 0 case ">=": return d >= 0 case "<": return d < 0 case "<=": return d <= 0 } return d == 0 } // Range is a set of comparator sets, any one of which a version must satisfy (`||`). type Range struct { sets [][]comparator raw string } func (r Range) String() string { return r.raw } // partial is a version that may stop early or carry x/X/*: -1 for a missing or wild part. type partial struct { major, minor, patch int pre []string } var partialPattern = regexp.MustCompile(`^v?(\d+|[xX*])?(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$`) func parsePartial(s string) (partial, error) { m := partialPattern.FindStringSubmatch(s) if m == nil { return partial{}, fmt.Errorf("%q is not a version or a partial one", s) } num := func(x string) int { if x == "" || x == "x" || x == "X" || x == "*" { return -1 } n, _ := strconv.Atoi(x) return n } p := partial{major: num(m[1]), minor: num(m[2]), patch: num(m[3])} // Nothing below a wild part counts: 1.x.3 is 1.x. if p.major < 0 { p.minor, p.patch = -1, -1 } else if p.minor < 0 { p.patch = -1 } if m[4] != "" { if p.patch < 0 { return partial{}, fmt.Errorf("%q has a pre-release on a partial version", s) } p.pre = strings.Split(m[4], ".") } return p, nil } func ver(major, minor, patch int, pre ...string) Version { raw := fmt.Sprintf("%d.%d.%d", major, minor, patch) if len(pre) > 0 { raw += "-" + strings.Join(pre, ".") } return Version{Major: major, Minor: minor, Patch: patch, Pre: pre, raw: raw} } // lower is the lowest version a partial names; upperExclusive the first version above it (with -0, so // no pre-release of that next version slips in), and whether it has one. func (p partial) lower() Version { switch { case p.major < 0: return ver(0, 0, 0) case p.minor < 0: return ver(p.major, 0, 0) case p.patch < 0: return ver(p.major, p.minor, 0) } return ver(p.major, p.minor, p.patch, p.pre...) } func (p partial) upperExclusive() (Version, bool) { switch { case p.major < 0: return Version{}, false case p.minor < 0: return ver(p.major+1, 0, 0, "0"), true case p.patch < 0: return ver(p.major, p.minor+1, 0, "0"), true } return Version{}, false } var ( hyphenPattern = regexp.MustCompile(`^\s*(\S+)\s+-\s+(\S+)\s*$`) opSpace = regexp.MustCompile(`(>=|<=|>|<|=|\^|~>?)\s+`) ) // ParseRange reads a range as npm does: exact, `^`, `~`, x-ranges, comparisons, hyphen ranges, sets // joined by spaces, and `||`. func ParseRange(s string) (Range, error) { r := Range{raw: s} for _, part := range strings.Split(s, "||") { set, err := parseSet(strings.TrimSpace(part)) if err != nil { return Range{}, err } r.sets = append(r.sets, set) } return r, nil } func parseSet(s string) ([]comparator, error) { if m := hyphenPattern.FindStringSubmatch(s); m != nil { from, err := parsePartial(m[1]) if err != nil { return nil, err } to, err := parsePartial(m[2]) if err != nil { return nil, err } set := []comparator{{">=", from.lower()}} if up, has := to.upperExclusive(); has { set = append(set, comparator{"<", up}) } else if to.major >= 0 { set = append(set, comparator{"<=", to.lower()}) } return set, nil } s = opSpace.ReplaceAllString(s, "$1") if s == "" { return []comparator{{">=", ver(0, 0, 0)}}, nil } var set []comparator for _, word := range strings.Fields(s) { cs, err := parseComparator(word) if err != nil { return nil, err } set = append(set, cs...) } return set, nil } func parseComparator(w string) ([]comparator, error) { var op string for _, o := range []string{">=", "<=", "~>", ">", "<", "=", "^", "~"} { if strings.HasPrefix(w, o) { op, w = o, w[len(o):] break } } p, err := parsePartial(w) if err != nil { return nil, err } lo := p.lower() up, bounded := p.upperExclusive() switch op { case "", "=": if !bounded && p.major >= 0 { return []comparator{{"=", lo}}, nil } return xrange(lo, up, bounded), nil case "^": var hi Version switch { case p.major < 0: return []comparator{{">=", ver(0, 0, 0)}}, nil case p.major > 0 || p.minor < 0: hi = ver(p.major+1, 0, 0, "0") case p.minor > 0 || p.patch < 0: hi = ver(0, p.minor+1, 0, "0") default: hi = ver(0, 0, p.patch+1, "0") } return []comparator{{">=", lo}, {"<", hi}}, nil case "~", "~>": var hi Version switch { case p.major < 0: return []comparator{{">=", ver(0, 0, 0)}}, nil case p.minor < 0: hi = ver(p.major+1, 0, 0, "0") default: hi = ver(p.major, p.minor+1, 0, "0") } return []comparator{{">=", lo}, {"<", hi}}, nil case ">": if p.major < 0 { return []comparator{{"<", ver(0, 0, 0, "0")}}, nil // nothing } if bounded { return []comparator{{">=", up}}, nil } return []comparator{{">", lo}}, nil case ">=": return []comparator{{">=", lo}}, nil case "<": if p.major < 0 { return []comparator{{"<", ver(0, 0, 0, "0")}}, nil } return []comparator{{"<", lo}}, nil case "<=": if p.major < 0 { return []comparator{{">=", ver(0, 0, 0)}}, nil } if bounded { return []comparator{{"<", up}}, nil } return []comparator{{"<=", lo}}, nil } return nil, fmt.Errorf("%q is not a comparator", w) } func xrange(lo, up Version, bounded bool) []comparator { if !bounded { return []comparator{{">=", lo}} } return []comparator{{">=", lo}, {"<", up}} } // Satisfies is whether v is in the range. A pre-release satisfies a set only when one of its // comparators names the same major.minor.patch with a pre-release of its own — npm's rule, so that // `^1.2.0` never resolves to 1.3.0-beta. func (r Range) Satisfies(v Version) bool { for _, set := range r.sets { if testSet(set, v) { return true } } return false } func testSet(set []comparator, v Version) bool { for _, c := range set { if !c.test(v) { return false } } if len(v.Pre) == 0 { return true } for _, c := range set { if len(c.v.Pre) > 0 && c.v.Major == v.Major && c.v.Minor == v.Minor && c.v.Patch == v.Patch { return true } } return false } // MaxSatisfying is the highest of the versions in the range; false when none is. func MaxSatisfying(r Range, versions []Version) (Version, bool) { var best Version found := false for _, v := range versions { if r.Satisfies(v) && (!found || Compare(v, best) > 0) { best, found = v, true } } return best, found }