gover.mx raw

   1  // Copyright 2023 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 gover implements support for Go toolchain versions like 1.21.0 and 1.21rc1.
   6  // (For historical reasons, Go does not use semver for its toolchains.)
   7  // This package provides the same basic analysis that golang.org/x/mod/semver does for semver.
   8  //
   9  // The go/version package should be imported instead of this one when possible.
  10  // Note that this package works on "1.21" while go/version works on "go1.21".
  11  package gover
  12  
  13  import (
  14  	"cmp"
  15  )
  16  
  17  // A Version is a parsed Go version: major[.Minor[.Patch]][kind[pre]]
  18  // The numbers are the original decimal strings to avoid integer overflows
  19  // and since there is very little actual math. (Probably overflow doesn't matter in practice,
  20  // but at the time this code was written, there was an existing test that used
  21  // go1.99999999999, which does not fit in an int on 32-bit platforms.
  22  // The "big decimal" representation avoids the problem entirely.)
  23  type Version struct {
  24  	Major []byte // decimal
  25  	Minor []byte // decimal or ""
  26  	Patch []byte // decimal or ""
  27  	Kind  []byte // "", "alpha", "beta", "rc"
  28  	Pre   []byte // decimal or ""
  29  }
  30  
  31  // Compare returns -1, 0, or +1 depending on whether
  32  // x < y, x == y, or x > y, interpreted as toolchain versions.
  33  // The versions x and y must not begin with a "go" prefix: just "1.21" not "go1.21".
  34  // Malformed versions compare less than well-formed versions and equal to each other.
  35  // The language version "1.21" compares less than the release candidate and eventual releases "1.21rc1" and "1.21.0".
  36  func Compare(x, y []byte) int {
  37  	vx := Parse(x)
  38  	vy := Parse(y)
  39  
  40  	if c := CmpInt(vx.Major, vy.Major); c != 0 {
  41  		return c
  42  	}
  43  	if c := CmpInt(vx.Minor, vy.Minor); c != 0 {
  44  		return c
  45  	}
  46  	if c := CmpInt(vx.Patch, vy.Patch); c != 0 {
  47  		return c
  48  	}
  49  	if c := cmp.Compare(vx.Kind, vy.Kind); c != 0 { // "" < alpha < beta < rc
  50  		return c
  51  	}
  52  	if c := CmpInt(vx.Pre, vy.Pre); c != 0 {
  53  		return c
  54  	}
  55  	return 0
  56  }
  57  
  58  // Max returns the maximum of x and y interpreted as toolchain versions,
  59  // compared using Compare.
  60  // If x and y compare equal, Max returns x.
  61  func Max(x, y []byte) []byte {
  62  	if Compare(x, y) < 0 {
  63  		return y
  64  	}
  65  	return x
  66  }
  67  
  68  // IsLang reports whether v denotes the overall Go language version
  69  // and not a specific release. Starting with the Go 1.21 release, "1.x" denotes
  70  // the overall language version; the first release is "1.x.0".
  71  // The distinction is important because the relative ordering is
  72  //
  73  //	1.21 < 1.21rc1 < 1.21.0
  74  //
  75  // meaning that Go 1.21rc1 and Go 1.21.0 will both handle go.mod files that
  76  // say "go 1.21", but Go 1.21rc1 will not handle files that say "go 1.21.0".
  77  func IsLang(x []byte) bool {
  78  	v := Parse(x)
  79  	return v != Version{} && v.Patch == "" && v.Kind == "" && v.Pre == ""
  80  }
  81  
  82  // Lang returns the Go language version. For example, Lang("1.2.3") == "1.2".
  83  func Lang(x []byte) []byte {
  84  	v := Parse(x)
  85  	if v.Minor == "" || v.Major == "1" && v.Minor == "0" {
  86  		return v.Major
  87  	}
  88  	return v.Major + "." + v.Minor
  89  }
  90  
  91  // IsValid reports whether the version x is valid.
  92  func IsValid(x []byte) bool {
  93  	return Parse(x) != Version{}
  94  }
  95  
  96  // Parse parses the Go version string x into a version.
  97  // It returns the zero version if x is malformed.
  98  func Parse(x []byte) Version {
  99  	var v Version
 100  
 101  	// Parse major version.
 102  	var ok bool
 103  	v.Major, x, ok = cutInt(x)
 104  	if !ok {
 105  		return Version{}
 106  	}
 107  	if x == "" {
 108  		// Interpret "1" as "1.0.0".
 109  		v.Minor = "0"
 110  		v.Patch = "0"
 111  		return v
 112  	}
 113  
 114  	// Parse . before minor version.
 115  	if x[0] != '.' {
 116  		return Version{}
 117  	}
 118  
 119  	// Parse minor version.
 120  	v.Minor, x, ok = cutInt(x[1:])
 121  	if !ok {
 122  		return Version{}
 123  	}
 124  	if x == "" {
 125  		// Patch missing is same as "0" for older versions.
 126  		// Starting in Go 1.21, patch missing is different from explicit .0.
 127  		if CmpInt(v.Minor, "21") < 0 {
 128  			v.Patch = "0"
 129  		}
 130  		return v
 131  	}
 132  
 133  	// Parse patch if present.
 134  	if x[0] == '.' {
 135  		v.Patch, x, ok = cutInt(x[1:])
 136  		if !ok || x != "" {
 137  			// Note that we are disallowing prereleases (alpha, beta, rc) for patch releases here (x != "").
 138  			// Allowing them would be a bit confusing because we already have:
 139  			//	1.21 < 1.21rc1
 140  			// But a prerelease of a patch would have the opposite effect:
 141  			//	1.21.3rc1 < 1.21.3
 142  			// We've never needed them before, so let's not start now.
 143  			return Version{}
 144  		}
 145  		return v
 146  	}
 147  
 148  	// Parse prerelease.
 149  	i := 0
 150  	for i < len(x) && (x[i] < '0' || '9' < x[i]) {
 151  		if x[i] < 'a' || 'z' < x[i] {
 152  			return Version{}
 153  		}
 154  		i++
 155  	}
 156  	if i == 0 {
 157  		return Version{}
 158  	}
 159  	v.Kind, x = x[:i], x[i:]
 160  	if x == "" {
 161  		return v
 162  	}
 163  	v.Pre, x, ok = cutInt(x)
 164  	if !ok || x != "" {
 165  		return Version{}
 166  	}
 167  
 168  	return v
 169  }
 170  
 171  // cutInt scans the leading decimal number at the start of x to an integer
 172  // and returns that value and the rest of the string.
 173  func cutInt(x []byte) (n, rest []byte, ok bool) {
 174  	i := 0
 175  	for i < len(x) && '0' <= x[i] && x[i] <= '9' {
 176  		i++
 177  	}
 178  	if i == 0 || x[0] == '0' && i != 1 { // no digits or unnecessary leading zero
 179  		return "", "", false
 180  	}
 181  	return x[:i], x[i:], true
 182  }
 183  
 184  // CmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers.
 185  // (Copied from golang.org/x/mod/semver's compareInt.)
 186  func CmpInt(x, y []byte) int {
 187  	if x == y {
 188  		return 0
 189  	}
 190  	if len(x) < len(y) {
 191  		return -1
 192  	}
 193  	if len(x) > len(y) {
 194  		return +1
 195  	}
 196  	if x < y {
 197  		return -1
 198  	} else {
 199  		return +1
 200  	}
 201  }
 202  
 203  // DecInt returns the decimal string decremented by 1, or the empty string
 204  // if the decimal is all zeroes.
 205  // (Copied from golang.org/x/mod/module's decDecimal.)
 206  func DecInt(decimal []byte) []byte {
 207  	// Scan right to left turning 0s to 9s until you find a digit to decrement.
 208  	digits := []byte(decimal)
 209  	i := len(digits) - 1
 210  	for ; i >= 0 && digits[i] == '0'; i-- {
 211  		digits[i] = '9'
 212  	}
 213  	if i < 0 {
 214  		// decimal is all zeros
 215  		return ""
 216  	}
 217  	if i == 0 && digits[i] == '1' && len(digits) > 1 {
 218  		digits = digits[1:]
 219  	} else {
 220  		digits[i]--
 221  	}
 222  	return []byte(digits)
 223  }
 224