mirror of
https://github.com/hanzoai/flags.git
synced 2026-10-30 18:36:27 +00:00
410 lines
9.1 KiB
Go
410 lines
9.1 KiB
Go
package flags
|
|
|
|
// Semantic versions, matching the Rust `semver` crate the reference evaluator
|
|
// uses — which is Cargo's dialect, not the bare SemVer spec:
|
|
//
|
|
// - parsing is strict: three numeric components, no leading zeros, no
|
|
// surrounding whitespace (the evaluator strips a leading "v" and trims first)
|
|
// - ORDERING ignores nothing: build metadata participates, so 1.2.3+build.1
|
|
// sorts below 1.2.3+build.2 even though the spec says they have equal
|
|
// precedence
|
|
// - a bare requirement means caret: "1.2.3" is "^1.2.3"
|
|
// - a prerelease version only satisfies a requirement when some comparator
|
|
// carries a prerelease on the same major.minor.patch, so ^1.2.3 rejects
|
|
// 1.3.0-alpha
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type semver struct {
|
|
major, minor, patch uint64
|
|
pre []string
|
|
build []string
|
|
}
|
|
|
|
// normalizeVersion strips a leading "v" and then trims. The order matters and
|
|
// is the reference's: " v1.2.3 " does not normalize, because the "v" is not at
|
|
// position zero when the prefix is tested.
|
|
func normalizeVersion(s string) string {
|
|
return strings.TrimSpace(strings.TrimPrefix(s, "v"))
|
|
}
|
|
|
|
func toSemver(v value) (semver, bool) {
|
|
return parseVersion(normalizeVersion(v.str()))
|
|
}
|
|
|
|
func parseVersion(s string) (semver, bool) {
|
|
var out semver
|
|
rest := s
|
|
if i := strings.IndexByte(rest, '+'); i >= 0 {
|
|
b, ok := parseIdents(rest[i+1:], true)
|
|
if !ok {
|
|
return out, false
|
|
}
|
|
out.build = b
|
|
rest = rest[:i]
|
|
}
|
|
if i := strings.IndexByte(rest, '-'); i >= 0 {
|
|
p, ok := parseIdents(rest[i+1:], false)
|
|
if !ok {
|
|
return out, false
|
|
}
|
|
out.pre = p
|
|
rest = rest[:i]
|
|
}
|
|
parts := strings.Split(rest, ".")
|
|
if len(parts) != 3 {
|
|
return out, false
|
|
}
|
|
var ok bool
|
|
if out.major, ok = parseNumeric(parts[0]); !ok {
|
|
return out, false
|
|
}
|
|
if out.minor, ok = parseNumeric(parts[1]); !ok {
|
|
return out, false
|
|
}
|
|
if out.patch, ok = parseNumeric(parts[2]); !ok {
|
|
return out, false
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// parseNumeric accepts digits with no leading zero (bare "0" excepted).
|
|
func parseNumeric(s string) (uint64, bool) {
|
|
if s == "" || (len(s) > 1 && s[0] == '0') {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
// parseIdents splits dot-separated identifiers. Prerelease numeric identifiers
|
|
// may not carry leading zeros; build metadata may.
|
|
func parseIdents(s string, isBuild bool) ([]string, bool) {
|
|
if s == "" {
|
|
return nil, false
|
|
}
|
|
parts := strings.Split(s, ".")
|
|
for _, p := range parts {
|
|
if p == "" {
|
|
return nil, false
|
|
}
|
|
for i := 0; i < len(p); i++ {
|
|
c := p[i]
|
|
alnum := c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '-'
|
|
if !alnum {
|
|
return nil, false
|
|
}
|
|
}
|
|
if !isBuild && isNumericIdent(p) && len(p) > 1 && p[0] == '0' {
|
|
return nil, false
|
|
}
|
|
}
|
|
return parts, true
|
|
}
|
|
|
|
// identValue reads a numeric identifier, tolerating the leading zeros build
|
|
// metadata is allowed to carry.
|
|
func identValue(s string) uint64 {
|
|
n, err := strconv.ParseUint(strings.TrimLeft(s, "0"), 10, 64)
|
|
if err != nil {
|
|
return 0 // all zeros, or wider than u64
|
|
}
|
|
return n
|
|
}
|
|
|
|
func isNumericIdent(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return len(s) > 0
|
|
}
|
|
|
|
// equal is structural equality, build metadata included.
|
|
func (v semver) equal(o semver) bool {
|
|
return v.major == o.major && v.minor == o.minor && v.patch == o.patch &&
|
|
identsEqual(v.pre, o.pre) && identsEqual(v.build, o.build)
|
|
}
|
|
|
|
func identsEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (v semver) compare(o semver) int {
|
|
if c := cmpUint(v.major, o.major); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmpUint(v.minor, o.minor); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmpUint(v.patch, o.patch); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmpPre(v.pre, o.pre); c != 0 {
|
|
return c
|
|
}
|
|
return cmpIdents(v.build, o.build)
|
|
}
|
|
|
|
func cmpUint(a, b uint64) int {
|
|
switch {
|
|
case a < b:
|
|
return -1
|
|
case a > b:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cmpPre orders prereleases, where "absent" ranks above any prerelease.
|
|
func cmpPre(a, b []string) int {
|
|
switch {
|
|
case len(a) == 0 && len(b) == 0:
|
|
return 0
|
|
case len(a) == 0:
|
|
return 1
|
|
case len(b) == 0:
|
|
return -1
|
|
}
|
|
return cmpIdents(a, b)
|
|
}
|
|
|
|
// cmpIdents compares identifier lists: numeric before alphanumeric, numerics by
|
|
// value, alphanumerics bytewise, and a shorter list before its own prefix.
|
|
func cmpIdents(a, b []string) int {
|
|
for i := 0; i < len(a) && i < len(b); i++ {
|
|
an, bn := isNumericIdent(a[i]), isNumericIdent(b[i])
|
|
switch {
|
|
case an && bn:
|
|
if c := cmpUint(identValue(a[i]), identValue(b[i])); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmpUint(uint64(len(a[i])), uint64(len(b[i]))); c != 0 {
|
|
return c // "090" ranks above "90" once the values tie
|
|
}
|
|
case an:
|
|
return -1
|
|
case bn:
|
|
return 1
|
|
default:
|
|
if c := strings.Compare(a[i], b[i]); c != 0 {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return cmpUint(uint64(len(a)), uint64(len(b)))
|
|
}
|
|
|
|
// ── requirements ────────────────────────────────────────────────────────────
|
|
|
|
type op uint8
|
|
|
|
const (
|
|
opCaret op = iota
|
|
opTilde
|
|
opExactReq
|
|
opWildcard
|
|
)
|
|
|
|
type comparator struct {
|
|
o op
|
|
major uint64
|
|
minor, patch *uint64
|
|
pre []string
|
|
}
|
|
|
|
type versionReq struct{ comparators []comparator }
|
|
|
|
// parseVersionReq reads the single-comparator forms the evaluator builds:
|
|
// "^X.Y.Z", "~X.Y.Z", "X.Y.x" and "x".
|
|
func parseVersionReq(s string) (versionReq, bool) {
|
|
s = strings.TrimSpace(s)
|
|
c := comparator{o: opCaret}
|
|
explicit := false
|
|
switch {
|
|
case strings.HasPrefix(s, "^"):
|
|
c.o, s, explicit = opCaret, s[1:], true
|
|
case strings.HasPrefix(s, "~"):
|
|
c.o, s, explicit = opTilde, s[1:], true
|
|
case strings.HasPrefix(s, "="):
|
|
c.o, s, explicit = opExactReq, s[1:], true
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return versionReq{}, false
|
|
}
|
|
|
|
if i := strings.IndexByte(s, '+'); i >= 0 {
|
|
if _, ok := parseIdents(s[i+1:], true); !ok {
|
|
return versionReq{}, false
|
|
}
|
|
s = s[:i]
|
|
}
|
|
if i := strings.IndexByte(s, '-'); i >= 0 {
|
|
p, ok := parseIdents(s[i+1:], false)
|
|
if !ok {
|
|
return versionReq{}, false
|
|
}
|
|
c.pre = p
|
|
s = s[:i]
|
|
}
|
|
|
|
parts := strings.Split(s, ".")
|
|
if len(parts) > 3 {
|
|
return versionReq{}, false
|
|
}
|
|
// A wildcard major means "any version at all" — no comparators to satisfy.
|
|
if isWildcard(parts[0]) {
|
|
return versionReq{}, len(parts) == 1 || allWildcard(parts[1:])
|
|
}
|
|
var ok bool
|
|
if c.major, ok = parseNumeric(parts[0]); !ok {
|
|
return versionReq{}, false
|
|
}
|
|
for i, p := range parts[1:] {
|
|
if isWildcard(p) {
|
|
if !explicit {
|
|
c.o = opWildcard
|
|
}
|
|
if !allWildcard(parts[i+2:]) {
|
|
return versionReq{}, false
|
|
}
|
|
break
|
|
}
|
|
n, ok := parseNumeric(p)
|
|
if !ok {
|
|
return versionReq{}, false
|
|
}
|
|
if i == 0 {
|
|
c.minor = &n
|
|
} else {
|
|
c.patch = &n
|
|
}
|
|
}
|
|
if len(parts) == 1 && !explicit {
|
|
c.o = opWildcard // a bare major behaves as "X.*"
|
|
}
|
|
return versionReq{comparators: []comparator{c}}, true
|
|
}
|
|
|
|
func isWildcard(s string) bool { return s == "x" || s == "X" || s == "*" }
|
|
|
|
func allWildcard(parts []string) bool {
|
|
for _, p := range parts {
|
|
if !isWildcard(p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// matches applies every comparator, then the prerelease rule: a prerelease
|
|
// version needs a comparator that carries a prerelease on the same triple.
|
|
func (r versionReq) matches(v semver) bool {
|
|
for _, c := range r.comparators {
|
|
if !c.matches(v) {
|
|
return false
|
|
}
|
|
}
|
|
if len(v.pre) == 0 {
|
|
return true
|
|
}
|
|
for _, c := range r.comparators {
|
|
if c.major == v.major && c.minor != nil && *c.minor == v.minor &&
|
|
c.patch != nil && *c.patch == v.patch && len(c.pre) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c comparator) matches(v semver) bool {
|
|
switch c.o {
|
|
case opTilde:
|
|
return c.matchesTilde(v)
|
|
case opCaret:
|
|
return c.matchesCaret(v)
|
|
default: // exact and wildcard share the same test
|
|
return c.matchesExact(v)
|
|
}
|
|
}
|
|
|
|
func (c comparator) matchesExact(v semver) bool {
|
|
if v.major != c.major {
|
|
return false
|
|
}
|
|
if c.minor != nil && v.minor != *c.minor {
|
|
return false
|
|
}
|
|
if c.patch != nil && v.patch != *c.patch {
|
|
return false
|
|
}
|
|
return cmpPre(v.pre, c.pre) == 0
|
|
}
|
|
|
|
func (c comparator) matchesTilde(v semver) bool {
|
|
if v.major != c.major {
|
|
return false
|
|
}
|
|
if c.minor != nil && v.minor != *c.minor {
|
|
return false
|
|
}
|
|
if c.patch != nil && v.patch != *c.patch {
|
|
return v.patch > *c.patch
|
|
}
|
|
return cmpPre(v.pre, c.pre) >= 0
|
|
}
|
|
|
|
func (c comparator) matchesCaret(v semver) bool {
|
|
if v.major != c.major {
|
|
return false
|
|
}
|
|
if c.minor == nil {
|
|
return true
|
|
}
|
|
minor := *c.minor
|
|
if c.patch == nil {
|
|
if c.major > 0 {
|
|
return v.minor >= minor
|
|
}
|
|
return v.minor == minor
|
|
}
|
|
patch := *c.patch
|
|
switch {
|
|
case c.major > 0:
|
|
if v.minor != minor {
|
|
return v.minor > minor
|
|
}
|
|
if v.patch != patch {
|
|
return v.patch > patch
|
|
}
|
|
case minor > 0:
|
|
if v.minor != minor {
|
|
return false
|
|
}
|
|
if v.patch != patch {
|
|
return v.patch > patch
|
|
}
|
|
default:
|
|
if v.minor != minor || v.patch != patch {
|
|
return false
|
|
}
|
|
}
|
|
return cmpPre(v.pre, c.pre) >= 0
|
|
}
|