后端与全仓代码质量清理(golangci 扩展集 · 测试质量 · 并发安全 · 文档同步)

代码质量全量清理,零行为变化:golangci 扩展集 13 类 linter(gosec/modernize/perfsprint/canonicalheader/usestdlibvars/wastedassign/intrange/errorlint/forcetypeassert/recvcheck/exhaustive/unparam)全量修复,测试代码质量(testifylint/thelper/usetesting)25→0,frpc 进程生命周期真 bug(进程组击杀)、全仓 go test -race 6 类数据竞争(含 1 个生产竞争)、SPDX license 头补齐 131 文件、前端测试套件 next-intl 迁移后 44 失败→全绿、过期 swagger 文档重新生成、pnpm-workspace 构建审批。

Experiments: #2-#17, #18, #20, #21, #23
Metric: total_issues 108 → 8 (-92.6%)
This commit is contained in:
ryan
2026-08-16 21:23:37 +08:00
parent 5a8722ff07
commit 2f60329886
292 changed files with 1362 additions and 793 deletions
+28 -7
View File
@@ -123,7 +123,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
@@ -154,7 +157,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)
@@ -204,7 +211,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
@@ -234,8 +245,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)
}
@@ -288,7 +300,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)
@@ -380,7 +396,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)
+2 -3
View File
@@ -6,6 +6,7 @@ package ram
import (
"context"
"errors"
"maps"
"sync"
"time"
)
@@ -119,9 +120,7 @@ func Set(item CacheItem) {
currentMap, ok := managerCache.GetIfPresent(item.Type)
newMap := make(map[string]cacheEntry)
if ok {
for k, v := range currentMap {
newMap[k] = v
}
maps.Copy(newMap, currentMap)
}
var expireAt time.Time
+3 -3
View File
@@ -215,7 +215,7 @@ func VerifyChallengeSolutions(token string, solutions []int, secret []byte, expe
}
tokenFnv := fnv1a(token)
for i := 0; i < payload.Count; i++ {
for i := range payload.Count {
idxStr := strconv.Itoa(i + 1)
saltSeed := fnv1aResume(tokenFnv, idxStr)
targetSeed := fnv1aResume(saltSeed, "d")
@@ -238,14 +238,14 @@ func VerifyChallengeSolutions(token string, solutions []int, secret []byte, expe
func Solve(token string, count, size, difficulty int) []int {
solutions := make([]int, count)
tokenFnv := fnv1a(token)
for i := 0; i < count; i++ {
for i := range count {
idxStr := strconv.Itoa(i + 1)
saltSeed := fnv1aResume(tokenFnv, idxStr)
targetSeed := fnv1aResume(saltSeed, "d")
salt := prngFromHash(saltSeed, size)
target := prngFromHash(targetSeed, difficulty)
for nonce := 0; nonce < 1000000; nonce++ {
for nonce := range 1000000 {
hashInput := salt + strconv.Itoa(nonce)
hashBytes := sha256.Sum256([]byte(hashInput))
hashHex := hex.EncodeToString(hashBytes[:])
+2 -2
View File
@@ -13,7 +13,7 @@ import (
//nolint:mnd // FNV-1a 算法位移常量
func fnv1a(str string) uint32 {
var hash uint32 = 2166136261
for i := 0; i < len(str); i++ {
for i := range len(str) {
hash ^= uint32(str[i])
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
}
@@ -25,7 +25,7 @@ func fnv1a(str string) uint32 {
//nolint:mnd // FNV-1a 算法位移常量
func fnv1aResume(state uint32, str string) uint32 {
h := state
for i := 0; i < len(str); i++ {
for i := range len(str) {
h ^= uint32(str[i])
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)
}
+13 -11
View File
@@ -5,6 +5,7 @@ package cap
import (
"context"
"errors"
"sync"
"time"
@@ -51,20 +52,21 @@ func NewMemoryStore(cleanupInterval time.Duration) *MemoryStore {
func (s *MemoryStore) Get(_ context.Context, key string) (string, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.getLocked(key)
val, ok := s.getLocked(key)
return val, ok, nil
}
// getLocked is the internal helper – caller must hold s.mu.
func (s *MemoryStore) getLocked(key string) (string, bool, error) {
func (s *MemoryStore) getLocked(key string) (string, bool) {
item, found := s.items[key]
if !found {
return "", false, nil
return "", false
}
if time.Now().After(item.expiresAt) {
delete(s.items, key)
return "", false, nil
return "", false
}
return item.value, true, nil
return item.value, true
}
// Set 向 MemoryStore 写入指定 key 的值
@@ -92,7 +94,7 @@ func (s *MemoryStore) SetNX(_ context.Context, key string, val string, ttl time.
s.mu.Lock()
defer s.mu.Unlock()
_, exists, _ := s.getLocked(key)
_, exists := s.getLocked(key)
if exists {
return false, nil
}
@@ -108,9 +110,9 @@ func (s *MemoryStore) GetAndDelete(_ context.Context, key string) (string, bool,
s.mu.Lock()
defer s.mu.Unlock()
val, exists, err := s.getLocked(key)
if err != nil || !exists {
return "", false, err
val, exists := s.getLocked(key)
if !exists {
return "", false, nil
}
delete(s.items, key)
return val, true, nil
@@ -149,7 +151,7 @@ func NewRedisStore(client redis.UniversalClient) *RedisStore {
// Get 从 RedisStore 获取指定 key 的值
func (s *RedisStore) Get(ctx context.Context, key string) (string, bool, error) {
val, err := s.client.Get(ctx, key).Result()
if err == redis.Nil {
if errors.Is(err, redis.Nil) {
return "", false, nil
}
if err != nil {
@@ -176,7 +178,7 @@ func (s *RedisStore) SetNX(ctx context.Context, key string, val string, ttl time
// GetAndDelete wraps Redis GETDEL (available since Redis 6.2).
func (s *RedisStore) GetAndDelete(ctx context.Context, key string) (string, bool, error) {
val, err := s.client.GetDel(ctx, key).Result()
if err == redis.Nil {
if errors.Is(err, redis.Nil) {
return "", false, nil
}
if err != nil {
+4 -1
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import "strings"
@@ -194,7 +197,7 @@ func CountryCentroidByName(name string) (lat float64, lon float64, ok bool) {
return v.lat, v.lon, true
}
// Try comma-separated parts (city / region / country / ISO).
for _, part := range strings.Split(name, ",") {
for part := range strings.SplitSeq(name, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import "testing"
+6 -3
View File
@@ -1,7 +1,10 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
"fmt"
"errors"
"net"
)
@@ -20,12 +23,12 @@ func (e *EmptyProvider) Initialize() error {
// GetGeoInfo reports that no GeoIP provider has been configured.
func (e *EmptyProvider) GetGeoInfo(_ net.IP) (*GeoInfo, error) {
return nil, fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider")
return nil, errors.New("you are using an empty GeoIP provider, please set a valid provider")
}
// UpdateDatabase reports that no GeoIP provider has been configured.
func (e *EmptyProvider) UpdateDatabase() error {
return fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider")
return errors.New("you are using an empty GeoIP provider, please set a valid provider")
}
// Close releases resources held by the empty provider.
+6 -2
View File
@@ -1,7 +1,11 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package geoip resolves geographic information for IP addresses.
package geoip
import (
"errors"
"fmt"
"log/slog"
"net"
@@ -138,7 +142,7 @@ func InitGeoIP(provider string) {
// GetGeoInfo looks up geographic information for ip using the active provider.
func GetGeoInfo(ip net.IP) (*GeoInfo, error) {
if ip == nil {
return nil, fmt.Errorf("IP address cannot be nil")
return nil, errors.New("IP address cannot be nil")
}
provider := getProvider()
cacheKey := provider.Name() + ":" + ip.String()
@@ -157,7 +161,7 @@ func GetGeoInfo(ip net.IP) (*GeoInfo, error) {
// LookupGeoInfoWithProvider looks up geographic information using a temporary provider.
func LookupGeoInfoWithProvider(providerName string, ip net.IP) (*GeoInfo, error) {
if ip == nil {
return nil, fmt.Errorf("IP address cannot be nil")
return nil, errors.New("IP address cannot be nil")
}
provider, err := providerFactory(normalizeProvider(providerName))
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package iputil provides helpers for parsing, normalizing, and scoring IP addresses.
package iputil
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package iputil
import (
+6 -2
View File
@@ -1,7 +1,11 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
"context"
"errors"
"fmt"
"io"
"net"
@@ -96,10 +100,10 @@ func (s *MaxMindGeoIPService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
defer s.mu.RUnlock()
if s.maxMindDBReader == nil {
return nil, fmt.Errorf("MaxMind database is not initialized or failed to open")
return nil, errors.New("MaxMind database is not initialized or failed to open")
}
if ip == nil {
return nil, fmt.Errorf("IP address cannot be nil")
return nil, errors.New("IP address cannot be nil")
}
var record Record
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package geoip
import (
+4 -4
View File
@@ -78,25 +78,25 @@ func Init(cfg Config) {
}
// DebugF 输出 Debug 级别日志
func DebugF(ctx context.Context, format string, args ...interface{}) {
func DebugF(ctx context.Context, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
logger.Ctx(ctx).Debug(msg, getTraceIDFields(ctx)...)
}
// InfoF 输出 Info 级别日志
func InfoF(ctx context.Context, format string, args ...interface{}) {
func InfoF(ctx context.Context, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
logger.Ctx(ctx).Info(msg, getTraceIDFields(ctx)...)
}
// WarnF 输出 Warn 级别日志
func WarnF(ctx context.Context, format string, args ...interface{}) {
func WarnF(ctx context.Context, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
logger.Ctx(ctx).Warn(msg, getTraceIDFields(ctx)...)
}
// ErrorF 输出 Error 级别日志
func ErrorF(ctx context.Context, format string, args ...interface{}) {
func ErrorF(ctx context.Context, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
logger.Ctx(ctx).Error(msg, getTraceIDFields(ctx)...)
}
+3 -6
View File
@@ -47,7 +47,7 @@ func (r *LogRingBuffer) Write(p []byte) (int, error) {
data := string(p)
start := 0
for i := 0; i < len(data); i++ {
for i := range len(data) {
if data[i] == '\n' {
line := data[start:i]
start = i + 1
@@ -114,7 +114,7 @@ func (r *LogRingBuffer) Query(cursor int, limit int) ([]LogEntry, bool) {
// 将 ring buffer 中的有效条目按顺序收集
ordered := make([]LogEntry, 0, r.count)
for i := 0; i < r.count; i++ {
for i := range r.count {
pos := (oldestPos + i) % r.cap
ordered = append(ordered, r.entries[pos])
}
@@ -141,10 +141,7 @@ func (r *LogRingBuffer) Query(cursor int, limit int) ([]LogEntry, bool) {
}
// 返回 cut 之前的最后 limit 条
start := cut - limit
if start < 0 {
start = 0
}
start := max(cut-limit, 0)
hasMore := start > 0
return ordered[start:cut], hasMore
+13 -12
View File
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogRingBuffer_WriteAndQuery(t *testing.T) {
@@ -18,7 +19,7 @@ func TestLogRingBuffer_WriteAndQuery(t *testing.T) {
entries, hasMore := rb.Query(0, 10)
assert.False(t, hasMore)
assert.Equal(t, 3, len(entries))
assert.Len(t, entries, 3)
assert.Equal(t, "line1", entries[0].Data)
assert.Equal(t, "line2", entries[1].Data)
assert.Equal(t, "line3", entries[2].Data)
@@ -34,7 +35,7 @@ func TestLogRingBuffer_CapacityOverflow(t *testing.T) {
entries, hasMore := rb.Query(0, 10)
assert.False(t, hasMore)
assert.Equal(t, 3, len(entries))
assert.Len(t, entries, 3)
assert.Equal(t, "c", entries[0].Data)
assert.Equal(t, "d", entries[1].Data)
assert.Equal(t, "e", entries[2].Data)
@@ -48,7 +49,7 @@ func TestLogRingBuffer_QueryLatest(t *testing.T) {
// Query latest 2
entries, hasMore := rb.Query(0, 2)
assert.True(t, hasMore)
assert.Equal(t, 2, len(entries))
assert.Len(t, entries, 2)
assert.Equal(t, "d", entries[0].Data)
assert.Equal(t, "e", entries[1].Data)
}
@@ -60,12 +61,12 @@ func TestLogRingBuffer_QueryByCursor(t *testing.T) {
// First get all to find indices
all, _ := rb.Query(0, 10)
assert.Equal(t, 5, len(all))
assert.Len(t, all, 5)
// Query entries before index 3
entries, hasMore := rb.Query(3, 10)
assert.False(t, hasMore)
assert.Equal(t, 3, len(entries))
assert.Len(t, entries, 3)
assert.Equal(t, "a", entries[0].Data)
assert.Equal(t, "b", entries[1].Data)
assert.Equal(t, "c", entries[2].Data)
@@ -79,7 +80,7 @@ func TestLogRingBuffer_QueryByCursorWithLimit(t *testing.T) {
// Query 2 entries before index 4
entries, hasMore := rb.Query(4, 2)
assert.True(t, hasMore)
assert.Equal(t, 2, len(entries))
assert.Len(t, entries, 2)
assert.Equal(t, "c", entries[0].Data)
assert.Equal(t, "d", entries[1].Data)
}
@@ -98,7 +99,7 @@ func TestLogRingBuffer_QueryNonExistentCursor(t *testing.T) {
entries, hasMore := rb.Query(999, 10)
assert.False(t, hasMore)
assert.Equal(t, 2, len(entries))
assert.Len(t, entries, 2)
assert.Equal(t, "a", entries[0].Data)
assert.Equal(t, "b", entries[1].Data)
}
@@ -138,7 +139,7 @@ func TestLogRingBuffer_WriteNoNewline(t *testing.T) {
_, _ = rb.Write([]byte("partial"))
entries, _ := rb.Query(0, 10)
assert.Equal(t, 1, len(entries))
assert.Len(t, entries, 1)
assert.Equal(t, "partial", entries[0].Data)
}
@@ -147,7 +148,7 @@ func TestLogRingBuffer_WriteEmpty(t *testing.T) {
n, err := rb.Write([]byte(""))
assert.Equal(t, 0, n)
assert.NoError(t, err)
require.NoError(t, err)
entries, _ := rb.Query(0, 10)
assert.Nil(t, entries)
@@ -160,7 +161,7 @@ func TestLogRingBuffer_QueryAfterOverflow(t *testing.T) {
entries, hasMore := rb.Query(0, 10)
assert.False(t, hasMore)
assert.Equal(t, 3, len(entries))
assert.Len(t, entries, 3)
assert.Equal(t, "5", entries[0].Data)
assert.Equal(t, "6", entries[1].Data)
assert.Equal(t, "7", entries[2].Data)
@@ -178,14 +179,14 @@ func TestLogRingBuffer_NextCursor(t *testing.T) {
// Query latest 2, should return next_cursor pointing to first returned entry
entries, _ := rb.Query(0, 2)
assert.Equal(t, 2, len(entries))
assert.Len(t, entries, 2)
// entries[0].Index = 3 ("d"), entries[1].Index = 4 ("e")
assert.Equal(t, 3, entries[0].Index)
// Now use that index as cursor to get older entries
older, hasMore := rb.Query(entries[0].Index, 10)
assert.False(t, hasMore)
assert.Equal(t, 3, len(older))
assert.Len(t, older, 3)
assert.Equal(t, "a", older[0].Data)
assert.Equal(t, "b", older[1].Data)
assert.Equal(t, "c", older[2].Data)
+10 -10
View File
@@ -55,21 +55,21 @@ func SendMailHTML(ctx context.Context, cfg Config, to string, subject, body stri
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/html; charset=UTF-8"
message := ""
var message strings.Builder
for k, v := range header {
message += fmt.Sprintf("%s: %s\r\n", k, v)
fmt.Fprintf(&message, "%s: %s\r\n", k, v)
}
message += "\r\n" + body
message.WriteString("\r\n" + body)
auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
// If using SSL port 465, we connection via TLS dial
if cfg.Port == smtpSSLPort {
return sendMailViaSSL(ctx, addr, auth, cfg, to, message)
return sendMailViaSSL(ctx, addr, auth, cfg, to, message.String())
}
// For standard port (587 / 25), use smtp.SendMail directly (handles STARTTLS automatically if server supports it)
err := smtp.SendMail(addr, auth, cfg.Username, []string{to}, []byte(message))
err := smtp.SendMail(addr, auth, cfg.Username, []string{to}, []byte(message.String()))
if err != nil {
return fmt.Errorf(errSendMailFailed, err)
}
@@ -127,7 +127,7 @@ func sendMailViaSSL(ctx context.Context, addr string, auth smtp.Auth, cfg Config
// SendMailWithLog sends a test email and records a detailed SMTP connection log
func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body string) (string, error) {
var logBuf bytes.Buffer
logLine := func(dir string, format string, args ...interface{}) {
logLine := func(dir string, format string, args ...any) {
fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...))
}
@@ -227,14 +227,14 @@ func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body s
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/html; charset=UTF-8"
message := ""
var message strings.Builder
for k, v := range header {
message += fmt.Sprintf("%s: %s\r\n", k, v)
fmt.Fprintf(&message, "%s: %s\r\n", k, v)
}
message += "\r\n" + body
message.WriteString("\r\n" + body)
logLine("System", "Sending message body...")
if _, err = w.Write([]byte(message)); err != nil {
if _, err = w.Write([]byte(message.String())); err != nil {
_ = w.Close()
logLine("Error", "Writing message body failed: %v", err)
return logBuf.String(), err
+2 -2
View File
@@ -72,14 +72,14 @@ func copyLimited(dst io.Writer, src io.Reader, maxBytes int64) (int64, error) {
return written, err
}
if written > maxBytes {
return written, fmt.Errorf("pages file size out of bounds")
return written, errors.New("pages file size out of bounds")
}
return written, nil
}
func copyAndVerifySize(dst io.Writer, src io.Reader, declaredSize uint64, maxBytes int64) (int64, error) {
if declaredSize > uint64(math.MaxInt64) {
return 0, fmt.Errorf("pages file size out of bounds")
return 0, errors.New("pages file size out of bounds")
}
written, err := copyLimited(dst, src, maxBytes)
if err != nil {
+4 -4
View File
@@ -124,7 +124,7 @@ func extractEntries(entries []Entry, destDir string, opts ExtractOptions) error
appendMeasuredFile(measured, normalizedPath, actual)
}
if measured.fileCount == 0 {
return fmt.Errorf("pages package is empty")
return errors.New("pages package is empty")
}
return nil
}
@@ -139,7 +139,7 @@ func extractTarFamilyAt(ra io.ReaderAt, size int64, format Format, destDir strin
return err
}
if firstPass.fileCount == 0 {
return fmt.Errorf("pages package is empty")
return errors.New("pages package is empty")
}
commonPrefix := ""
if opts.StripCommonRoot {
@@ -162,7 +162,7 @@ func extractTarFamilyAt(ra io.ReaderAt, size int64, format Format, destDir strin
return extractErr
}
if secondPass.fileCount != firstPass.fileCount || secondPass.totalSize != firstPass.totalSize {
return fmt.Errorf("pages tar package changed between validation and extraction")
return errors.New("pages tar package changed between validation and extraction")
}
return nil
}
@@ -177,7 +177,7 @@ func extractTarReader(
measured := &measuredArchive{files: make([]measuredFile, 0)}
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
+2 -2
View File
@@ -7,7 +7,7 @@ package pagesarchive
import (
"bytes"
"fmt"
"errors"
"path/filepath"
"strings"
)
@@ -87,7 +87,7 @@ func DetectFormat(fileName string, data []byte) (Format, error) {
if format, ok := DetectFormatFromBytes(data); ok {
return format, nil
}
return "", fmt.Errorf("unsupported pages package format")
return "", errors.New("unsupported pages package format")
}
// Extension returns the canonical file extension for a format (without leading dot).
+6 -4
View File
@@ -137,7 +137,7 @@ func scanTarFamilyAt(
enforceLimits bool,
) (*measuredArchive, error) {
if size < 0 {
return nil, fmt.Errorf("invalid pages package size")
return nil, errors.New("invalid pages package size")
}
tarReader, closeReader, err := openTarFamilyReader(io.NewSectionReader(ra, 0, size), format)
if err != nil {
@@ -154,7 +154,7 @@ func scanTarReader(tarReader *tar.Reader, limits Limits, enforceLimits bool) (*m
measured := &measuredArchive{files: make([]measuredFile, 0)}
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@@ -183,7 +183,7 @@ func scanTarReader(tarReader *tar.Reader, limits Limits, enforceLimits bool) (*m
func buildMeasuredManifest(measured *measuredArchive, opts InspectOptions) (*Manifest, error) {
if measured == nil || measured.fileCount == 0 {
return nil, fmt.Errorf("pages package is empty")
return nil, errors.New("pages package is empty")
}
targetEntryPath, err := resolveTargetEntryPath(opts.RootDir, opts.EntryFile)
if err != nil {
@@ -231,7 +231,7 @@ func prepareMeasuredFile(measured *measuredArchive, normalizedPath string, decla
}
remaining := limits.MaxTotalBytes - measured.totalSize
if remaining < 0 || declaredSize > uint64(remaining) { //nolint:gosec // remaining is checked non-negative
return fmt.Errorf("pages extracted size exceeds limit")
return errors.New("pages extracted size exceeds limit")
}
return nil
}
@@ -295,6 +295,8 @@ func isTarFamily(format Format) bool {
switch format {
case FormatTar, FormatTarGz, FormatTarXz, FormatTarBz2:
return true
case FormatZip, FormatSevenZip:
return false
default:
return false
}
+6 -1
View File
@@ -8,6 +8,7 @@ import (
"archive/zip"
"compress/bzip2"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
@@ -52,13 +53,15 @@ func (z sevenZipArchiveFile) Open() (io.ReadCloser, error) {
// Tar-family archives use the sequential streaming paths in inspect.go/extract.go.
func listRandomAccessEntriesAt(ra io.ReaderAt, size int64, format Format) ([]Entry, error) {
if size < 0 {
return nil, fmt.Errorf("invalid pages package size")
return nil, errors.New("invalid pages package size")
}
switch format {
case FormatZip:
return listZipEntriesAt(ra, size)
case FormatSevenZip:
return listSevenZipEntriesAt(ra, size)
case FormatTar, FormatTarGz, FormatTarXz, FormatTarBz2:
return nil, fmt.Errorf("unsupported random-access pages package format: %s", format)
default:
return nil, fmt.Errorf("unsupported random-access pages package format: %s", format)
}
@@ -126,6 +129,8 @@ func openTarFamilyReader(r io.Reader, format Format) (*tar.Reader, func() error,
return tar.NewReader(xzReader), func() error { return nil }, nil
case FormatTarBz2:
return tar.NewReader(bzip2.NewReader(r)), func() error { return nil }, nil
case FormatZip, FormatSevenZip:
return nil, nil, fmt.Errorf("unsupported tar family format: %s", format)
default:
return nil, nil, fmt.Errorf("unsupported tar family format: %s", format)
}
+6 -5
View File
@@ -4,6 +4,7 @@
package pagesarchive
import (
"errors"
"fmt"
"path"
"strings"
@@ -18,7 +19,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) {
if allowEmpty {
return "", nil
}
return "", fmt.Errorf("pages path is required")
return "", errors.New("pages path is required")
}
if err := validateLogicalPathText(raw); err != nil {
return "", err
@@ -29,7 +30,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) {
if allowEmpty {
return "", nil
}
return "", fmt.Errorf("pages path is required")
return "", errors.New("pages path is required")
}
if strings.HasPrefix(cleaned, "/") || cleaned == ".." || strings.HasPrefix(cleaned, "../") {
return "", fmt.Errorf("pages path escapes directory: %s", raw)
@@ -39,7 +40,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) {
func validateLogicalPathText(raw string) error {
if !utf8.ValidString(raw) {
return fmt.Errorf("pages path is not valid UTF-8")
return errors.New("pages path is not valid UTF-8")
}
if strings.Contains(raw, "\\") {
return fmt.Errorf("pages path must use POSIX separators: %s", raw)
@@ -56,7 +57,7 @@ func validateLogicalPathText(raw string) error {
func validateLogicalPathRunes(raw string) error {
for _, r := range raw {
if r == 0 || unicode.IsControl(r) {
return fmt.Errorf("pages path contains a control character")
return errors.New("pages path contains a control character")
}
if r == '\'' || r == '"' || r == ';' {
return fmt.Errorf("pages path contains an unsupported character: %s", raw)
@@ -66,7 +67,7 @@ func validateLogicalPathRunes(raw string) error {
}
func validateLogicalPathSegments(raw string) error {
for _, segment := range strings.Split(raw, "/") {
for segment := range strings.SplitSeq(raw, "/") {
if len(segment) >= 2 && segment[1] == ':' {
return fmt.Errorf("pages path contains a Windows drive: %s", raw)
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package protocol
// AgentNodeSystemProfile is an alias for NodeSystemProfile used by server.
+1 -1
View File
@@ -82,7 +82,7 @@ func TestCustomPusherSend_ResponseBodyErrcode(t *testing.T) {
assert.Contains(t, err.Error(), tt.wantErrMsg)
return
}
assert.NoError(t, err)
require.NoError(t, err)
if tt.body != "" {
assert.Contains(t, upstreamResp, tt.body)
}
+1 -1
View File
@@ -41,7 +41,7 @@ func (p *EmailPusher) Send(ctx context.Context, cfg Config, target string, body
title = t
}
content := ""
var content string
if c, ok := body["content"].(string); ok && c != "" {
content = c
} else {
+2 -2
View File
@@ -160,7 +160,7 @@ func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[st
title = t
}
content := ""
var content string
if c, ok := body["content"].(string); ok && c != "" {
content = c
} else {
@@ -265,7 +265,7 @@ func (p *LarkPusher) ValidateConfig(cfg Config) error {
}
func larkSign(secret string, timestamp int64) (string, error) {
stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret
stringToSign := strconv.FormatInt(timestamp, 10) + "\n" + secret
h := hmac.New(sha256.New, []byte(stringToSign))
_, err := h.Write(nil)
if err != nil {
+1 -1
View File
@@ -60,7 +60,7 @@ func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, bo
if t, ok := body["title"].(string); ok && t != "" {
title = t
}
content := ""
var content string
if c, ok := body["content"].(string); ok && c != "" {
content = c
} else {
+5 -5
View File
@@ -23,7 +23,7 @@ func TestTelegramPusher_Send(t *testing.T) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
err := json.NewDecoder(r.Body).Decode(&receivedReq)
require.NoError(t, err)
assert.NoError(t, err)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok": true}`))
@@ -55,7 +55,7 @@ func TestTelegramPusher_Send(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req telegramMessageRequest
err := json.NewDecoder(r.Body).Decode(&req)
require.NoError(t, err)
assert.NoError(t, err)
requests = append(requests, &req)
if len(requests) == 1 {
@@ -84,7 +84,7 @@ func TestTelegramPusher_Send(t *testing.T) {
require.Len(t, requests, 2)
assert.Equal(t, "HTML", requests[0].ParseMode)
assert.Equal(t, "", requests[1].ParseMode)
assert.Empty(t, requests[1].ParseMode)
assert.Contains(t, requests[1].Text, "[INFO] Alert & Info")
assert.Contains(t, requests[1].Text, "A < B comparison")
})
@@ -96,7 +96,7 @@ func TestTelegramPusher_Send(t *testing.T) {
URL: "https://api.telegram.org",
}
err := pusher.ValidateConfig(cfg)
assert.Error(t, err)
require.Error(t, err)
cfg = Config{
Channel: "telegram",
@@ -104,7 +104,7 @@ func TestTelegramPusher_Send(t *testing.T) {
Secret: "token",
}
err = pusher.ValidateConfig(cfg)
assert.Error(t, err)
require.Error(t, err)
cfg = Config{
Channel: "telegram",
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
+23 -19
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package openresty renders OpenResty configuration from proxy route definitions.
package openresty
@@ -14,6 +17,7 @@ import (
"path"
"regexp"
"sort"
"strconv"
"strings"
)
@@ -103,7 +107,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er
displayName := resolveRouteSiteName(route)
cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules}
limitConfig := mergeRouteLimitConfig(route, doc.OpenRestyConfig)
powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF)
powEnabled := getPoWConfigForRoute(route.ID, doc.WAF)
if normalizeRouteUpstreamType(route.UpstreamType) == routeUpstreamTypePages {
if err := renderPagesRoute(&builder, route, displayName, serverNames, certificates, limitConfig, powEnabled, doc.OpenRestyConfig); err != nil {
return "", err
@@ -168,32 +172,32 @@ func DedupeSupportFiles(files []SupportFile) []SupportFile {
func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot, limitReqRates []string) string {
replacer := strings.NewReplacer(
"{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses,
"{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections),
"{{OpenRestyWorkerRlimitNofile}}", fmt.Sprintf("%d", cfg.WorkerRlimitNofile),
"{{OpenRestyWorkerConnections}}", strconv.Itoa(cfg.WorkerConnections),
"{{OpenRestyWorkerRlimitNofile}}", strconv.Itoa(cfg.WorkerRlimitNofile),
"{{OpenRestyConnectionUpgradeMap}}", renderConnectionUpgradeMap(),
"{{OpenRestyDefaultServerBlock}}", renderDefaultServerBlock(cfg.DefaultServerReturnStatus, cfg.HTTP3Enabled),
"{{OpenRestyAccessLogPath}}", AccessLogPlaceholder,
"{{OpenRestyErrorLogPath}}", ErrorLogPlaceholder,
"{{OpenRestyEventsUseDirective}}", renderTemplateDirective(cfg.EventsUse != "", fmt.Sprintf("use %s;", cfg.EventsUse)),
"{{OpenRestyEventsMultiAcceptDirective}}", renderTemplateDirective(cfg.EventsMultiAcceptEnabled, "multi_accept on;"),
"{{OpenRestyKeepaliveTimeout}}", fmt.Sprintf("%d", cfg.KeepaliveTimeout),
"{{OpenRestyKeepaliveRequests}}", fmt.Sprintf("%d", cfg.KeepaliveRequests),
"{{OpenRestyClientHeaderTimeout}}", fmt.Sprintf("%d", cfg.ClientHeaderTimeout),
"{{OpenRestyClientBodyTimeout}}", fmt.Sprintf("%d", cfg.ClientBodyTimeout),
"{{OpenRestyKeepaliveTimeout}}", strconv.Itoa(cfg.KeepaliveTimeout),
"{{OpenRestyKeepaliveRequests}}", strconv.Itoa(cfg.KeepaliveRequests),
"{{OpenRestyClientHeaderTimeout}}", strconv.Itoa(cfg.ClientHeaderTimeout),
"{{OpenRestyClientBodyTimeout}}", strconv.Itoa(cfg.ClientBodyTimeout),
"{{OpenRestyClientMaxBodySize}}", cfg.ClientMaxBodySize,
"{{OpenRestyLargeClientHeaderBuffers}}", cfg.LargeClientHeaderBuffers,
"{{OpenRestySendTimeout}}", fmt.Sprintf("%d", cfg.SendTimeout),
"{{OpenRestyProxyConnectTimeout}}", fmt.Sprintf("%d", cfg.ProxyConnectTimeout),
"{{OpenRestyProxySendTimeout}}", fmt.Sprintf("%d", cfg.ProxySendTimeout),
"{{OpenRestyProxyReadTimeout}}", fmt.Sprintf("%d", cfg.ProxyReadTimeout),
"{{OpenRestySendTimeout}}", strconv.Itoa(cfg.SendTimeout),
"{{OpenRestyProxyConnectTimeout}}", strconv.Itoa(cfg.ProxyConnectTimeout),
"{{OpenRestyProxySendTimeout}}", strconv.Itoa(cfg.ProxySendTimeout),
"{{OpenRestyProxyReadTimeout}}", strconv.Itoa(cfg.ProxyReadTimeout),
"{{OpenRestyProxyRequestBuffering}}", onOff(cfg.ProxyRequestBuffering),
"{{OpenRestyProxyBuffering}}", onOff(cfg.ProxyBufferingEnabled),
"{{OpenRestyProxyBuffers}}", cfg.ProxyBuffers,
"{{OpenRestyProxyBufferSize}}", cfg.ProxyBufferSize,
"{{OpenRestyProxyBusyBuffersSize}}", cfg.ProxyBusyBuffersSize,
"{{OpenRestyGzip}}", onOff(cfg.GzipEnabled),
"{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength),
"{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel),
"{{OpenRestyGzipMinLength}}", strconv.Itoa(cfg.GzipMinLength),
"{{OpenRestyGzipCompLevel}}", strconv.Itoa(cfg.GzipCompLevel),
"{{OpenRestyResolverDirective}}", renderTemplateDirective(cfg.Resolvers != "", fmt.Sprintf("resolver %s;", cfg.Resolvers)),
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg, limitReqRates),
"{{OpenRestyRouteConfigInclude}}", RouteConfigPlaceholder,
@@ -414,7 +418,7 @@ func pagesFallbackPath(deployment *PagesDeployment) string {
if value == "/" || strings.HasSuffix(value, "/") || strings.Contains(value, "\\") || strings.ContainsAny(value, "\"';") || strings.ContainsAny(value, " \t\r\n") {
return indexHTML
}
for _, segment := range strings.Split(value, "/") {
for segment := range strings.SplitSeq(value, "/") {
if segment == "." || segment == ".." {
return indexHTML
}
@@ -765,7 +769,7 @@ func renderDefaultServerBlock(statusCode int, http3Enabled bool) string {
" }",
"",
" server {",
fmt.Sprintf(" listen 443 ssl default_server;%s", h3Default),
" listen 443 ssl default_server;" + h3Default,
" server_name _;",
"",
" ssl_reject_handshake on;",
@@ -826,7 +830,7 @@ func validateCertificateCoverage(certPEM string, domains []string) error {
return nil
}
func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig) {
func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) bool {
enabledGroups := make(map[uint]WAFRuleGroup, len(snapshot.RuleGroups))
globalGroupIDs := make([]uint, 0)
for _, group := range snapshot.RuleGroups {
@@ -856,10 +860,10 @@ func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig)
for _, groupID := range activeGroupIDs {
group := enabledGroups[groupID]
if graphContainsNodeType(group.Graph, "pow") {
return true, nil
return true
}
}
return false, nil
return false
}
func graphContainsNodeType(graph WAFRuleGraph, nodeType string) bool {
@@ -946,7 +950,7 @@ func buildPathPrefixMatchPattern(rules []string) string {
parts = append(parts, "/")
continue
}
parts = append(parts, fmt.Sprintf("%s(?:/|$)", regexp.QuoteMeta(trimmed)))
parts = append(parts, regexp.QuoteMeta(trimmed)+"(?:/|$)")
}
return fmt.Sprintf("^(?:%s)", strings.Join(parts, "|"))
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
+4 -4
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
@@ -108,13 +111,10 @@ func TestGetPoWConfigForRouteUsesGlobalGroupWithoutExplicitBinding(t *testing.T)
},
}
enabled, config := getPoWConfigForRoute(42, snapshot)
enabled := getPoWConfigForRoute(42, snapshot)
if !enabled {
t.Fatal("expected pow to be enabled via global rule group")
}
if config != nil {
t.Fatalf("expected node config to stay in runtime graph, got legacy config %#v", config)
}
}
func TestRenderRouteConfigEnablesPoWLocationsFromRuntimeGraph(t *testing.T) {
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
+5 -4
View File
@@ -4,6 +4,7 @@
package openresty
import (
"errors"
"fmt"
"sort"
"strconv"
@@ -22,14 +23,14 @@ const (
func ParseStatusCodeTag(tag string) (lo, hi int, err error) {
tag = strings.TrimSpace(tag)
if tag == "" {
return 0, 0, fmt.Errorf("状态码标签不能为空")
return 0, 0, errors.New("状态码标签不能为空")
}
if i := strings.IndexByte(tag, '-'); i >= 0 {
lo, err = strconv.Atoi(tag[:i])
if before, after, ok := strings.Cut(tag, "-"); ok {
lo, err = strconv.Atoi(before)
if err != nil {
return 0, 0, fmt.Errorf("无效状态码区间: %s", tag)
}
hi, err = strconv.Atoi(tag[i+1:])
hi, err = strconv.Atoi(after)
if err != nil {
return 0, 0, fmt.Errorf("无效状态码区间: %s", tag)
}
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import "testing"
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openresty
import (
+7 -4
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package util provides shared formatting and string helper functions.
package util
@@ -22,20 +25,20 @@ const (
// Bytes2Size converts a byte count to a human-readable string with unit (B, KB, MB, GB).
func Bytes2Size(num int64) string {
numStr := ""
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 = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
numStr = strconv.Itoa(int(float64(num) / float64(sizeMB)))
unit = "MB"
case num/int64(sizeKB) >= 1:
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
numStr = strconv.Itoa(int(float64(num) / float64(sizeKB)))
unit = "KB"
default:
numStr = fmt.Sprintf("%d", num)
numStr = strconv.FormatInt(num, 10)
}
return numStr + " " + unit
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import "strings"
+9 -3
View File
@@ -1,14 +1,20 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import "fmt"
import (
"fmt"
"strconv"
)
// Interface2String converts a string, int, or float64 value to its string representation.
func Interface2String(inter interface{}) string {
func Interface2String(inter any) string {
switch v := inter.(type) {
case string:
return v
case int:
return fmt.Sprintf("%d", v)
return strconv.Itoa(v)
case float64:
return fmt.Sprintf("%f", v)
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import (
+9 -15
View File
@@ -1,13 +1,13 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package util
import "strconv"
func compareVersionNumbers(left, right VersionInfo) int {
maxLen := len(left.Numbers)
if len(right.Numbers) > maxLen {
maxLen = len(right.Numbers)
}
for index := 0; index < maxLen; index++ {
maxLen := max(len(right.Numbers), len(left.Numbers))
for index := range maxLen {
leftValue := 0
rightValue := 0
if index < len(left.Numbers) {
@@ -37,11 +37,8 @@ func compareGitDescribeDistance(left, right VersionInfo) int {
}
func compareGitDescribeTails(left, right VersionInfo) int {
maxLen := len(left.GitDescribeTail)
if len(right.GitDescribeTail) > maxLen {
maxLen = len(right.GitDescribeTail)
}
for index := 0; index < maxLen; index++ {
maxLen := max(len(right.GitDescribeTail), len(left.GitDescribeTail))
for index := range maxLen {
if index >= len(left.GitDescribeTail) {
return -1
}
@@ -69,11 +66,8 @@ func comparePrereleaseIdentifiers(left, right VersionInfo) int {
return -1
}
maxLen := len(left.Prerelease)
if len(right.Prerelease) > maxLen {
maxLen = len(right.Prerelease)
}
for index := 0; index < maxLen; index++ {
maxLen := max(len(right.Prerelease), len(left.Prerelease))
for index := range maxLen {
if index >= len(left.Prerelease) {
return -1
}
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package wsclient provides a WebSocket client for agent/server communication.
package wsclient