aboutsummaryrefslogtreecommitdiffstats
path: root/modules/util
diff options
context:
space:
mode:
Diffstat (limited to 'modules/util')
-rw-r--r--modules/util/error.go4
-rw-r--r--modules/util/filebuffer/file_backed_buffer.go35
-rw-r--r--modules/util/filebuffer/file_backed_buffer_test.go3
-rw-r--r--modules/util/map.go13
-rw-r--r--modules/util/map_test.go26
-rw-r--r--modules/util/remove.go6
-rw-r--r--modules/util/rotatingfilewriter/writer_test.go2
-rw-r--r--modules/util/slice.go3
-rw-r--r--modules/util/string.go23
-rw-r--r--modules/util/time_str.go2
-rw-r--r--modules/util/truncate.go20
-rw-r--r--modules/util/truncate_test.go2
-rw-r--r--modules/util/util.go7
13 files changed, 99 insertions, 47 deletions
diff --git a/modules/util/error.go b/modules/util/error.go
index 8e67d5a82f..6b2721618e 100644
--- a/modules/util/error.go
+++ b/modules/util/error.go
@@ -17,8 +17,8 @@ var (
ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
- // ErrUnprocessableContent implies HTTP 422, syntax of the request content was correct,
- // but server was unable to process the contained instructions
+ // ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
+ // but the server is unable to process the contained instructions
ErrUnprocessableContent = errors.New("unprocessable content")
)
diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go
index 739543e297..0731ba30c8 100644
--- a/modules/util/filebuffer/file_backed_buffer.go
+++ b/modules/util/filebuffer/file_backed_buffer.go
@@ -7,16 +7,10 @@ import (
"bytes"
"errors"
"io"
- "math"
"os"
)
-var (
- // ErrInvalidMemorySize occurs if the memory size is not in a valid range
- ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32")
- // ErrWriteAfterRead occurs if Write is called after a read operation
- ErrWriteAfterRead = errors.New("Write is unsupported after a read operation")
-)
+var ErrWriteAfterRead = errors.New("write is unsupported after a read operation") // occurs if Write is called after a read operation
type readAtSeeker interface {
io.ReadSeeker
@@ -30,34 +24,17 @@ type FileBackedBuffer struct {
maxMemorySize int64
size int64
buffer bytes.Buffer
+ tempDir string
file *os.File
reader readAtSeeker
}
// New creates a file backed buffer with a specific maximum memory size
-func New(maxMemorySize int) (*FileBackedBuffer, error) {
- if maxMemorySize < 0 || maxMemorySize > math.MaxInt32 {
- return nil, ErrInvalidMemorySize
- }
-
+func New(maxMemorySize int, tempDir string) *FileBackedBuffer {
return &FileBackedBuffer{
maxMemorySize: int64(maxMemorySize),
- }, nil
-}
-
-// CreateFromReader creates a file backed buffer and copies the provided reader data into it.
-func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) {
- b, err := New(maxMemorySize)
- if err != nil {
- return nil, err
+ tempDir: tempDir,
}
-
- _, err = io.Copy(b, r)
- if err != nil {
- return nil, err
- }
-
- return b, nil
}
// Write implements io.Writer
@@ -73,7 +50,7 @@ func (b *FileBackedBuffer) Write(p []byte) (int, error) {
n, err = b.file.Write(p)
} else {
if b.size+int64(len(p)) > b.maxMemorySize {
- b.file, err = os.CreateTemp("", "gitea-buffer-")
+ b.file, err = os.CreateTemp(b.tempDir, "gitea-buffer-")
if err != nil {
return 0, err
}
@@ -148,7 +125,7 @@ func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) {
func (b *FileBackedBuffer) Close() error {
if b.file != nil {
err := b.file.Close()
- os.Remove(b.file.Name())
+ _ = os.Remove(b.file.Name())
b.file = nil
return err
}
diff --git a/modules/util/filebuffer/file_backed_buffer_test.go b/modules/util/filebuffer/file_backed_buffer_test.go
index 16d5a1965f..3f13c6ac7b 100644
--- a/modules/util/filebuffer/file_backed_buffer_test.go
+++ b/modules/util/filebuffer/file_backed_buffer_test.go
@@ -21,7 +21,8 @@ func TestFileBackedBuffer(t *testing.T) {
}
for _, c := range cases {
- buf, err := CreateFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
+ buf := New(c.MaxMemorySize, t.TempDir())
+ _, err := io.Copy(buf, strings.NewReader(c.Data))
assert.NoError(t, err)
assert.EqualValues(t, len(c.Data), buf.Size())
diff --git a/modules/util/map.go b/modules/util/map.go
new file mode 100644
index 0000000000..f307faad1f
--- /dev/null
+++ b/modules/util/map.go
@@ -0,0 +1,13 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+func GetMapValueOrDefault[T any](m map[string]any, key string, defaultValue T) T {
+ if value, ok := m[key]; ok {
+ if v, ok := value.(T); ok {
+ return v
+ }
+ }
+ return defaultValue
+}
diff --git a/modules/util/map_test.go b/modules/util/map_test.go
new file mode 100644
index 0000000000..1a141cec88
--- /dev/null
+++ b/modules/util/map_test.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetMapValueOrDefault(t *testing.T) {
+ testMap := map[string]any{
+ "key1": "value1",
+ "key2": 42,
+ "key3": nil,
+ }
+
+ assert.Equal(t, "value1", GetMapValueOrDefault(testMap, "key1", "default"))
+ assert.Equal(t, 42, GetMapValueOrDefault(testMap, "key2", 0))
+
+ assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key4", "default"))
+ assert.Equal(t, 100, GetMapValueOrDefault(testMap, "key5", 100))
+
+ assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key3", "default"))
+}
diff --git a/modules/util/remove.go b/modules/util/remove.go
index d1e38faf5f..3db0b5a796 100644
--- a/modules/util/remove.go
+++ b/modules/util/remove.go
@@ -15,7 +15,7 @@ const windowsSharingViolationError syscall.Errno = 32
// Remove removes the named file or (empty) directory with at most 5 attempts.
func Remove(name string) error {
var err error
- for i := 0; i < 5; i++ {
+ for range 5 {
err = os.Remove(name)
if err == nil {
break
@@ -44,7 +44,7 @@ func Remove(name string) error {
// RemoveAll removes the named file or (empty) directory with at most 5 attempts.
func RemoveAll(name string) error {
var err error
- for i := 0; i < 5; i++ {
+ for range 5 {
err = os.RemoveAll(name)
if err == nil {
break
@@ -73,7 +73,7 @@ func RemoveAll(name string) error {
// Rename renames (moves) oldpath to newpath with at most 5 attempts.
func Rename(oldpath, newpath string) error {
var err error
- for i := 0; i < 5; i++ {
+ for i := range 5 {
err = os.Rename(oldpath, newpath)
if err == nil {
break
diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go
index 88392797b3..f6ea1d50ae 100644
--- a/modules/util/rotatingfilewriter/writer_test.go
+++ b/modules/util/rotatingfilewriter/writer_test.go
@@ -23,7 +23,7 @@ func TestCompressOldFile(t *testing.T) {
ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
- for i := 0; i < 999; i++ {
+ for range 999 {
f.WriteString("This is a test file\n")
ng.WriteString("This is a test file\n")
}
diff --git a/modules/util/slice.go b/modules/util/slice.go
index da6886491e..aaa729c1c9 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -12,8 +12,7 @@ import (
// SliceContainsString sequential searches if string exists in slice.
func SliceContainsString(slice []string, target string, insensitive ...bool) bool {
if len(insensitive) != 0 && insensitive[0] {
- target = strings.ToLower(target)
- return slices.ContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target })
+ return slices.ContainsFunc(slice, func(t string) bool { return strings.EqualFold(t, target) })
}
return slices.Contains(slice, target)
diff --git a/modules/util/string.go b/modules/util/string.go
index 19cf75b8b3..b9b59df3ef 100644
--- a/modules/util/string.go
+++ b/modules/util/string.go
@@ -103,10 +103,31 @@ func UnsafeStringToBytes(s string) []byte {
func SplitTrimSpace(input, sep string) []string {
input = strings.TrimSpace(input)
var stringList []string
- for _, s := range strings.Split(input, sep) {
+ for s := range strings.SplitSeq(input, sep) {
if s = strings.TrimSpace(s); s != "" {
stringList = append(stringList, s)
}
}
return stringList
}
+
+func asciiLower(b byte) byte {
+ if 'A' <= b && b <= 'Z' {
+ return b + ('a' - 'A')
+ }
+ return b
+}
+
+// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
+// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
+func AsciiEqualFold(s, t string) bool { //nolint:revive // PascalCase
+ if len(s) != len(t) {
+ return false
+ }
+ for i := 0; i < len(s); i++ {
+ if asciiLower(s[i]) != asciiLower(t[i]) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/modules/util/time_str.go b/modules/util/time_str.go
index 0fccfe82cc..81b132c3db 100644
--- a/modules/util/time_str.go
+++ b/modules/util/time_str.go
@@ -59,7 +59,7 @@ func TimeEstimateParse(timeStr string) (int64, error) {
unit := timeStr[match[4]:match[5]]
found := false
for _, u := range timeStrGlobalVars().units {
- if strings.ToLower(unit) == u.name {
+ if strings.EqualFold(unit, u.name) {
total += amount * u.num
found = true
break
diff --git a/modules/util/truncate.go b/modules/util/truncate.go
index 2bce248281..52534d3cac 100644
--- a/modules/util/truncate.go
+++ b/modules/util/truncate.go
@@ -19,7 +19,7 @@ func IsLikelyEllipsisLeftPart(s string) bool {
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
}
-func ellipsisGuessDisplayWidth(r rune) int {
+func ellipsisDisplayGuessWidth(r rune) int {
// To make the truncated string as long as possible,
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
// Here we only make the best guess (better than counting them in bytes),
@@ -48,13 +48,17 @@ func ellipsisGuessDisplayWidth(r rune) int {
// It appends "…" or "..." at the end of truncated string.
// It guarantees the length of the returned runes doesn't exceed the limit.
func EllipsisDisplayString(str string, limit int) string {
- s, _, _, _ := ellipsisDisplayString(str, limit)
+ s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
return s
}
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
- left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
+ return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
+}
+
+func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
+ left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
if truncated {
right = str[offset:]
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
@@ -68,7 +72,7 @@ func EllipsisDisplayStringX(str string, limit int) (left, right string) {
return left, right
}
-func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
+func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
if len(str) <= limit {
return str, len(str), false, false
}
@@ -81,7 +85,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
for i, r := range str {
encounterInvalid = encounterInvalid || r == utf8.RuneError
pos = i
- runeWidth := ellipsisGuessDisplayWidth(r)
+ runeWidth := widthGuess(r)
if used+runeWidth+3 > limit {
break
}
@@ -96,7 +100,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
if nextCnt >= 4 {
break
}
- nextWidth += ellipsisGuessDisplayWidth(r)
+ nextWidth += widthGuess(r)
nextCnt++
}
if nextCnt <= 3 && used+nextWidth <= limit {
@@ -114,6 +118,10 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
return str[:offset] + ellipsis, offset, true, encounterInvalid
}
+func EllipsisTruncateRunes(str string, limit int) (left, right string) {
+ return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
+}
+
// TruncateRunes returns a truncated string with given rune limit,
// it returns input string if its rune length doesn't exceed the limit.
func TruncateRunes(str string, limit int) string {
diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go
index 9f4ad7dc20..6d71f38c0c 100644
--- a/modules/util/truncate_test.go
+++ b/modules/util/truncate_test.go
@@ -29,7 +29,7 @@ func TestEllipsisGuessDisplayWidth(t *testing.T) {
t.Run(c.r, func(t *testing.T) {
w := 0
for _, r := range c.r {
- w += ellipsisGuessDisplayWidth(r)
+ w += ellipsisDisplayGuessWidth(r)
}
assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
})
diff --git a/modules/util/util.go b/modules/util/util.go
index 72fcddbe13..dd8e073888 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -219,6 +219,13 @@ func IfZero[T comparable](v, def T) T {
return v
}
+func IfEmpty[T any](v, def []T) []T {
+ if len(v) == 0 {
+ return def
+ }
+ return v
+}
+
// OptionalArg helps the "optional argument" in Golang:
//
// func foo(optArg ...int) { return OptionalArg(optArg) }