src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

semver.go (9260B)


      1 // Copyright 2018 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // Package semver implements comparison of semantic version strings.
      6 // In this package, semantic version strings must begin with a leading "v",
      7 // as in "v1.0.0".
      8 //
      9 // The general form of a semantic version string accepted by this package is
     10 //
     11 //	vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]]
     12 //
     13 // where square brackets indicate optional parts of the syntax;
     14 // MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros;
     15 // PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers
     16 // using only alphanumeric characters and hyphens; and
     17 // all-numeric PRERELEASE identifiers must not have leading zeros.
     18 //
     19 // This package follows Semantic Versioning 2.0.0 (see semver.org)
     20 // with two exceptions. First, it requires the "v" prefix. Second, it recognizes
     21 // vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes)
     22 // as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0.
     23 package semver
     24 
     25 import (
     26 	"slices"
     27 	"strings"
     28 )
     29 
     30 // parsed returns the parsed form of a semantic version string.
     31 type parsed struct {
     32 	major      string
     33 	minor      string
     34 	patch      string
     35 	short      string
     36 	prerelease string
     37 	build      string
     38 }
     39 
     40 // IsValid reports whether v is a valid semantic version string.
     41 func IsValid(v string) bool {
     42 	_, ok := parse(v)
     43 	return ok
     44 }
     45 
     46 // Canonical returns the canonical formatting of the semantic version v.
     47 // It fills in any missing .MINOR or .PATCH and discards build metadata.
     48 // Two semantic versions compare equal only if their canonical formatting
     49 // is an identical string.
     50 // The canonical invalid semantic version is the empty string.
     51 func Canonical(v string) string {
     52 	p, ok := parse(v)
     53 	if !ok {
     54 		return ""
     55 	}
     56 	if p.build != "" {
     57 		return v[:len(v)-len(p.build)]
     58 	}
     59 	if p.short != "" {
     60 		return v + p.short
     61 	}
     62 	return v
     63 }
     64 
     65 // Major returns the major version prefix of the semantic version v.
     66 // For example, Major("v2.1.0") == "v2".
     67 // If v is an invalid semantic version string, Major returns the empty string.
     68 func Major(v string) string {
     69 	pv, ok := parse(v)
     70 	if !ok {
     71 		return ""
     72 	}
     73 	return v[:1+len(pv.major)]
     74 }
     75 
     76 // MajorMinor returns the major.minor version prefix of the semantic version v.
     77 // For example, MajorMinor("v2.1.0") == "v2.1".
     78 // If v is an invalid semantic version string, MajorMinor returns the empty string.
     79 func MajorMinor(v string) string {
     80 	pv, ok := parse(v)
     81 	if !ok {
     82 		return ""
     83 	}
     84 	i := 1 + len(pv.major)
     85 	if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor {
     86 		return v[:j]
     87 	}
     88 	return v[:i] + "." + pv.minor
     89 }
     90 
     91 // Prerelease returns the prerelease suffix of the semantic version v.
     92 // For example, Prerelease("v2.1.0-pre+meta") == "-pre".
     93 // If v is an invalid semantic version string, Prerelease returns the empty string.
     94 func Prerelease(v string) string {
     95 	pv, ok := parse(v)
     96 	if !ok {
     97 		return ""
     98 	}
     99 	return pv.prerelease
    100 }
    101 
    102 // Build returns the build suffix of the semantic version v.
    103 // For example, Build("v2.1.0+meta") == "+meta".
    104 // If v is an invalid semantic version string, Build returns the empty string.
    105 func Build(v string) string {
    106 	pv, ok := parse(v)
    107 	if !ok {
    108 		return ""
    109 	}
    110 	return pv.build
    111 }
    112 
    113 // Compare returns an integer comparing two versions according to
    114 // semantic version precedence.
    115 // The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
    116 //
    117 // An invalid semantic version string is considered less than a valid one.
    118 // All invalid semantic version strings compare equal to each other.
    119 func Compare(v, w string) int {
    120 	pv, ok1 := parse(v)
    121 	pw, ok2 := parse(w)
    122 	if !ok1 && !ok2 {
    123 		return 0
    124 	}
    125 	if !ok1 {
    126 		return -1
    127 	}
    128 	if !ok2 {
    129 		return +1
    130 	}
    131 	if c := compareInt(pv.major, pw.major); c != 0 {
    132 		return c
    133 	}
    134 	if c := compareInt(pv.minor, pw.minor); c != 0 {
    135 		return c
    136 	}
    137 	if c := compareInt(pv.patch, pw.patch); c != 0 {
    138 		return c
    139 	}
    140 	return comparePrerelease(pv.prerelease, pw.prerelease)
    141 }
    142 
    143 // Max canonicalizes its arguments and then returns the version string
    144 // that compares greater.
    145 //
    146 // Deprecated: use [Compare] instead. In most cases, returning a canonicalized
    147 // version is not expected or desired.
    148 func Max(v, w string) string {
    149 	v = Canonical(v)
    150 	w = Canonical(w)
    151 	if Compare(v, w) > 0 {
    152 		return v
    153 	}
    154 	return w
    155 }
    156 
    157 // ByVersion implements [sort.Interface] for sorting semantic version strings.
    158 type ByVersion []string
    159 
    160 func (vs ByVersion) Len() int           { return len(vs) }
    161 func (vs ByVersion) Swap(i, j int)      { vs[i], vs[j] = vs[j], vs[i] }
    162 func (vs ByVersion) Less(i, j int) bool { return compareVersion(vs[i], vs[j]) < 0 }
    163 
    164 // Sort sorts a list of semantic version strings using [Compare] and falls back
    165 // to use [strings.Compare] if both versions are considered equal.
    166 func Sort(list []string) {
    167 	slices.SortFunc(list, compareVersion)
    168 }
    169 
    170 func compareVersion(a, b string) int {
    171 	cmp := Compare(a, b)
    172 	if cmp != 0 {
    173 		return cmp
    174 	}
    175 	return strings.Compare(a, b)
    176 }
    177 
    178 func parse(v string) (p parsed, ok bool) {
    179 	if v == "" || v[0] != 'v' {
    180 		return
    181 	}
    182 	p.major, v, ok = parseInt(v[1:])
    183 	if !ok {
    184 		return
    185 	}
    186 	if v == "" {
    187 		p.minor = "0"
    188 		p.patch = "0"
    189 		p.short = ".0.0"
    190 		return
    191 	}
    192 	if v[0] != '.' {
    193 		ok = false
    194 		return
    195 	}
    196 	p.minor, v, ok = parseInt(v[1:])
    197 	if !ok {
    198 		return
    199 	}
    200 	if v == "" {
    201 		p.patch = "0"
    202 		p.short = ".0"
    203 		return
    204 	}
    205 	if v[0] != '.' {
    206 		ok = false
    207 		return
    208 	}
    209 	p.patch, v, ok = parseInt(v[1:])
    210 	if !ok {
    211 		return
    212 	}
    213 	if len(v) > 0 && v[0] == '-' {
    214 		p.prerelease, v, ok = parsePrerelease(v)
    215 		if !ok {
    216 			return
    217 		}
    218 	}
    219 	if len(v) > 0 && v[0] == '+' {
    220 		p.build, v, ok = parseBuild(v)
    221 		if !ok {
    222 			return
    223 		}
    224 	}
    225 	if v != "" {
    226 		ok = false
    227 		return
    228 	}
    229 	ok = true
    230 	return
    231 }
    232 
    233 func parseInt(v string) (t, rest string, ok bool) {
    234 	if v == "" {
    235 		return
    236 	}
    237 	if v[0] < '0' || '9' < v[0] {
    238 		return
    239 	}
    240 	i := 1
    241 	for i < len(v) && '0' <= v[i] && v[i] <= '9' {
    242 		i++
    243 	}
    244 	if v[0] == '0' && i != 1 {
    245 		return
    246 	}
    247 	return v[:i], v[i:], true
    248 }
    249 
    250 func parsePrerelease(v string) (t, rest string, ok bool) {
    251 	// "A pre-release version MAY be denoted by appending a hyphen and
    252 	// a series of dot separated identifiers immediately following the patch version.
    253 	// Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
    254 	// Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes."
    255 	if v == "" || v[0] != '-' {
    256 		return
    257 	}
    258 	i := 1
    259 	start := 1
    260 	for i < len(v) && v[i] != '+' {
    261 		if !isIdentChar(v[i]) && v[i] != '.' {
    262 			return
    263 		}
    264 		if v[i] == '.' {
    265 			if start == i || isBadNum(v[start:i]) {
    266 				return
    267 			}
    268 			start = i + 1
    269 		}
    270 		i++
    271 	}
    272 	if start == i || isBadNum(v[start:i]) {
    273 		return
    274 	}
    275 	return v[:i], v[i:], true
    276 }
    277 
    278 func parseBuild(v string) (t, rest string, ok bool) {
    279 	if v == "" || v[0] != '+' {
    280 		return
    281 	}
    282 	i := 1
    283 	start := 1
    284 	for i < len(v) {
    285 		if !isIdentChar(v[i]) && v[i] != '.' {
    286 			return
    287 		}
    288 		if v[i] == '.' {
    289 			if start == i {
    290 				return
    291 			}
    292 			start = i + 1
    293 		}
    294 		i++
    295 	}
    296 	if start == i {
    297 		return
    298 	}
    299 	return v[:i], v[i:], true
    300 }
    301 
    302 func isIdentChar(c byte) bool {
    303 	return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-'
    304 }
    305 
    306 func isBadNum(v string) bool {
    307 	i := 0
    308 	for i < len(v) && '0' <= v[i] && v[i] <= '9' {
    309 		i++
    310 	}
    311 	return i == len(v) && i > 1 && v[0] == '0'
    312 }
    313 
    314 func isNum(v string) bool {
    315 	i := 0
    316 	for i < len(v) && '0' <= v[i] && v[i] <= '9' {
    317 		i++
    318 	}
    319 	return i == len(v)
    320 }
    321 
    322 func compareInt(x, y string) int {
    323 	if x == y {
    324 		return 0
    325 	}
    326 	if len(x) < len(y) {
    327 		return -1
    328 	}
    329 	if len(x) > len(y) {
    330 		return +1
    331 	}
    332 	if x < y {
    333 		return -1
    334 	} else {
    335 		return +1
    336 	}
    337 }
    338 
    339 func comparePrerelease(x, y string) int {
    340 	// "When major, minor, and patch are equal, a pre-release version has
    341 	// lower precedence than a normal version.
    342 	// Example: 1.0.0-alpha < 1.0.0.
    343 	// Precedence for two pre-release versions with the same major, minor,
    344 	// and patch version MUST be determined by comparing each dot separated
    345 	// identifier from left to right until a difference is found as follows:
    346 	// identifiers consisting of only digits are compared numerically and
    347 	// identifiers with letters or hyphens are compared lexically in ASCII
    348 	// sort order. Numeric identifiers always have lower precedence than
    349 	// non-numeric identifiers. A larger set of pre-release fields has a
    350 	// higher precedence than a smaller set, if all of the preceding
    351 	// identifiers are equal.
    352 	// Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
    353 	// 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0."
    354 	if x == y {
    355 		return 0
    356 	}
    357 	if x == "" {
    358 		return +1
    359 	}
    360 	if y == "" {
    361 		return -1
    362 	}
    363 	for x != "" && y != "" {
    364 		x = x[1:] // skip - or .
    365 		y = y[1:] // skip - or .
    366 		var dx, dy string
    367 		dx, x = nextIdent(x)
    368 		dy, y = nextIdent(y)
    369 		if dx != dy {
    370 			ix := isNum(dx)
    371 			iy := isNum(dy)
    372 			if ix != iy {
    373 				if ix {
    374 					return -1
    375 				} else {
    376 					return +1
    377 				}
    378 			}
    379 			if ix {
    380 				if len(dx) < len(dy) {
    381 					return -1
    382 				}
    383 				if len(dx) > len(dy) {
    384 					return +1
    385 				}
    386 			}
    387 			if dx < dy {
    388 				return -1
    389 			} else {
    390 				return +1
    391 			}
    392 		}
    393 	}
    394 	if x == "" {
    395 		return -1
    396 	} else {
    397 		return +1
    398 	}
    399 }
    400 
    401 func nextIdent(x string) (dx, rest string) {
    402 	i := 0
    403 	for i < len(x) && x[i] != '.' {
    404 		i++
    405 	}
    406 	return x[:i], x[i:]
    407 }