// Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package util import ( "bytes" "crypto/rand" "encoding/hex" "fmt" "math/big" rand2 "math/rand/v2" "slices" "strconv" "strings" "sync" "code.gitea.io/gitea/modules/container" "golang.org/x/text/cases" "golang.org/x/text/language" ) // IsEmptyString checks if the provided string is empty func IsEmptyString(s string) bool { return len(strings.TrimSpace(s)) == 0 } // NormalizeEOL will convert Windows (CRLF) and Mac (CR) EOLs to UNIX (LF) func NormalizeEOL(input []byte) []byte { var right, left, pos int if right = bytes.IndexByte(input, '\r'); right == -1 { return input } length := len(input) tmp := make([]byte, length) // We know that left < length because otherwise right would be -1 from IndexByte. copy(tmp[pos:pos+right], input[left:left+right]) pos += right tmp[pos] = '\n' left += right + 1 pos++ for left < length { if input[left] == '\n' { left++ } right = bytes.IndexByte(input[left:], '\r') if right == -1 { copy(tmp[pos:], input[left:]) pos += length - left break } copy(tmp[pos:pos+right], input[left:left+right]) pos += right tmp[pos] = '\n' left += right + 1 pos++ } return tmp[:pos] } // CryptoRandomInt returns a crypto random integer between 0 and limit, inclusive func CryptoRandomInt(limit int64) int64 { rInt, err := rand.Int(rand.Reader, big.NewInt(limit)) if err != nil { panic(err) // this should never happen } return rInt.Int64() } // CryptoRandomString generates a crypto random alphanumerical string, each byte is generated by [0,61] range func CryptoRandomString(length int64) string { const alphanumericalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" buf := make([]byte, length) limit := int64(len(alphanumericalChars)) for i := range buf { num := CryptoRandomInt(limit) buf[i] = alphanumericalChars[num] } return string(buf) } // CryptoRandomBytes generates `length` crypto bytes // This differs from CryptoRandomString, as each byte in CryptoRandomString is generated by [0,61] range // This function generates totally random bytes, each byte is generated by [0,255] range func CryptoRandomBytes(length int64) []byte { buf := make([]byte, length) if _, err := rand.Read(buf); err != nil { panic(err) // this should never happen, "rand.Read" never fails } return buf } var chaCha8RandPool = sync.OnceValue(func() *sync.Pool { return &sync.Pool{ New: func() any { seed := CryptoRandomBytes(32) return rand2.NewChaCha8([32]byte(seed)) }, } }) func FastCryptoRandomBytes(length int) []byte { // ChaCha8 is about 20x times faster than system's crypto/rand. // It is suitable for UUIDs, session IDs, etc pool := chaCha8RandPool() chaCha8Rand := pool.Get().(*rand2.ChaCha8) defer pool.Put(chaCha8Rand) buf := make([]byte, length) _, _ = chaCha8Rand.Read(buf) return buf } func FastCryptoRandomHex(length int) string { buf := FastCryptoRandomBytes(length / 2) return hex.EncodeToString(buf) } // ToLowerASCII returns s with all ASCII letters mapped to their lower case. func ToLowerASCII(s string) string { b := []byte(s) for i, c := range b { if 'A' <= c && c <= 'Z' { b[i] += 'a' - 'A' } } return string(b) } // ToTitleCase returns s with all english words capitalized func ToTitleCase(s string) string { // `cases.Title` is not thread-safe, do not use global shared variable for it return cases.Title(language.English).String(s) } // ToTitleCaseNoLower returns s with all english words capitalized without lower-casing func ToTitleCaseNoLower(s string) string { // `cases.Title` is not thread-safe, do not use global shared variable for it return cases.Title(language.English, cases.NoLower).String(s) } // ToInt64 transform a given int into int64. func ToInt64(number any) (int64, error) { var value int64 switch v := number.(type) { case int: value = int64(v) case int8: value = int64(v) case int16: value = int64(v) case int32: value = int64(v) case int64: value = v case uint: value = int64(v) case uint8: value = int64(v) case uint16: value = int64(v) case uint32: value = int64(v) case uint64: value = int64(v) case float32: value = int64(v) case float64: value = int64(v) case string: var err error if value, err = strconv.ParseInt(v, 10, 64); err != nil { return 0, err } default: return 0, fmt.Errorf("unable to convert %v to int64", number) } return value, nil } // ToFloat64 transform a given int into float64. func ToFloat64(number any) (float64, error) { var value float64 switch v := number.(type) { case int: value = float64(v) case int8: value = float64(v) case int16: value = float64(v) case int32: value = float64(v) case int64: value = float64(v) case uint: value = float64(v) case uint8: value = float64(v) case uint16: value = float64(v) case uint32: value = float64(v) case uint64: value = float64(v) case float32: value = float64(v) case float64: value = v case string: var err error if value, err = strconv.ParseFloat(v, 64); err != nil { return 0, err } default: return 0, fmt.Errorf("unable to convert %v to float64", number) } return value, nil } // Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal" func Iif[T any](condition bool, trueVal, falseVal T) T { if condition { return trueVal } return falseVal } // IfZero returns "def" if "v" is a zero value, otherwise "v" func IfZero[T comparable](v, def T) T { var zero T if v == zero { return def } 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) } // calling `foo()` gets zero value 0, calling `foo(100)` gets 100 // func bar(optArg ...int) { return OptionalArg(optArg, 42) } // calling `bar()` gets default value 42, calling `bar(100)` gets 100 // // Passing more than 1 item to `optArg` or `defaultValue` is undefined behavior. // At the moment only the first item is used. func OptionalArg[T any](optArg []T, defaultValue ...T) (ret T) { if len(optArg) >= 1 { return optArg[0] } if len(defaultValue) >= 1 { return defaultValue[0] } return ret } type EnumConst[T comparable] interface { EnumValues() []T } // EnumValue returns the value if it's in the enum const's values, // otherwise returns the first item of enums as default value. func EnumValue[T comparable](val EnumConst[T]) (ret T, valid bool) { enums := val.EnumValues() if slices.Contains(enums, val.(T)) { return val.(T), true } return enums[0], false } func NormalizeStringEOL(input string) string { // Since the content is from a form which is a textarea, the line endings are \r\n. // It's a standard behavior of HTML. // But in most cases, we only want "\n" for EOL // * Text files: use "\n" by default because "\r\n" sometimes doesn't work in POSIX // * Actions values: store them as "\n" like what GitHub does. // And users are unlikely to really need the "\r". // Other than this, we should respect the original content, even leading or trailing spaces. return UnsafeBytesToString(NormalizeEOL(UnsafeStringToBytes(input))) } func DiffSlice[T comparable](oldSlice, newSlice []T) (added, removed []T) { oldSet := container.SetOf(oldSlice...) newSet := container.SetOf(newSlice...) addedSet, removedSet := container.Set[T]{}, container.Set[T]{} for _, v := range newSlice { if !oldSet.Contains(v) && addedSet.Add(v) { added = append(added, v) } } for _, v := range oldSlice { if !newSet.Contains(v) && removedSet.Add(v) { removed = append(removed, v) } } return added, removed }