cmp.go 4.1 KB

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