| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package version
- import (
- "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, ok := atoi(version[5:])
- if !ok {
- return parsed{}, false
- }
- return parsed{Datestamp: stamp}, true
- }
- var ret parsed
- major, rest, ok := splitNumericPrefix(version)
- if !ok {
- return parsed{}, false
- }
- ret.Major = major
- if len(rest) == 0 {
- return ret, true
- }
- ret.Minor, rest, ok = splitNumericPrefix(rest[1:])
- if !ok {
- 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, ok = splitNumericPrefix(rest[1:])
- if !ok {
- 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, ok = splitNumericPrefix(rest[1:])
- if !ok || (len(trailer) > 0 && trailer[0] != '-') {
- // rest was probably the string trailer, ignore it.
- ret.ExtraCommits = 0
- }
- return ret, true
- }
- func splitNumericPrefix(s string) (n int, rest string, ok bool) {
- for i, r := range s {
- if r >= '0' && r <= '9' {
- continue
- }
- ret, ok := atoi(s[:i])
- if !ok {
- return 0, "", false
- }
- return ret, s[i:], true
- }
- ret, ok := atoi(s)
- if !ok {
- return 0, "", false
- }
- return ret, "", true
- }
- const (
- maxUint = ^uint(0)
- maxInt = int(maxUint >> 1)
- )
- // atoi parses an int from a string s.
- // The bool result reports whether s is a number
- // representable by a value of type int.
- //
- // From Go's runtime/string.go.
- func atoi(s string) (int, bool) {
- if s == "" {
- return 0, false
- }
- neg := false
- if s[0] == '-' {
- neg = true
- s = s[1:]
- }
- un := uint(0)
- for i := range len(s) {
- c := s[i]
- if c < '0' || c > '9' {
- return 0, false
- }
- if un > maxUint/10 {
- // overflow
- return 0, false
- }
- un *= 10
- un1 := un + uint(c) - '0'
- if un1 < un {
- // overflow
- return 0, false
- }
- un = un1
- }
- if !neg && un > uint(maxInt) {
- return 0, false
- }
- if neg && un > uint(maxInt)+1 {
- return 0, false
- }
- n := int(un)
- if neg {
- n = -n
- }
- return n, true
- }
|