Files
tailscale/util/cmpver/version_test.go
T
Raphael Fakhri b0e35bf5b5 util/cmpver: don't panic on numbers that overflow a uint64
Compare parsed each numeric field with strconv.ParseUint and panicked
if that failed, so a version with a field of 20 or more digits, such as
a long build stamp, crashed the caller.

Compare digit runs by length after trimming leading zeros, then
lexically. This gives the same results as before for numbers that fit
in a uint64 and works for numbers of any length.

Fixes #21532

Signed-off-by: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com>
2026-09-30 21:41:37 -07:00

222 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package cmpver_test
import (
"testing"
"tailscale.com/util/cmpver"
)
func TestCompare(t *testing.T) {
tests := []struct {
name string
v1, v2 string
want int
}{
{
name: "both-empty",
want: 0,
},
{
name: "v1-empty",
v2: "1.2.3",
want: -1,
},
{
name: "v2-empty",
v1: "1.2.3",
want: 1,
},
{
name: "semver-major",
v1: "2.0.0",
v2: "1.9.9",
want: 1,
},
{
name: "semver-major",
v1: "2.0.0",
v2: "1.9.9",
want: 1,
},
{
name: "semver-minor",
v1: "1.9.0",
v2: "1.8.9",
want: 1,
},
{
name: "semver-patch",
v1: "1.9.9",
v2: "1.9.8",
want: 1,
},
{
name: "semver-equal",
v1: "1.9.8",
v2: "1.9.8",
want: 0,
},
{
name: "tailscale-major",
v1: "1.0-0",
v2: "0.97-105",
want: 1,
},
{
name: "tailscale-minor",
v1: "0.98-0",
v2: "0.97-105",
want: 1,
},
{
name: "tailscale-patch",
v1: "0.97-120",
v2: "0.97-105",
want: 1,
},
{
name: "tailscale-equal",
v1: "0.97-105",
v2: "0.97-105",
want: 0,
},
{
name: "tailscale-weird-extra-field",
v1: "0.96.1-0", // more fields == larger
v2: "0.96-105",
want: 1,
},
{
name: "tailscale-three-digits",
v1: "1.100.2",
v2: "1.55.0",
want: 1,
},
{
// Though ۱ and ۲ both satisfy unicode.IsNumber, our previous use
// of strconv.ParseUint with these characters would have lead us to
// panic. We're now only looking at ascii numbers, so test these are
// compared as text.
name: "only-ascii-numbers",
v1: "۱۱", // 2x EXTENDED ARABIC-INDIC DIGIT ONE
v2: "۲", // 1x EXTENDED ARABIC-INDIC DIGIT TWO
want: -1,
},
{
// Numbers too large for a uint64 must compare by value
// instead of panicking.
name: "number-exceeds-uint64",
v1: "1.18446744073709551616",
v2: "1.18446744073709551615",
want: 1,
},
{
name: "very-long-numbers",
v1: "99999999999999999999999999999999",
v2: "100000000000000000000000000000000",
want: -1,
},
{
name: "leading-zeros",
v1: "1.0007.00",
v2: "1.7.0",
want: 0,
},
// A few specific OS version tests below.
{
name: "windows-version",
v1: "10.0.19045.3324",
v2: "10.0.18362",
want: 1,
},
{
name: "windows-11-above-10_0_22000",
v1: "10.0.22631.2262",
v2: "10.0.22000",
want: 1,
},
{
name: "android-short-version",
v1: "10",
v2: "7",
want: 1,
},
{
name: "android-longer-version",
v1: "7.1.2",
v2: "7",
want: 1,
},
{
name: "iOS-version",
v1: "15.6.1",
v2: "15.6",
want: 1,
},
{
name: "linux-short-kernel-version",
v1: "4.4.302+",
v2: "4.0",
want: 1,
},
{
name: "linux-long-kernel-version",
v1: "4.14.255-311-248.529.amzn2.x86_64",
v2: "4.0",
want: 1,
},
{
name: "freebsd-version",
v1: "14.0-CURRENT",
v2: "14",
want: 1,
},
{
name: "synology-version",
v1: "Synology 6.2.4; kernel=3.10.105",
v2: "Synology 6",
want: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := cmpver.Compare(test.v1, test.v2)
if got != test.want {
t.Errorf("Compare(%q, %q) = %v, want %v", test.v1, test.v2, got, test.want)
}
// Reversing the comparison should reverse the outcome.
got2 := cmpver.Compare(test.v2, test.v1)
if got2 != -test.want {
t.Errorf("Compare(%q, %q) = %v, want %v", test.v2, test.v1, got2, -test.want)
}
if got, want := cmpver.Less(test.v1, test.v2), test.want < 0; got != want {
t.Errorf("Less(%q, %q) = %v, want %v", test.v1, test.v2, got, want)
}
if got, want := cmpver.Less(test.v2, test.v1), test.want > 0; got != want {
t.Errorf("Less(%q, %q) = %v, want %v", test.v2, test.v1, got, want)
}
if got, want := cmpver.LessEq(test.v1, test.v2), test.want <= 0; got != want {
t.Errorf("LessEq(%q, %q) = %v, want %v", test.v1, test.v2, got, want)
}
if got, want := cmpver.LessEq(test.v2, test.v1), test.want >= 0; got != want {
t.Errorf("LessEq(%q, %q) = %v, want %v", test.v2, test.v1, got, want)
}
// Check that version comparison does not allocate.
if n := testing.AllocsPerRun(100, func() { cmpver.Compare(test.v1, test.v2) }); n > 0 {
t.Errorf("Compare(%q, %q) got %v allocs per run", test.v1, test.v2, n)
}
})
}
}