diff --git a/internal/util/version/version.go b/internal/util/version/version.go new file mode 100644 index 000000000..42f6f42f2 --- /dev/null +++ b/internal/util/version/version.go @@ -0,0 +1,47 @@ +// Package version compares 3x-ui release versions ("v3.8.0" or "3.8.0"). +package version + +import ( + "strconv" + "strings" +) + +// Compare returns -1, 0 or 1 as a is older than, equal to or newer than b, and +// false when either side is not a plain MAJOR.MINOR.PATCH version. +func Compare(a, b string) (int, bool) { + aParts, okA := parse(a) + bParts, okB := parse(b) + if !okA || !okB { + return 0, false + } + for i := range len(aParts) { + if aParts[i] > bParts[i] { + return 1, true + } + if aParts[i] < bParts[i] { + return -1, true + } + } + return 0, true +} + +// Normalize strips surrounding space and a leading "v" from a version tag. +func Normalize(v string) string { + return strings.TrimPrefix(strings.TrimSpace(v), "v") +} + +func parse(v string) ([3]int, bool) { + var result [3]int + parts := strings.Split(Normalize(v), ".") + if len(parts) != 3 { + return result, false + } + for i, part := range parts { + n, err := strconv.Atoi(part) + if err != nil { + return result, false + } + result[i] = n + } + return result, true +} diff --git a/internal/util/version/version_test.go b/internal/util/version/version_test.go new file mode 100644 index 000000000..2cc1e072e --- /dev/null +++ b/internal/util/version/version_test.go @@ -0,0 +1,12 @@ +package version + +import "testing" + +func TestCompareRejectsUnexpectedFormats(t *testing.T) { + if _, ok := Compare("latest", "2.9.3"); ok { + t.Fatal("expected non-semver latest tag to be rejected") + } + if _, ok := Compare("v2.9", "2.9.3"); ok { + t.Fatal("expected short version to be rejected") + } +} diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index 0e2a9fa2d..62ce7d10a 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -19,6 +19,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/logger" + "github.com/mhsanaei/3x-ui/v3/internal/util/version" "github.com/mhsanaei/3x-ui/v3/internal/web/global" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) @@ -522,50 +523,13 @@ func resolveUpdateFolders() (string, string) { } func isNewerVersion(latest string, current string) bool { - cmp, ok := compareVersionStrings(latest, current) + cmp, ok := version.Compare(latest, current) if !ok { - return normalizeVersionTag(latest) != normalizeVersionTag(current) + return version.Normalize(latest) != version.Normalize(current) } return cmp > 0 } -func compareVersionStrings(a string, b string) (int, bool) { - aParts, okA := parseVersionParts(a) - bParts, okB := parseVersionParts(b) - if !okA || !okB { - return 0, false - } - for i := range len(aParts) { - if aParts[i] > bParts[i] { - return 1, true - } - if aParts[i] < bParts[i] { - return -1, true - } - } - return 0, true -} - -func parseVersionParts(version string) ([3]int, bool) { - var result [3]int - parts := strings.Split(normalizeVersionTag(version), ".") - if len(parts) != 3 { - return result, false - } - for i, part := range parts { - n, err := strconv.Atoi(part) - if err != nil { - return result, false - } - result[i] = n - } - return result, true -} - -func normalizeVersionTag(version string) string { - return strings.TrimPrefix(strings.TrimSpace(version), "v") -} - func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" } diff --git a/internal/web/service/panel/panel_test.go b/internal/web/service/panel/panel_test.go index d6e5d79e7..967760640 100644 --- a/internal/web/service/panel/panel_test.go +++ b/internal/web/service/panel/panel_test.go @@ -33,15 +33,6 @@ func TestIsNewerVersion(t *testing.T) { } } -func TestCompareVersionStringsRejectsUnexpectedFormats(t *testing.T) { - if _, ok := compareVersionStrings("latest", "2.9.3"); ok { - t.Fatal("expected non-semver latest tag to be rejected") - } - if _, ok := compareVersionStrings("v2.9", "2.9.3"); ok { - t.Fatal("expected short version to be rejected") - } -} - func TestShellQuote(t *testing.T) { if got := shellQuote("/usr/bin/curl"); got != "'/usr/bin/curl'" { t.Fatalf("unexpected quote result: %s", got)