mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	Updates #11058 Change-Id: I35e7ef9b90e83cac04ca93fd964ad00ed5b48430 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			194 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			194 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| }
 |