1// Copyright 2017 The Gitea Authors. All rights reserved.
2// Copyright 2024 The Forgejo Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package util
6
7import (
8 "bytes"
9 "crypto/ed25519"
10 "crypto/rand"
11 "encoding/pem"
12 "fmt"
13 "math/big"
14 "strconv"
15 "strings"
16
17 "golang.org/x/crypto/ssh"
18 "golang.org/x/text/cases"
19 "golang.org/x/text/language"
20)
21
22// IsEmptyString checks if the provided string is empty
23func IsEmptyString(s string) bool {
24 return len(strings.TrimSpace(s)) == 0
25}
26
27// NormalizeEOL will convert Windows (CRLF) and Mac (CR) EOLs to UNIX (LF)
28func NormalizeEOL(input []byte) []byte {
29 var right, left, pos int
30 if right = bytes.IndexByte(input, '\r'); right == -1 {
31 return input
32 }
33 length := len(input)
34 tmp := make([]byte, length)
35
36 // We know that left < length because otherwise right would be -1 from IndexByte.
37 copy(tmp[pos:pos+right], input[left:left+right])
38 pos += right
39 tmp[pos] = '\n'
40 left += right + 1
41 pos++
42
43 for left < length {
44 if input[left] == '\n' {
45 left++
46 }
47
48 right = bytes.IndexByte(input[left:], '\r')
49 if right == -1 {
50 copy(tmp[pos:], input[left:])
51 pos += length - left
52 break
53 }
54 copy(tmp[pos:pos+right], input[left:left+right])
55 pos += right
56 tmp[pos] = '\n'
57 left += right + 1
58 pos++
59 }
60 return tmp[:pos]
61}
62
63// CryptoRandomInt returns a crypto random integer between 0 and limit, inclusive
64func CryptoRandomInt(limit int64) (int64, error) {
65 rInt, err := rand.Int(rand.Reader, big.NewInt(limit))
66 if err != nil {
67 return 0, err
68 }
69 return rInt.Int64(), nil
70}
71
72const alphanumericalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
73
74// CryptoRandomString generates a crypto random alphanumerical string, each byte is generated by [0,61] range
75func CryptoRandomString(length int64) (string, error) {
76 buf := make([]byte, length)
77 limit := int64(len(alphanumericalChars))
78 for i := range buf {
79 num, err := CryptoRandomInt(limit)
80 if err != nil {
81 return "", err
82 }
83 buf[i] = alphanumericalChars[num]
84 }
85 return string(buf), nil
86}
87
88// CryptoRandomBytes generates `length` crypto bytes
89// This differs from CryptoRandomString, as each byte in CryptoRandomString is generated by [0,61] range
90// This function generates totally random bytes, each byte is generated by [0,255] range
91func CryptoRandomBytes(length int64) []byte {
92 // crypto/rand.Read is documented to never return a error.
93 // https://go.dev/issue/66821
94 buf := make([]byte, length)
95 n, err := rand.Read(buf)
96 if err != nil || n != int(length) {
97 panic(err)
98 }
99
100 return buf
101}
102
103// ToUpperASCII returns s with all ASCII letters mapped to their upper case.
104func ToUpperASCII(s string) string {
105 b := []byte(s)
106 for i, c := range b {
107 if 'a' <= c && c <= 'z' {
108 b[i] -= 'a' - 'A'
109 }
110 }
111 return string(b)
112}
113
114// ToTitleCase returns s with all english words capitalized
115func ToTitleCase(s string) string {
116 // `cases.Title` is not thread-safe, do not use global shared variable for it
117 return cases.Title(language.English).String(s)
118}
119
120// ToTitleCaseNoLower returns s with all english words capitalized without lower-casing
121func ToTitleCaseNoLower(s string) string {
122 // `cases.Title` is not thread-safe, do not use global shared variable for it
123 return cases.Title(language.English, cases.NoLower).String(s)
124}
125
126// ToInt64 transform a given int into int64.
127func ToInt64(number any) (int64, error) {
128 var value int64
129 switch v := number.(type) {
130 case int:
131 value = int64(v)
132 case int8:
133 value = int64(v)
134 case int16:
135 value = int64(v)
136 case int32:
137 value = int64(v)
138 case int64:
139 value = v
140
141 case uint:
142 value = int64(v)
143 case uint8:
144 value = int64(v)
145 case uint16:
146 value = int64(v)
147 case uint32:
148 value = int64(v)
149 case uint64:
150 value = int64(v)
151
152 case float32:
153 value = int64(v)
154 case float64:
155 value = int64(v)
156
157 case string:
158 var err error
159 if value, err = strconv.ParseInt(v, 10, 64); err != nil {
160 return 0, err
161 }
162 default:
163 return 0, fmt.Errorf("unable to convert %v to int64", number)
164 }
165 return value, nil
166}
167
168// ToFloat64 transform a given int into float64.
169func ToFloat64(number any) (float64, error) {
170 var value float64
171 switch v := number.(type) {
172 case int:
173 value = float64(v)
174 case int8:
175 value = float64(v)
176 case int16:
177 value = float64(v)
178 case int32:
179 value = float64(v)
180 case int64:
181 value = float64(v)
182
183 case uint:
184 value = float64(v)
185 case uint8:
186 value = float64(v)
187 case uint16:
188 value = float64(v)
189 case uint32:
190 value = float64(v)
191 case uint64:
192 value = float64(v)
193
194 case float32:
195 value = float64(v)
196 case float64:
197 value = v
198
199 case string:
200 var err error
201 if value, err = strconv.ParseFloat(v, 64); err != nil {
202 return 0, err
203 }
204 default:
205 return 0, fmt.Errorf("unable to convert %v to float64", number)
206 }
207 return value, nil
208}
209
210// ToPointer returns the pointer of a copy of any given value
211func ToPointer[T any](val T) *T {
212 return &val
213}
214
215// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
216func Iif[T any](condition bool, trueVal, falseVal T) T {
217 if condition {
218 return trueVal
219 }
220 return falseVal
221}
222
223// IfZero returns "def" if "v" is a zero value, otherwise "v"
224func IfZero[T comparable](v, def T) T {
225 var zero T
226 if v == zero {
227 return def
228 }
229 return v
230}
231
232// OptionalArg helps the "optional argument" in Golang:
233//
234// func foo(optArg ...int) { return OptionalArg(optArg) }
235// calling `foo()` gets zero value 0, calling `foo(100)` gets 100
236// func bar(optArg ...int) { return OptionalArg(optArg, 42) }
237// calling `bar()` gets default value 42, calling `bar(100)` gets 100
238//
239// Passing more than 1 item to `optArg` or `defaultValue` is undefined behavior.
240// At the moment only the first item is used.
241func OptionalArg[T any](optArg []T, defaultValue ...T) (ret T) {
242 if len(optArg) >= 1 {
243 return optArg[0]
244 }
245 if len(defaultValue) >= 1 {
246 return defaultValue[0]
247 }
248 return ret
249}
250
251func ReserveLineBreakForTextarea(input string) string {
252 // Since the content is from a form which is a textarea, the line endings are \r\n.
253 // It's a standard behavior of HTML.
254 // But we want to store them as \n like what GitHub does.
255 // And users are unlikely to really need to keep the \r.
256 // Other than this, we should respect the original content, even leading or trailing spaces.
257 return strings.ReplaceAll(input, "\r\n", "\n")
258}
259
260// GenerateSSHKeypair generates a ed25519 SSH-compatible keypair.
261func GenerateSSHKeypair() (publicKey, privateKey []byte, err error) {
262 public, private, err := ed25519.GenerateKey(nil)
263 if err != nil {
264 return nil, nil, fmt.Errorf("ed25519.GenerateKey: %w", err)
265 }
266
267 privPEM, err := ssh.MarshalPrivateKey(private, "")
268 if err != nil {
269 return nil, nil, fmt.Errorf("ssh.MarshalPrivateKey: %w", err)
270 }
271
272 sshPublicKey, err := ssh.NewPublicKey(public)
273 if err != nil {
274 return nil, nil, fmt.Errorf("ssh.NewPublicKey: %w", err)
275 }
276
277 return ssh.MarshalAuthorizedKey(sshPublicKey), pem.EncodeToMemory(privPEM), nil
278}