-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
cmp.go
140 lines (126 loc) · 3.52 KB
/
cmp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package version
import (
"strconv"
"strings"
)
// AtLeast returns whether version is at least the specified minimum
// version.
//
// Version comparison in Tailscale is a little complex, because we
// switched "styles" a few times, and additionally have a completely
// separate track of version numbers for OSS-only builds.
//
// AtLeast acts conservatively, returning true only if it's certain
// that version is at least minimum. As a result, it can produce false
// negatives, for example when an OSS build supports a given feature,
// but AtLeast is called with an official release number as the
// minimum
//
// version and minimum can both be either an official Tailscale
// version numbers (major.minor.patch-extracommits-extrastring), or an
// OSS build datestamp (date.YYYYMMDD). For Tailscale version numbers,
// AtLeast also accepts a prefix of a full version, in which case all
// missing fields are assumed to be zero.
func AtLeast(version string, minimum string) bool {
v, ok := parse(version)
if !ok {
return false
}
m, ok := parse(minimum)
if !ok {
return false
}
switch {
case v.Datestamp != 0 && m.Datestamp == 0:
// OSS version vs. Tailscale version
return false
case v.Datestamp == 0 && m.Datestamp != 0:
// Tailscale version vs. OSS version
return false
case v.Datestamp != 0:
// OSS version vs. OSS version
return v.Datestamp >= m.Datestamp
case v.Major == m.Major && v.Minor == m.Minor && v.Patch == m.Patch && v.ExtraCommits == m.ExtraCommits:
// Exactly equal Tailscale versions
return true
case v.Major != m.Major:
return v.Major > m.Major
case v.Minor != m.Minor:
return v.Minor > m.Minor
case v.Patch != m.Patch:
return v.Patch > m.Patch
default:
return v.ExtraCommits > m.ExtraCommits
}
}
type parsed struct {
Major, Minor, Patch, ExtraCommits int // for Tailscale version e.g. e.g. "0.99.1-20"
Datestamp int // for OSS version e.g. "date.20200612"
}
func parse(version string) (parsed, bool) {
if strings.HasPrefix(version, "date.") {
stamp, err := strconv.Atoi(version[5:])
if err != nil {
return parsed{}, false
}
return parsed{Datestamp: stamp}, true
}
var ret parsed
major, rest, err := splitNumericPrefix(version)
if err != nil {
return parsed{}, false
}
ret.Major = major
if len(rest) == 0 {
return ret, true
}
ret.Minor, rest, err = splitNumericPrefix(rest[1:])
if err != nil {
return parsed{}, false
}
if len(rest) == 0 {
return ret, true
}
// Optional patch version, if the next separator is a dot.
if rest[0] == '.' {
ret.Patch, rest, err = splitNumericPrefix(rest[1:])
if err != nil {
return parsed{}, false
}
if len(rest) == 0 {
return ret, true
}
}
// Optional extraCommits, if the next bit can be completely
// consumed as an integer.
if rest[0] != '-' {
return parsed{}, false
}
var trailer string
ret.ExtraCommits, trailer, err = splitNumericPrefix(rest[1:])
if err != nil || (len(trailer) > 0 && trailer[0] != '-') {
// rest was probably the string trailer, ignore it.
ret.ExtraCommits = 0
}
return ret, true
}
func splitNumericPrefix(s string) (int, string, error) {
for i, r := range s {
if r >= '0' && r <= '9' {
continue
}
ret, err := strconv.Atoi(s[:i])
if err != nil {
return 0, "", err
}
return ret, s[i:], nil
}
ret, err := strconv.Atoi(s)
if err != nil {
return 0, "", err
}
return ret, "", nil
}