cmp.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // Copyright (c) 2020 Tailscale Inc & 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. package version
  5. import (
  6. "strings"
  7. )
  8. // AtLeast returns whether version is at least the specified minimum
  9. // version.
  10. //
  11. // Version comparison in Tailscale is a little complex, because we
  12. // switched "styles" a few times, and additionally have a completely
  13. // separate track of version numbers for OSS-only builds.
  14. //
  15. // AtLeast acts conservatively, returning true only if it's certain
  16. // that version is at least minimum. As a result, it can produce false
  17. // negatives, for example when an OSS build supports a given feature,
  18. // but AtLeast is called with an official release number as the
  19. // minimum
  20. //
  21. // version and minimum can both be either an official Tailscale
  22. // version numbers (major.minor.patch-extracommits-extrastring), or an
  23. // OSS build datestamp (date.YYYYMMDD). For Tailscale version numbers,
  24. // AtLeast also accepts a prefix of a full version, in which case all
  25. // missing fields are assumed to be zero.
  26. func AtLeast(version string, minimum string) bool {
  27. v, ok := parse(version)
  28. if !ok {
  29. return false
  30. }
  31. m, ok := parse(minimum)
  32. if !ok {
  33. return false
  34. }
  35. switch {
  36. case v.Datestamp != 0 && m.Datestamp == 0:
  37. // OSS version vs. Tailscale version
  38. return false
  39. case v.Datestamp == 0 && m.Datestamp != 0:
  40. // Tailscale version vs. OSS version
  41. return false
  42. case v.Datestamp != 0:
  43. // OSS version vs. OSS version
  44. return v.Datestamp >= m.Datestamp
  45. case v.Major == m.Major && v.Minor == m.Minor && v.Patch == m.Patch && v.ExtraCommits == m.ExtraCommits:
  46. // Exactly equal Tailscale versions
  47. return true
  48. case v.Major != m.Major:
  49. return v.Major > m.Major
  50. case v.Minor != m.Minor:
  51. return v.Minor > m.Minor
  52. case v.Patch != m.Patch:
  53. return v.Patch > m.Patch
  54. default:
  55. return v.ExtraCommits > m.ExtraCommits
  56. }
  57. }
  58. type parsed struct {
  59. Major, Minor, Patch, ExtraCommits int // for Tailscale version e.g. e.g. "0.99.1-20"
  60. Datestamp int // for OSS version e.g. "date.20200612"
  61. }
  62. func parse(version string) (parsed, bool) {
  63. if strings.HasPrefix(version, "date.") {
  64. stamp, ok := atoi(version[5:])
  65. if !ok {
  66. return parsed{}, false
  67. }
  68. return parsed{Datestamp: stamp}, true
  69. }
  70. var ret parsed
  71. major, rest, ok := splitNumericPrefix(version)
  72. if !ok {
  73. return parsed{}, false
  74. }
  75. ret.Major = major
  76. if len(rest) == 0 {
  77. return ret, true
  78. }
  79. ret.Minor, rest, ok = splitNumericPrefix(rest[1:])
  80. if !ok {
  81. return parsed{}, false
  82. }
  83. if len(rest) == 0 {
  84. return ret, true
  85. }
  86. // Optional patch version, if the next separator is a dot.
  87. if rest[0] == '.' {
  88. ret.Patch, rest, ok = splitNumericPrefix(rest[1:])
  89. if !ok {
  90. return parsed{}, false
  91. }
  92. if len(rest) == 0 {
  93. return ret, true
  94. }
  95. }
  96. // Optional extraCommits, if the next bit can be completely
  97. // consumed as an integer.
  98. if rest[0] != '-' {
  99. return parsed{}, false
  100. }
  101. var trailer string
  102. ret.ExtraCommits, trailer, ok = splitNumericPrefix(rest[1:])
  103. if !ok || (len(trailer) > 0 && trailer[0] != '-') {
  104. // rest was probably the string trailer, ignore it.
  105. ret.ExtraCommits = 0
  106. }
  107. return ret, true
  108. }
  109. func splitNumericPrefix(s string) (n int, rest string, ok bool) {
  110. for i, r := range s {
  111. if r >= '0' && r <= '9' {
  112. continue
  113. }
  114. ret, ok := atoi(s[:i])
  115. if !ok {
  116. return 0, "", false
  117. }
  118. return ret, s[i:], true
  119. }
  120. ret, ok := atoi(s)
  121. if !ok {
  122. return 0, "", false
  123. }
  124. return ret, "", true
  125. }
  126. const (
  127. maxUint = ^uint(0)
  128. maxInt = int(maxUint >> 1)
  129. )
  130. // atoi parses an int from a string s.
  131. // The bool result reports whether s is a number
  132. // representable by a value of type int.
  133. //
  134. // From Go's runtime/string.go.
  135. func atoi(s string) (int, bool) {
  136. if s == "" {
  137. return 0, false
  138. }
  139. neg := false
  140. if s[0] == '-' {
  141. neg = true
  142. s = s[1:]
  143. }
  144. un := uint(0)
  145. for i := 0; i < len(s); i++ {
  146. c := s[i]
  147. if c < '0' || c > '9' {
  148. return 0, false
  149. }
  150. if un > maxUint/10 {
  151. // overflow
  152. return 0, false
  153. }
  154. un *= 10
  155. un1 := un + uint(c) - '0'
  156. if un1 < un {
  157. // overflow
  158. return 0, false
  159. }
  160. un = un1
  161. }
  162. if !neg && un > uint(maxInt) {
  163. return 0, false
  164. }
  165. if neg && un > uint(maxInt)+1 {
  166. return 0, false
  167. }
  168. n := int(un)
  169. if neg {
  170. n = -n
  171. }
  172. return n, true
  173. }