mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
merge: merge branch 'feat/cordis-router-raw-routes' into main
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package extpoints
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestHandleRawPreservesTrailingSlash 验证 HandleRaw 能表达 /x 与 /x/ 两条不同路由,
|
||||
// 而 Handle 会归一化掉尾部斜杠(server 插件的 list 端点历史行为依赖这一点)。
|
||||
func TestHandleRawPreservesTrailingSlash(t *testing.T) {
|
||||
r := &RouterRegistry{}
|
||||
g := r.Group("/api/v1/nodes")
|
||||
@@ -19,9 +24,29 @@ func TestHandleRawPreservesTrailingSlash(t *testing.T) {
|
||||
t.Errorf("HandleRaw(\"/\") path = %q, want %q", slashed.Path, "/api/v1/nodes/")
|
||||
}
|
||||
if slashed.ID == slashless.ID {
|
||||
t.Error("HandleRaw must allocate its own route ID")
|
||||
t.Error("HandleRaw must allocate its own route ID so scoped teardown can unregister both")
|
||||
}
|
||||
if got := len(r.Routes()); got != 2 {
|
||||
t.Errorf("registry routes = %d, want 2", got)
|
||||
}
|
||||
if !r.UnregisterByID(slashed.ID) {
|
||||
t.Error("UnregisterByID(HandleRaw route) = false, want true")
|
||||
}
|
||||
if got := len(r.Routes()); got != 1 {
|
||||
t.Errorf("routes after unregister = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegistryHandleRawKeepsAbsolutePath 根注册表上 HandleRaw 只做绝对化处理。
|
||||
func TestRegistryHandleRawKeepsAbsolutePath(t *testing.T) {
|
||||
r := &RouterRegistry{}
|
||||
if got := r.HandleRaw("GET", "/health/").Path; got != "/health/" {
|
||||
t.Errorf("path = %q, want %q", got, "/health/")
|
||||
}
|
||||
if got := r.HandleRaw("POST", "submit").Path; got != "/submit" {
|
||||
t.Errorf("path = %q, want %q", got, "/submit")
|
||||
}
|
||||
if got := r.BasePath(); got != "" {
|
||||
t.Errorf("registry BasePath() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+28
-7
@@ -143,7 +143,10 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) error {
|
||||
|
||||
// Update memory tracker
|
||||
if elem, ok := c.items[key]; ok {
|
||||
item := elem.Value.(*cacheItem)
|
||||
item, ok := elem.Value.(*cacheItem)
|
||||
if !ok {
|
||||
return fmt.Errorf("cache: evict list entry for %q has invalid type %T", key, elem.Value)
|
||||
}
|
||||
c.currentSize += size - item.size
|
||||
item.size = size
|
||||
item.expiredAt = expiredAt
|
||||
@@ -174,7 +177,11 @@ func (c *Cache) Get(key string) ([]byte, error) {
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
|
||||
item := elem.Value.(*cacheItem)
|
||||
item, ok := elem.Value.(*cacheItem)
|
||||
if !ok {
|
||||
c.mu.RUnlock()
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) {
|
||||
c.mu.RUnlock()
|
||||
return c.getAndDeleteIfExpired(key)
|
||||
@@ -224,7 +231,11 @@ func (c *Cache) getAndDeleteIfExpired(key string) ([]byte, error) {
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
|
||||
item := elem.Value.(*cacheItem)
|
||||
item, ok := elem.Value.(*cacheItem)
|
||||
if !ok {
|
||||
_ = c.deleteUnlocked(key)
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) {
|
||||
_ = c.deleteUnlocked(key)
|
||||
return nil, ErrCacheMiss
|
||||
@@ -254,8 +265,9 @@ func (c *Cache) Delete(key string) error {
|
||||
|
||||
func (c *Cache) deleteUnlocked(key string) error {
|
||||
if elem, ok := c.items[key]; ok {
|
||||
item := elem.Value.(*cacheItem)
|
||||
c.currentSize -= item.size
|
||||
if item, ok := elem.Value.(*cacheItem); ok {
|
||||
c.currentSize -= item.size
|
||||
}
|
||||
c.evictList.Remove(elem)
|
||||
delete(c.items, key)
|
||||
}
|
||||
@@ -308,7 +320,11 @@ func (c *Cache) evict() {
|
||||
|
||||
for c.currentSize > c.maxSize && c.evictList.Len() > 0 {
|
||||
elem := c.evictList.Back()
|
||||
item := elem.Value.(*cacheItem)
|
||||
item, ok := elem.Value.(*cacheItem)
|
||||
if !ok {
|
||||
c.evictList.Remove(elem)
|
||||
continue
|
||||
}
|
||||
c.currentSize -= item.size
|
||||
c.evictList.Remove(elem)
|
||||
delete(c.items, item.key)
|
||||
@@ -400,7 +416,12 @@ func (c *Cache) cleanExpired() {
|
||||
|
||||
now := time.Now()
|
||||
for key, elem := range c.items {
|
||||
item := elem.Value.(*cacheItem)
|
||||
item, ok := elem.Value.(*cacheItem)
|
||||
if !ok {
|
||||
c.evictList.Remove(elem)
|
||||
delete(c.items, key)
|
||||
continue
|
||||
}
|
||||
if !item.expiredAt.IsZero() && now.After(item.expiredAt) {
|
||||
c.currentSize -= item.size
|
||||
c.evictList.Remove(elem)
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 这些用例锁住「LRU 链表节点被污染时不得 panic」的行为:一旦 items 与 evictList
|
||||
// 的不变量被破坏(例如后续改动误写节点),缓存必须降级为未命中/跳过,
|
||||
// 而不是在读、写、删除与淘汰路径上崩掉整个进程。
|
||||
|
||||
// corruptEntry 写入一个键后把其链表节点值换成非法类型,返回缓存。
|
||||
func corruptEntry(t *testing.T, key string) *Cache {
|
||||
t.Helper()
|
||||
|
||||
c := New(t.TempDir())
|
||||
require.NoError(t, c.Set(key, []byte("payload"), time.Minute))
|
||||
|
||||
elem, ok := c.items[key]
|
||||
require.True(t, ok, "entry must be tracked after Set")
|
||||
elem.Value = "not-a-cacheItem"
|
||||
return c
|
||||
}
|
||||
|
||||
func TestGetToleratesCorruptEvictEntry(t *testing.T) {
|
||||
c := corruptEntry(t, "k")
|
||||
|
||||
got, err := c.Get("k")
|
||||
require.ErrorIs(t, err, ErrCacheMiss)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestSetOverCorruptEvictEntryReportsError(t *testing.T) {
|
||||
c := corruptEntry(t, "k")
|
||||
|
||||
err := c.Set("k", []byte("second"), time.Minute)
|
||||
require.Error(t, err, "Set must report the corrupted tracker entry instead of panicking")
|
||||
require.Contains(t, err.Error(), "invalid type")
|
||||
}
|
||||
|
||||
func TestDeleteToleratesCorruptEvictEntry(t *testing.T) {
|
||||
c := corruptEntry(t, "k")
|
||||
|
||||
require.NotPanics(t, func() { _ = c.Delete("k") })
|
||||
require.NotContains(t, c.items, "k")
|
||||
}
|
||||
|
||||
func TestEvictToleratesCorruptEvictEntry(t *testing.T) {
|
||||
c := corruptEntry(t, "k")
|
||||
// 让任意写入都触发淘汰扫描:扫到被污染的节点必须跳过而非 panic。
|
||||
c.UpdatePolicy(0, 0, true)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
for i := range 4 {
|
||||
_ = c.Set(string(rune('a'+i)), []byte("x"), time.Minute)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package util provides shared formatting and string helper functions.
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
secondsPerYear = 31104000 // 360 days
|
||||
secondsPerMonth = 2592000 // 30 days
|
||||
secondsPerDay = 86400
|
||||
secondsPerHour = 3600
|
||||
secondsPerMinute = 60
|
||||
)
|
||||
|
||||
const (
|
||||
sizeKB = 1024
|
||||
sizeMB = sizeKB * 1024
|
||||
sizeGB = sizeMB * 1024
|
||||
)
|
||||
|
||||
// Bytes2Size converts a byte count to a human-readable string with unit (B, KB, MB, GB).
|
||||
func Bytes2Size(num int64) string {
|
||||
var numStr string
|
||||
unit := "B"
|
||||
switch {
|
||||
case num/int64(sizeGB) >= 1:
|
||||
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
|
||||
unit = "GB"
|
||||
case num/int64(sizeMB) >= 1:
|
||||
numStr = strconv.Itoa(int(float64(num) / float64(sizeMB)))
|
||||
unit = "MB"
|
||||
case num/int64(sizeKB) >= 1:
|
||||
numStr = strconv.Itoa(int(float64(num) / float64(sizeKB)))
|
||||
unit = "KB"
|
||||
default:
|
||||
numStr = strconv.FormatInt(num, 10)
|
||||
}
|
||||
return numStr + " " + unit
|
||||
}
|
||||
|
||||
// Seconds2Time converts a number of seconds to a human-readable Chinese duration string.
|
||||
func Seconds2Time(num int) (time string) {
|
||||
if num/secondsPerYear > 0 {
|
||||
time += strconv.Itoa(num/secondsPerYear) + " 年 "
|
||||
num %= secondsPerYear
|
||||
}
|
||||
if num/secondsPerMonth > 0 {
|
||||
time += strconv.Itoa(num/secondsPerMonth) + " 个月 "
|
||||
num %= secondsPerMonth
|
||||
}
|
||||
if num/secondsPerDay > 0 {
|
||||
time += strconv.Itoa(num/secondsPerDay) + " 天 "
|
||||
num %= secondsPerDay
|
||||
}
|
||||
if num/secondsPerHour > 0 {
|
||||
time += strconv.Itoa(num/secondsPerHour) + " 小时 "
|
||||
num %= secondsPerHour
|
||||
}
|
||||
if num/secondsPerMinute > 0 {
|
||||
time += strconv.Itoa(num/secondsPerMinute) + " 分钟 "
|
||||
num %= secondsPerMinute
|
||||
}
|
||||
time += strconv.Itoa(num) + " 秒"
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBytes2Size(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int64
|
||||
expected string
|
||||
}{
|
||||
{0, "0 B"},
|
||||
{500, "500 B"},
|
||||
{1023, "1023 B"},
|
||||
{1024, "1 KB"},
|
||||
{2048, "2 KB"},
|
||||
{1024 * 1024, "1 MB"},
|
||||
{1024 * 1024 * 1024, "1.00 GB"},
|
||||
{1024 * 1024 * 1024 * 2, "2.00 GB"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := Bytes2Size(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Bytes2Size(%d) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeconds2Time(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
expected string
|
||||
}{
|
||||
{0, "0 秒"},
|
||||
{30, "30 秒"},
|
||||
{60, "1 分钟 0 秒"},
|
||||
{125, "2 分钟 5 秒"},
|
||||
{3600, "1 小时 0 秒"},
|
||||
{86400, "1 天 0 秒"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := Seconds2Time(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Seconds2Time(%d) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
)
|
||||
|
||||
// GetIP returns the first private IPv4 address found on the local network interfaces.
|
||||
func GetIP() (ip string) {
|
||||
ips, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
slog.Error("get interface addresses failed", "error", err)
|
||||
return ip
|
||||
}
|
||||
|
||||
for _, a := range ips {
|
||||
if candidate, ok := privateIPv4FromAddr(a); ok {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func privateIPv4FromAddr(addr net.Addr) (string, bool) {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok || ipNet.IP.IsLoopback() || ipNet.IP.To4() == nil {
|
||||
return "", false
|
||||
}
|
||||
ip := ipNet.IP.String()
|
||||
if isPrivateIPv4(ip) {
|
||||
return ip, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isPrivateIPv4(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
return parsedIP.IsPrivate()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsPrivateIPv4(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
{"127.0.0.1", false}, // Loopback is not in RFC 1918 private range
|
||||
{"10.0.0.1", true},
|
||||
{"172.16.0.1", true},
|
||||
{"192.168.1.1", true},
|
||||
{"8.8.8.8", false},
|
||||
{"invalid-ip", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := isPrivateIPv4(tt.ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isPrivateIPv4(%q) = %v, expected %v", tt.ip, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIP(t *testing.T) {
|
||||
ip := GetIP()
|
||||
// GetIP should return empty if no private IPv4 address is configured, or a valid IP.
|
||||
// We just ensure it doesn't panic.
|
||||
t.Logf("GetIP returned: %q", ip)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Unique returns a new slice containing only the unique elements of the input slice,
|
||||
// preserving their original order.
|
||||
func Unique[T comparable](slice []T) []T {
|
||||
if slice == nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[T]struct{})
|
||||
result := make([]T, 0)
|
||||
for _, item := range slice {
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueAndCleanStringSlice trims spaces, removes empty elements, and returns only the unique elements
|
||||
// of the input string slice. It preserves order and returns nil if the resulting slice is empty.
|
||||
func UniqueAndCleanStringSlice(slice []string) []string {
|
||||
if slice == nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]string, 0)
|
||||
for _, item := range slice {
|
||||
trimmed := strings.TrimSpace(item)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IdentifiableTimeRecord represents a database record that has a unique ID and a primary timestamp field.
|
||||
type IdentifiableTimeRecord interface {
|
||||
GetID() uint
|
||||
GetTime() time.Time
|
||||
}
|
||||
|
||||
// SortAndLimitRecords sorts a slice of IdentifiableTimeRecord descendingly by their timestamp (and ID as a tie-breaker),
|
||||
// and limits the slice to the specified size if limit > 0.
|
||||
func SortAndLimitRecords[T IdentifiableTimeRecord](rows []T, limit int) []T {
|
||||
if len(rows) == 0 {
|
||||
return rows
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
ti := rows[i].GetTime()
|
||||
tj := rows[j].GetTime()
|
||||
if ti.Equal(tj) {
|
||||
return rows[i].GetID() > rows[j].GetID()
|
||||
}
|
||||
return ti.After(tj)
|
||||
})
|
||||
if limit > 0 && len(rows) > limit {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import "strings"
|
||||
|
||||
// TrimStringFields trims leading and trailing spaces from all provided string pointers.
|
||||
func TrimStringFields(fields ...*string) {
|
||||
for _, f := range fields {
|
||||
if f != nil {
|
||||
*f = strings.TrimSpace(*f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Interface2String converts a string, int, or float64 value to its string representation.
|
||||
func Interface2String(inter any) string {
|
||||
switch v := inter.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case float64:
|
||||
return fmt.Sprintf("%f", v)
|
||||
}
|
||||
return "Not Implemented"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const gitDescribeMinIdentifiers = 2
|
||||
|
||||
// VersionInfo holds the parsed components of a semantic version string.
|
||||
type VersionInfo struct {
|
||||
Valid bool
|
||||
IsDev bool
|
||||
Numbers []int
|
||||
Prerelease []string
|
||||
GitDescribeDistance int
|
||||
GitDescribeTail []string
|
||||
}
|
||||
|
||||
// ParseVersionInfo parses a version string into a structured VersionInfo.
|
||||
func ParseVersionInfo(version string) VersionInfo {
|
||||
normalized := strings.TrimSpace(strings.TrimPrefix(version, "v"))
|
||||
if normalized == "" || normalized == "dev" {
|
||||
return VersionInfo{IsDev: strings.EqualFold(normalized, "dev")}
|
||||
}
|
||||
base := normalized
|
||||
prerelease := ""
|
||||
if separator := strings.IndexRune(normalized, '-'); separator >= 0 {
|
||||
base = normalized[:separator]
|
||||
prerelease = normalized[separator+1:]
|
||||
}
|
||||
|
||||
segments := strings.Split(base, ".")
|
||||
parts := make([]int, 0, len(segments))
|
||||
for _, segment := range segments {
|
||||
segment = strings.TrimSpace(segment)
|
||||
if segment == "" {
|
||||
parts = append(parts, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
numeric := strings.Builder{}
|
||||
for _, r := range segment {
|
||||
if r < '0' || r > '9' {
|
||||
break
|
||||
}
|
||||
numeric.WriteRune(r)
|
||||
}
|
||||
if numeric.Len() == 0 {
|
||||
parts = append(parts, 0)
|
||||
continue
|
||||
}
|
||||
value, err := strconv.Atoi(numeric.String())
|
||||
if err != nil {
|
||||
return VersionInfo{}
|
||||
}
|
||||
parts = append(parts, value)
|
||||
}
|
||||
info := VersionInfo{Valid: len(parts) > 0, Numbers: parts}
|
||||
if prerelease != "" {
|
||||
identifiers := splitPrereleaseIdentifiers(prerelease)
|
||||
if distance, tail, ok := parseGitDescribeIdentifiers(identifiers); ok {
|
||||
info.GitDescribeDistance = distance
|
||||
info.GitDescribeTail = tail
|
||||
} else {
|
||||
info.Prerelease = identifiers
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func parseGitDescribeIdentifiers(identifiers []string) (int, []string, bool) {
|
||||
if len(identifiers) < gitDescribeMinIdentifiers {
|
||||
return 0, nil, false
|
||||
}
|
||||
distance, err := strconv.Atoi(strings.TrimSpace(identifiers[0]))
|
||||
if err != nil || distance <= 0 {
|
||||
return 0, nil, false
|
||||
}
|
||||
commitToken := strings.TrimSpace(identifiers[1])
|
||||
if commitToken == "" || !strings.HasPrefix(strings.ToLower(commitToken), "g") {
|
||||
return 0, nil, false
|
||||
}
|
||||
return distance, identifiers[1:], true
|
||||
}
|
||||
|
||||
func splitPrereleaseIdentifiers(value string) []string {
|
||||
parts := strings.FieldsFunc(strings.TrimSpace(value), func(r rune) bool {
|
||||
return r == '.' || r == '-'
|
||||
})
|
||||
filtered := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
filtered = append(filtered, part)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// CompareVersions compares two version strings.
|
||||
// Returns -1 if left < right, 1 if left > right, and 0 if they are equal.
|
||||
func CompareVersions(local, remote string) int {
|
||||
left := ParseVersionInfo(local)
|
||||
right := ParseVersionInfo(remote)
|
||||
if left.IsDev {
|
||||
if right.Valid {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if !left.Valid || !right.Valid {
|
||||
return 0
|
||||
}
|
||||
|
||||
if result := compareVersionNumbers(left, right); result != 0 {
|
||||
return result
|
||||
}
|
||||
if result := compareGitDescribeDistance(left, right); result != 0 {
|
||||
return result
|
||||
}
|
||||
if left.GitDescribeDistance > 0 || right.GitDescribeDistance > 0 {
|
||||
return compareGitDescribeTails(left, right)
|
||||
}
|
||||
return comparePrereleaseIdentifiers(left, right)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
|
||||
import "strconv"
|
||||
|
||||
func compareVersionNumbers(left, right VersionInfo) int {
|
||||
maxLen := max(len(right.Numbers), len(left.Numbers))
|
||||
for index := range maxLen {
|
||||
leftValue := 0
|
||||
rightValue := 0
|
||||
if index < len(left.Numbers) {
|
||||
leftValue = left.Numbers[index]
|
||||
}
|
||||
if index < len(right.Numbers) {
|
||||
rightValue = right.Numbers[index]
|
||||
}
|
||||
if leftValue < rightValue {
|
||||
return -1
|
||||
}
|
||||
if leftValue > rightValue {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func compareGitDescribeDistance(left, right VersionInfo) int {
|
||||
if left.GitDescribeDistance == right.GitDescribeDistance {
|
||||
return 0
|
||||
}
|
||||
if left.GitDescribeDistance < right.GitDescribeDistance {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func compareGitDescribeTails(left, right VersionInfo) int {
|
||||
maxLen := max(len(right.GitDescribeTail), len(left.GitDescribeTail))
|
||||
for index := range maxLen {
|
||||
if index >= len(left.GitDescribeTail) {
|
||||
return -1
|
||||
}
|
||||
if index >= len(right.GitDescribeTail) {
|
||||
return 1
|
||||
}
|
||||
if left.GitDescribeTail[index] < right.GitDescribeTail[index] {
|
||||
return -1
|
||||
}
|
||||
if left.GitDescribeTail[index] > right.GitDescribeTail[index] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func comparePrereleaseIdentifiers(left, right VersionInfo) int {
|
||||
if len(left.Prerelease) == 0 && len(right.Prerelease) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(left.Prerelease) == 0 {
|
||||
return 1
|
||||
}
|
||||
if len(right.Prerelease) == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
maxLen := max(len(right.Prerelease), len(left.Prerelease))
|
||||
for index := range maxLen {
|
||||
if index >= len(left.Prerelease) {
|
||||
return -1
|
||||
}
|
||||
if index >= len(right.Prerelease) {
|
||||
return 1
|
||||
}
|
||||
if result := comparePrereleasePart(left.Prerelease[index], right.Prerelease[index]); result != 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func comparePrereleasePart(leftPart, rightPart string) int {
|
||||
leftNumber, leftErr := strconv.Atoi(leftPart)
|
||||
rightNumber, rightErr := strconv.Atoi(rightPart)
|
||||
switch {
|
||||
case leftErr == nil && rightErr == nil:
|
||||
if leftNumber < rightNumber {
|
||||
return -1
|
||||
}
|
||||
if leftNumber > rightNumber {
|
||||
return 1
|
||||
}
|
||||
case leftErr == nil:
|
||||
return -1
|
||||
case rightErr == nil:
|
||||
return 1
|
||||
default:
|
||||
if leftPart < rightPart {
|
||||
return -1
|
||||
}
|
||||
if leftPart > rightPart {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user