• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

couchbase / sync_gateway / 433

05 Jun 2025 03:32PM UTC coverage: 64.334% (+0.03%) from 64.302%
433

push

jenkins

web-flow
Docs/API: Fix outdated 'keyspace_map' ISGR collections config reference (#7561)

36770 of 57155 relevant lines covered (64.33%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.24
/base/util.go
1
//  Copyright 2012-Present Couchbase, Inc.
2
//
3
//  Use of this software is governed by the Business Source License included
4
//  in the file licenses/BSL-Couchbase.txt.  As of the Change Date specified
5
//  in that file, in accordance with the Business Source License, use of this
6
//  software will be governed by the Apache License, Version 2.0, included in
7
//  the file licenses/APL2.txt.
8

9
package base
10

11
import (
12
        "bytes"
13
        "context"
14
        "crypto/rand"
15
        "crypto/sha1"
16
        "crypto/tls"
17
        "encoding/binary"
18
        "encoding/hex"
19
        "encoding/json"
20
        "errors"
21
        "expvar"
22
        "fmt"
23
        "hash/crc32"
24
        "io"
25
        "math"
26
        "net"
27
        "net/http"
28
        "net/url"
29
        "reflect"
30
        "regexp"
31
        "runtime"
32
        "runtime/debug"
33
        "sort"
34
        "strconv"
35
        "strings"
36
        "sync/atomic"
37
        "time"
38

39
        "github.com/couchbase/gomemcached"
40
        "github.com/gorilla/mux"
41
        pkgerrors "github.com/pkg/errors"
42
        "golang.org/x/exp/constraints"
43
)
44

45
const (
46
        kMaxDeltaTtl         = 60 * 60 * 24 * 30
47
        kMaxDeltaTtlDuration = 60 * 60 * 24 * 30 * time.Second
48
)
49

50
// NonCancellableContext is here to stroe a context that is not cancellable. Used to explicitly state when a change from
51
// a cancellable context to a context withoutr contex is required
52
type NonCancellableContext struct {
53
        Ctx context.Context
54
}
55

56
// NewNonCancelCtx creates a new background context struct for operations that require a fresh context
57
func NewNonCancelCtx() NonCancellableContext {
1✔
58
        ctxStruct := NonCancellableContext{
1✔
59
                Ctx: context.Background(),
1✔
60
        }
1✔
61
        return ctxStruct
1✔
62
}
1✔
63

64
// NewNonCancelCtx creates a new background context struct for operations that require a fresh context, with database logging context added
65
func NewNonCancelCtxForDatabase(parentContext context.Context) NonCancellableContext {
1✔
66
        ctx := getLogCtx(parentContext)
1✔
67

1✔
68
        dbLogContext := DatabaseLogCtx(context.Background(), ctx.Database, ctx.DbLogConfig)
1✔
69

1✔
70
        return NonCancellableContext{
1✔
71
                Ctx: dbLogContext,
1✔
72
        }
1✔
73
}
1✔
74

75
// RedactBasicAuthURLUserAndPassword returns the given string, with a redacted HTTP basic auth component.
76
func RedactBasicAuthURLUserAndPassword(ctx context.Context, urlIn string) string {
1✔
77
        redactedUrl, err := RedactBasicAuthURL(urlIn, false)
1✔
78
        if err != nil {
2✔
79
                WarnfCtx(ctx, "%v", err)
1✔
80
                return ""
1✔
81
        }
1✔
82
        return redactedUrl
1✔
83
}
84

85
// RedactBasicAuthURLPassword returns the given string, with a redacted HTTP basic auth password component.
86
func RedactBasicAuthURLPassword(ctx context.Context, urlIn string) string {
1✔
87
        redactedUrl, err := RedactBasicAuthURL(urlIn, true)
1✔
88
        if err != nil {
1✔
89
                WarnfCtx(ctx, "%v", err)
×
90
                return ""
×
91
        }
×
92
        return redactedUrl
1✔
93
}
94

95
func RedactBasicAuthURL(urlIn string, passwordOnly bool) (string, error) {
1✔
96
        urlParsed, err := url.Parse(urlIn)
1✔
97
        if err != nil {
2✔
98
                // err can't be wrapped or logged as it contains unredacted data from the provided url
1✔
99
                return "", fmt.Errorf("unable to redact URL, returning empty string")
1✔
100
        }
1✔
101
        if urlParsed.User != nil {
2✔
102
                user := urlParsed.User.Username()
1✔
103
                if !passwordOnly {
2✔
104
                        user = RedactedStr
1✔
105
                }
1✔
106
                urlParsed.User = url.UserPassword(user, RedactedStr)
1✔
107
        }
108

109
        return urlParsed.String(), nil
1✔
110
}
111

112
// GenerateRandomSecret returns a cryptographically-secure 160-bit random number encoded as a hex string.
113
func GenerateRandomSecret() (string, error) {
1✔
114
        val, err := randCryptoHex(160)
1✔
115
        if err != nil {
1✔
116
                return "", fmt.Errorf("RNG failed, can't create password: %w", err)
×
117
        }
×
118
        return val, nil
1✔
119
}
120

121
// GenerateRandomID returns a cryptographically-secure 128-bit random number encoded as a hex string.
122
func GenerateRandomID() (string, error) {
1✔
123
        val, err := randCryptoHex(128)
1✔
124
        if err != nil {
1✔
125
                return "", fmt.Errorf("failed to generate random ID: %w", err)
×
126
        }
×
127
        return val, nil
1✔
128
}
129

130
// randCryptoHex returns a cryptographically-secure random number of length sizeBits encoded as a hex string.
131
func randCryptoHex(sizeBits int) (string, error) {
1✔
132
        if sizeBits%8 != 0 {
1✔
133
                return "", fmt.Errorf("randCryptoHex sizeBits must be a multiple of 8: %d", sizeBits)
×
134
        }
×
135

136
        b := make([]byte, sizeBits/8)
1✔
137

1✔
138
        _, err := rand.Read(b)
1✔
139
        if err != nil {
1✔
140
                return "", err
×
141
        }
×
142

143
        return fmt.Sprintf("%x", b), nil
1✔
144
}
145

146
// This is a workaround for an incompatibility between Go's JSON marshaler and CouchDB.
147
// Go parses JSON numbers into float64 type, and then when it marshals float64 to JSON it uses
148
// scientific notation if the number is more than six digits long, even if it's an integer.
149
// However, CouchDB doesn't seem to like scientific notation and throws an exception.
150
// (See <https://issues.apache.org/jira/browse/COUCHDB-1670>)
151
// Thus, this function, which walks through a JSON-compatible object and converts float64 values
152
// to int64 when possible.
153
// NOTE: This function works on generic map[string]interface{}, but *not* on types based on it,
154
// like db.Body. Thus, db.Body has a special FixJSONNumbers method -- call that instead.
155
// TODO: In Go 1.1 we will be able to use a new option in the JSON parser that converts numbers
156
// to a special number type that preserves the exact formatting.
157
func FixJSONNumbers(value interface{}) interface{} {
1✔
158
        switch value := value.(type) {
1✔
159
        case float64:
1✔
160
                var asInt int64 = int64(value)
1✔
161
                if float64(asInt) == value {
2✔
162
                        return asInt // Representable as int, so return it as such
1✔
163
                }
1✔
164
        case map[string]interface{}:
1✔
165
                for k, v := range value {
2✔
166
                        value[k] = FixJSONNumbers(v)
1✔
167
                }
1✔
168
        case []interface{}:
1✔
169
                for i, v := range value {
2✔
170
                        value[i] = FixJSONNumbers(v)
1✔
171
                }
1✔
172
        default:
1✔
173
        }
174
        return value
1✔
175
}
176

177
// Convert a JSON string, which has extra double quotes (eg, `"thing"`) into a normal string
178
// with the extra double quotes removed (eg "thing").  Normal strings will be returned as-is.
179
//
180
// `"thing"` -> "thing"
181
// "thing" -> "thing"
182
func ConvertJSONString(s string) string {
1✔
183
        var jsonString string
1✔
184
        err := JSONUnmarshal([]byte(s), &jsonString)
1✔
185
        if err != nil {
2✔
186
                return s
1✔
187
        } else {
2✔
188
                return jsonString
1✔
189
        }
1✔
190
}
191

192
// ConvertToJSONString takes a string, and returns a JSON string, with any illegal characters escaped.
193
func ConvertToJSONString(s string) string {
1✔
194
        b, _ := JSONMarshal(s)
1✔
195
        return string(b)
1✔
196
}
1✔
197

198
// Concatenates and merges multiple string arrays into one, discarding all duplicates (including
199
// duplicates within a single array.) Ordering is preserved.
200
func MergeStringArrays(arrays ...[]string) (merged []string) {
×
201
        seen := make(map[string]bool)
×
202
        for _, array := range arrays {
×
203
                for _, str := range array {
×
204
                        if !seen[str] {
×
205
                                seen[str] = true
×
206
                                merged = append(merged, str)
×
207
                        }
×
208
                }
209
        }
210
        return
×
211
}
212

213
func ToArrayOfInterface(arrayOfString []string) []interface{} {
×
214
        arrayOfInterface := make([]interface{}, len(arrayOfString))
×
215
        for i, v := range arrayOfString {
×
216
                arrayOfInterface[i] = v
×
217
        }
×
218
        return arrayOfInterface
×
219
}
220

221
func ToInt64(value interface{}) (int64, bool) {
1✔
222
        switch value := value.(type) {
1✔
223
        case int64:
×
224
                return value, true
×
225
        case float64:
1✔
226
                return int64(value), true
1✔
227
        case int:
1✔
228
                return int64(value), true
1✔
229
        case json.Number:
1✔
230
                if n, err := value.Int64(); err == nil {
2✔
231
                        return n, true
1✔
232
                }
1✔
233
        }
234
        return 0, false
1✔
235
}
236

237
func CouchbaseUrlWithAuth(serverUrl, username, password, bucketname string) (string, error) {
1✔
238

1✔
239
        // parse url and reconstruct it piece by piece
1✔
240
        u, err := url.Parse(serverUrl)
1✔
241
        if err != nil {
1✔
242
                return "", pkgerrors.WithStack(RedactErrorf("Error parsing serverUrl: %v.  Error: %v", MD(serverUrl), err))
×
243
        }
×
244

245
        userPass := bytes.Buffer{}
1✔
246
        addedUsername := false
1✔
247

1✔
248
        // do we have a username?  if so add it
1✔
249
        if username != "" {
2✔
250
                userPass.WriteString(username)
1✔
251
                addedUsername = true
1✔
252
        } else {
2✔
253
                // do we have a non-default bucket name?  if so, use that as the username
1✔
254
                if bucketname != "" && bucketname != "default" {
1✔
255
                        userPass.WriteString(bucketname)
×
256
                        addedUsername = true
×
257
                }
×
258
        }
259

260
        if addedUsername {
2✔
261
                if password != "" {
2✔
262
                        userPass.WriteString(":")
1✔
263
                        userPass.WriteString(password)
1✔
264
                }
1✔
265

266
        }
267

268
        if addedUsername {
2✔
269
                return fmt.Sprintf(
1✔
270
                        "%v://%v@%v%v",
1✔
271
                        u.Scheme,
1✔
272
                        userPass.String(),
1✔
273
                        u.Host,
1✔
274
                        u.Path,
1✔
275
                ), nil
1✔
276
        } else {
2✔
277
                // just return the original
1✔
278
                return serverUrl, nil
1✔
279
        }
1✔
280

281
}
282

283
// This transforms raw input bucket credentials (for example, from config), to input
284
// credentials expected by Couchbase server, based on a few rules
285
func TransformBucketCredentials(inputUsername, inputPassword, inputBucketname string) (username, password, bucketname string) {
1✔
286

1✔
287
        username = inputUsername
1✔
288
        password = inputPassword
1✔
289

1✔
290
        // If the username is empty then set the username to the bucketname.
1✔
291
        if inputUsername == "" {
2✔
292
                username = inputBucketname
1✔
293
        }
1✔
294

295
        // if the username is empty, then the password should be empty too
296
        if username == "" {
1✔
297
                password = ""
×
298
        }
×
299

300
        return username, password, inputBucketname
1✔
301

302
}
303

304
func IsPowerOfTwo(n uint16) bool {
×
305
        return (n & (n - 1)) == 0
×
306
}
×
307

308
// This is how Couchbase Server handles document expiration times
309
//
310
// The actual value sent may either be
311
// Unix time (number of seconds since January 1, 1970, as a 32-bit
312
// value), or a number of seconds starting from current time. In the
313
// latter case, this number of seconds may not exceed 60*60*24*30 (number
314
// of seconds in 30 days); if the number sent by a client is larger than
315
// that, the server will consider it to be real Unix time value rather
316
// than an offset from current time.
317

318
// DurationToCbsExpiry takes a ttl as a Duration and returns an int
319
// formatted as required by CBS expiry processing
320
func DurationToCbsExpiry(ttl time.Duration) uint32 {
1✔
321
        if ttl <= kMaxDeltaTtlDuration {
2✔
322
                return uint32(ttl.Seconds())
1✔
323
        } else {
2✔
324
                return uint32(time.Now().Add(ttl).Unix())
1✔
325
        }
1✔
326
}
327

328
// SecondsToCbsExpiry takes a ttl in seconds and returns an int
329
// formatted as required by CBS expiry processing
330
func SecondsToCbsExpiry(ttl int) uint32 {
1✔
331
        return DurationToCbsExpiry(time.Duration(ttl) * time.Second)
1✔
332
}
1✔
333

334
// CbsExpiryToTime takes a CBS expiry and returns as a time
335
func CbsExpiryToTime(expiry uint32) time.Time {
1✔
336
        if expiry <= kMaxDeltaTtl {
2✔
337
                return time.Now().Add(time.Duration(expiry) * time.Second)
1✔
338
        } else {
2✔
339
                return time.Unix(int64(expiry), 0)
1✔
340
        }
1✔
341
}
342

343
// CbsExpiryToDuration takes a CBS expiry and returns as a duration
344
func CbsExpiryToDuration(expiry uint32) time.Duration {
1✔
345
        if expiry <= kMaxDeltaTtl {
1✔
346
                return time.Duration(expiry) * time.Second
×
347
        } else {
1✔
348
                expiryTime := time.Unix(int64(expiry), 0)
1✔
349
                return time.Until(expiryTime)
1✔
350
        }
1✔
351
}
352

353
// ReflectExpiry attempts to convert expiry from one of the following formats to a Couchbase Server expiry value:
354
//  1. Numeric JSON values are converted to uint32 and returned as-is
355
//  2. JSON numbers are converted to uint32 and returned as-is
356
//  3. String JSON values that are numbers are converted to int32 and returned as-is
357
//  4. String JSON values that are ISO-8601 dates are converted to UNIX time and returned
358
//  5. Null JSON values return 0
359
func ReflectExpiry(rawExpiry interface{}) (*uint32, error) {
1✔
360
        switch expiry := rawExpiry.(type) {
1✔
361
        case int64:
1✔
362
                return ValidateUint32Expiry(expiry)
1✔
363
        case float64:
1✔
364
                return ValidateUint32Expiry(int64(expiry))
1✔
365
        case json.Number:
1✔
366
                // Attempt to convert to int
1✔
367
                expInt, err := expiry.Int64()
1✔
368
                if err != nil {
1✔
369
                        return nil, err
×
370
                }
×
371
                return ValidateUint32Expiry(expInt)
1✔
372
        case string:
1✔
373
                // First check if it's a numeric string
1✔
374
                expInt, err := strconv.ParseInt(expiry, 10, 32)
1✔
375
                if err == nil {
2✔
376
                        return ValidateUint32Expiry(expInt)
1✔
377
                }
1✔
378
                // Check if it's an ISO-8601 date
379
                expRFC3339, err := time.Parse(time.RFC3339, expiry)
1✔
380
                if err == nil {
2✔
381
                        return ValidateUint32Expiry(expRFC3339.Unix())
1✔
382
                } else {
2✔
383
                        return nil, pkgerrors.Wrapf(err, "Unable to parse expiry %s as either numeric or date expiry", rawExpiry)
1✔
384
                }
1✔
385
        case nil:
1✔
386
                // Leave as zero/empty expiry
1✔
387
                return nil, nil
1✔
388
        default:
1✔
389
                return nil, fmt.Errorf("Unrecognized expiry format")
1✔
390
        }
391
}
392

393
func ValidateUint32Expiry(expiry int64) (*uint32, error) {
1✔
394
        if expiry < 0 || expiry > math.MaxUint32 {
2✔
395
                return nil, fmt.Errorf("Expiry value is not within valid range: %d", expiry)
1✔
396
        }
1✔
397
        uint32Expiry := uint32(expiry)
1✔
398
        return &uint32Expiry, nil
1✔
399
}
400

401
// Needed due to https://github.com/couchbase/sync_gateway/issues/1345
402
func AddDbPathToCookie(rq *http.Request, cookie *http.Cookie) {
1✔
403

1✔
404
        // "/db/foo" -> "db/foo"
1✔
405
        urlPathWithoutLeadingSlash := strings.TrimPrefix(rq.URL.Path, "/")
1✔
406

1✔
407
        dbPath := "/"
1✔
408
        pathComponents := strings.Split(urlPathWithoutLeadingSlash, "/")
1✔
409
        if len(pathComponents) > 0 && pathComponents[0] != "" {
2✔
410
                dbPath = fmt.Sprintf("/%v", pathComponents[0])
1✔
411
        }
1✔
412
        cookie.Path = dbPath
1✔
413

414
}
415

416
// A retry sleeper is called back by the retry loop and passed
417
// the current retryCount, and should return the amount of milliseconds
418
// that the retry should sleep.
419
type RetrySleeper func(retryCount int) (shouldContinue bool, timeTosleepMs int)
420

421
// A RetryWorker encapsulates the work being done in a Retry Loop.  The shouldRetry
422
// return value determines whether the worker will retry, regardless of the err value.
423
// If the worker has exceeded it's retry attempts, then it will not be called again
424
// even if it returns shouldRetry = true.
425
type RetryWorker[T any] func() (shouldRetry bool, err error, value T)
426

427
type RetryCasWorker func() (shouldRetry bool, err error, value uint64)
428

429
type RetryTimeoutError struct {
430
        description string
431
        attempts    int
432
}
433

434
func NewRetryTimeoutError(description string, attempts int) *RetryTimeoutError {
1✔
435
        return &RetryTimeoutError{
1✔
436
                description: description,
1✔
437
                attempts:    attempts,
1✔
438
        }
1✔
439
}
1✔
440

441
func (r *RetryTimeoutError) Error() string {
1✔
442
        return fmt.Sprintf("RetryLoop for %v giving up after %v attempts", r.description, r.attempts)
1✔
443
}
1✔
444

445
func RetryLoop[T any](ctx context.Context, description string, worker RetryWorker[T], sleeper RetrySleeper) (error, T) {
1✔
446

1✔
447
        numAttempts := 1
1✔
448

1✔
449
        for {
2✔
450
                shouldRetry, err, value := worker()
1✔
451
                if !shouldRetry {
2✔
452
                        if err != nil {
2✔
453
                                return err, *new(T)
1✔
454
                        }
1✔
455
                        return nil, value
1✔
456
                }
457
                shouldContinue, sleepMs := sleeper(numAttempts)
1✔
458
                if !shouldContinue {
2✔
459
                        if err == nil {
2✔
460
                                err = NewRetryTimeoutError(description, numAttempts)
1✔
461
                        }
1✔
462
                        WarnfCtx(ctx, "RetryLoop for %v giving up after %v attempts", description, numAttempts)
1✔
463
                        return err, value
1✔
464
                }
465
                DebugfCtx(ctx, KeyAll, "RetryLoop retrying %v after %v ms.", description, sleepMs)
1✔
466

1✔
467
                select {
1✔
468
                case <-ctx.Done():
1✔
469
                        verb := "closed"
1✔
470
                        ctxErr := ctx.Err()
1✔
471
                        if errors.Is(ctxErr, context.Canceled) {
2✔
472
                                verb = "canceled"
1✔
473
                        } else if errors.Is(ctxErr, context.DeadlineExceeded) {
3✔
474
                                verb = "timed out"
1✔
475
                        }
1✔
476
                        return fmt.Errorf("Retry loop for %v %s based on context: %w", description, verb, ctxErr), *new(T)
1✔
477
                case <-time.After(time.Millisecond * time.Duration(sleepMs)):
1✔
478
                }
479

480
                numAttempts += 1
1✔
481

482
        }
483
}
484

485
// A version of RetryLoop that returns a strongly typed cas as uint64, to avoid interface conversion overhead for
486
// high throughput operations.
487
func RetryLoopCas(ctx context.Context, description string, worker RetryCasWorker, sleeper RetrySleeper) (error, uint64) {
×
488

×
489
        numAttempts := 1
×
490

×
491
        for {
×
492
                shouldRetry, err, value := worker()
×
493
                if !shouldRetry {
×
494
                        if err != nil {
×
495
                                return err, value
×
496
                        }
×
497
                        return nil, value
×
498
                }
499
                shouldContinue, sleepMs := sleeper(numAttempts)
×
500
                if !shouldContinue {
×
501
                        if err == nil {
×
502
                                err = NewRetryTimeoutError(description, numAttempts)
×
503
                        }
×
504
                        WarnfCtx(ctx, "RetryLoopCas for %v giving up after %v attempts", description, numAttempts)
×
505
                        return err, value
×
506
                }
507
                DebugfCtx(ctx, KeyAll, "RetryLoopCas retrying %v after %v ms.", description, sleepMs)
×
508

×
509
                <-time.After(time.Millisecond * time.Duration(sleepMs))
×
510

×
511
                numAttempts += 1
×
512

513
        }
514
}
515

516
// SleeperFuncCtx wraps the given RetrySleeper with a context, so it can be cancelled, or have a deadline.
517
func SleeperFuncCtx(sleeperFunc RetrySleeper, ctx context.Context) RetrySleeper {
1✔
518
        return func(retryCount int) (bool, int) {
2✔
519
                if err := ctx.Err(); err != nil {
1✔
520
                        return false, -1
×
521
                }
×
522
                return sleeperFunc(retryCount)
1✔
523
        }
524
}
525

526
// Create a RetrySleeper that will double the retry time on every iteration and
527
// use the given parameters.
528
// The longest wait time can be calculated with: initialTimeToSleepMs * 2^maxNumAttempts
529
// The total wait time can be calculated with: initialTimeToSleepMs * 2^maxNumAttempts+1
530
func CreateDoublingSleeperFunc(maxNumAttempts, initialTimeToSleepMs int) RetrySleeper {
1✔
531

1✔
532
        timeToSleepMs := initialTimeToSleepMs
1✔
533

1✔
534
        sleeper := func(numAttempts int) (bool, int) {
2✔
535
                if numAttempts > maxNumAttempts {
2✔
536
                        return false, -1
1✔
537
                }
1✔
538
                if numAttempts > 1 {
2✔
539
                        timeToSleepMs *= 2
1✔
540
                }
1✔
541
                return true, timeToSleepMs
1✔
542
        }
543
        return sleeper
1✔
544

545
}
546

547
// Create a RetrySleeper that will double the retry time on every iteration with
548
// initial sleep time and a total max wait no longer than maxWait
549
func CreateDoublingSleeperDurationFunc(initialTimeToSleepMs int, maxWait time.Duration) RetrySleeper {
1✔
550

1✔
551
        timeToSleepMs := initialTimeToSleepMs
1✔
552
        startTime := time.Now()
1✔
553
        sleeper := func(numAttempts int) (bool, int) {
2✔
554
                totalWait := time.Since(startTime)
1✔
555
                if totalWait > maxWait {
2✔
556
                        return false, -1
1✔
557
                }
1✔
558
                if numAttempts > 1 {
2✔
559
                        timeToSleepMs *= 2
1✔
560
                }
1✔
561
                // If next sleep time would take us past maxWait, only sleep for required amount
562
                timeRemainingMs := int((maxWait - totalWait).Milliseconds())
1✔
563
                if timeRemainingMs < timeToSleepMs {
2✔
564
                        return true, timeRemainingMs
1✔
565
                }
1✔
566
                return true, timeToSleepMs
×
567
        }
568
        return sleeper
1✔
569
}
570

571
// Create a sleeper function that sleeps up to maxNumAttempts, sleeping timeToSleepMs each attempt
572
func CreateSleeperFunc(maxNumAttempts, timeToSleepMs int) RetrySleeper {
1✔
573

1✔
574
        sleeper := func(numAttempts int) (bool, int) {
2✔
575
                if numAttempts > maxNumAttempts {
2✔
576
                        return false, -1
1✔
577
                }
1✔
578
                return true, timeToSleepMs
1✔
579
        }
580
        return sleeper
1✔
581

582
}
583

584
// Create a RetrySleeper that will double the retry time on every iteration, with each sleep not exceeding maxSleepPerRetryMs.
585
func CreateMaxDoublingSleeperFunc(maxNumAttempts, initialTimeToSleepMs int, maxSleepPerRetryMs int) RetrySleeper {
1✔
586

1✔
587
        timeToSleepMs := initialTimeToSleepMs
1✔
588

1✔
589
        sleeper := func(numAttempts int) (bool, int) {
2✔
590
                if numAttempts > maxNumAttempts {
1✔
591
                        return false, -1
×
592
                }
×
593
                if numAttempts > 1 {
2✔
594
                        timeToSleepMs *= 2
1✔
595
                        if timeToSleepMs > maxSleepPerRetryMs {
2✔
596
                                timeToSleepMs = maxSleepPerRetryMs
1✔
597
                        }
1✔
598
                }
599
                return true, timeToSleepMs
1✔
600
        }
601
        return sleeper
1✔
602

603
}
604

605
// CreateIndefiniteMaxDoublingSleeperFunc is similar to CreateMaxDoublingSleeperFunc, with the exception that there is no number of maximum retries.
606
func CreateIndefiniteMaxDoublingSleeperFunc(initialTimeToSleepMs int, maxSleepPerRetryMs int) RetrySleeper {
1✔
607
        timeToSleepMs := initialTimeToSleepMs
1✔
608

1✔
609
        sleeper := func(numAttempts int) (bool, int) {
2✔
610
                timeToSleepMs *= 2
1✔
611
                if timeToSleepMs > maxSleepPerRetryMs {
2✔
612
                        timeToSleepMs = maxSleepPerRetryMs
1✔
613
                }
1✔
614
                return true, timeToSleepMs
1✔
615
        }
616

617
        return sleeper
1✔
618
}
619

620
// CreateFastFailRetrySleeperFunc returns a retry sleeper that will not retry.
621
func CreateFastFailRetrySleeperFunc() RetrySleeper {
1✔
622
        return func(retryCount int) (bool, int) {
2✔
623
                return false, -1
1✔
624
        }
1✔
625
}
626

627
// SortedUint64Slice attaches the methods of sort.Interface to []uint64, sorting in increasing order.
628
type SortedUint64Slice []uint64
629

630
func (s SortedUint64Slice) Len() int           { return len(s) }
1✔
631
func (s SortedUint64Slice) Less(i, j int) bool { return s[i] < s[j] }
1✔
632
func (s SortedUint64Slice) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
1✔
633

634
// Sort is a convenience method.
635
func (s SortedUint64Slice) Sort() {
1✔
636
        sort.Sort(s)
1✔
637
}
1✔
638

639
func WriteHistogram(ctx context.Context, expvarMap *expvar.Map, since time.Time, prefix string) {
×
640
        WriteHistogramForDuration(ctx, expvarMap, time.Since(since), prefix)
×
641
}
×
642

643
func WriteHistogramForDuration(ctx context.Context, expvarMap *expvar.Map, duration time.Duration, prefix string) {
×
644

×
645
        if LogDebugEnabled(ctx, KeyAll) {
×
646
                var durationMs int
×
647
                if duration < 1*time.Second {
×
648
                        durationMs = int(duration/(100*time.Millisecond)) * 100
×
649
                } else {
×
650
                        durationMs = int(duration/(1000*time.Millisecond)) * 1000
×
651
                }
×
652
                expvarMap.Add(fmt.Sprintf("%s-%06dms", prefix, durationMs), 1)
×
653
        }
654
}
655

656
/*
657
 * Returns a URL formatted string which excludes the path, query and fragment
658
 * This is used by _replicate to split the single URL passed in a CouchDB style
659
 * request into a source URL and a database name as used in sg_replicate
660
 */
661
func SyncSourceFromURL(u *url.URL) string {
1✔
662
        var buf bytes.Buffer
1✔
663
        if u.Scheme != "" {
2✔
664
                buf.WriteString(u.Scheme)
1✔
665
                buf.WriteByte(':')
1✔
666
        }
1✔
667
        if u.Scheme != "" || u.Host != "" || u.User != nil {
2✔
668
                buf.WriteString("//")
1✔
669
                if ui := u.User; ui != nil {
1✔
670
                        buf.WriteString(ui.String())
×
671
                        buf.WriteByte('@')
×
672
                }
×
673
                if h := u.Host; h != "" {
2✔
674
                        buf.WriteString(h)
1✔
675
                }
1✔
676
        }
677

678
        return buf.String()
1✔
679
}
680

681
// Convert string or array into a string array, otherwise return nil. If
682
// the input slice contains entries of mixed type, all string entries would
683
// be collected and returned as a slice and non-string entries as another.
684
func ValueToStringArray(value interface{}) ([]string, []interface{}) {
1✔
685
        var nonStrings []interface{}
1✔
686
        switch valueType := value.(type) {
1✔
687
        case string:
1✔
688
                return []string{valueType}, nil
1✔
689
        case []string:
1✔
690
                return valueType, nil
1✔
691
        case []interface{}:
1✔
692
                result := make([]string, 0, len(valueType))
1✔
693
                for _, item := range valueType {
2✔
694
                        if str, ok := item.(string); ok {
2✔
695
                                result = append(result, str)
1✔
696
                        } else {
2✔
697
                                nonStrings = append(nonStrings, item)
1✔
698
                        }
1✔
699
                }
700
                return result, nonStrings
1✔
701
        default:
1✔
702
                nonStrings = append(nonStrings, valueType)
1✔
703
                return nil, nonStrings
1✔
704
        }
705
}
706

707
// SanitizeRequestURL will return a sanitised string of the URL by:
708
// - Tagging mux path variables.
709
// - Tagging query parameters.
710
// - Replacing sensitive data from the URL query string with ******.
711
// Have to use string replacement instead of writing directly to the Values URL object, as only the URL's raw query is mutable.
712
func SanitizeRequestURL(req *http.Request, cachedQueryValues *url.Values) string {
1✔
713

1✔
714
        // Populate a cached copy of query values if nothing is passed in.
1✔
715
        if cachedQueryValues == nil {
2✔
716
                v := req.URL.Query()
1✔
717
                cachedQueryValues = &v
1✔
718
        }
1✔
719

720
        urlString := sanitizeRequestURLQueryParams(req.URL.String(), *cachedQueryValues)
1✔
721

1✔
722
        if RedactSystemData || RedactMetadata || RedactUserData {
2✔
723
                tagQueryParams(*cachedQueryValues, &urlString)
1✔
724
                tagPathVars(req, &urlString)
1✔
725
        }
1✔
726

727
        return urlString
1✔
728
}
729

730
// redactedPathVars is a lookup map of path variables to redaction types.
731
var redactedPathVars = map[string]string{
732
        "docid":   "UD",
733
        "attach":  "UD",
734
        "name":    "UD",
735
        "channel": "UD",
736

737
        // MD redaction is not yet supported.
738
        // "db":        "MD",
739
        // "newdb":     "MD",
740
        // "ddoc":      "MD",
741
        // "view":      "MD",
742
        // "sessionid": "MD",
743
}
744

745
// tagPathVars will tag all redactble path variables in the urlString for the given request.
746
func tagPathVars(req *http.Request, urlString *string) {
1✔
747
        if urlString == nil || req == nil {
1✔
748
                return
×
749
        }
×
750

751
        str := *urlString
1✔
752
        pathVars := mux.Vars(req)
1✔
753

1✔
754
        for k, v := range pathVars {
2✔
755
                switch redactedPathVars[k] {
1✔
756
                case "UD":
1✔
757
                        str = replaceLast(str, "/"+v, "/"+UD(v).Redact())
1✔
758
                case "MD":
×
759
                        str = replaceLast(str, "/"+v, "/"+MD(v).Redact())
×
760
                case "SD":
×
761
                        str = replaceLast(str, "/"+v, "/"+SD(v).Redact())
×
762
                }
763
        }
764

765
        *urlString = str
1✔
766
}
767

768
// replaceLast replaces the last instance of search in s with replacement.
769
func replaceLast(s, search, replacement string) string {
1✔
770
        idx := strings.LastIndex(s, search)
1✔
771
        if idx == -1 {
2✔
772
                return s
1✔
773
        }
1✔
774
        return s[:idx] + replacement + s[idx+len(search):]
1✔
775
}
776

777
// redactedQueryParams is a lookup map of query params to redaction types.
778
var redactedQueryParams = map[string]string{
779
        "channels": "UD", // updateChangesOptionsFromQuery, handleChanges
780
        "doc_ids":  "UD", // updateChangesOptionsFromQuery, handleChanges
781
        "startkey": "UD", // handleAllDocs
782
        "endkey":   "UD", // handleAllDocs
783

784
        // MD redaction is not yet supported.
785
        // "since":     "MD", // handleDumpChannel, updateChangesOptionsFromQuery, handleChanges
786
        // "rev":       "MD", // handleGetDoc, handlePutDoc, handleDeleteDoc, handleDelLocalDoc, handleGetAttachment, handlePutAttachment
787
        // "open_revs": "MD", // handleGetDoc
788
}
789

790
func tagQueryParams(values url.Values, urlString *string) {
1✔
791
        if urlString == nil || len(values) == 0 {
2✔
792
                return
1✔
793
        }
1✔
794

795
        str := *urlString
1✔
796
        str, _ = url.QueryUnescape(str)
1✔
797

1✔
798
        for k, vals := range values {
2✔
799
                // Query params can have more than one value (i.e: foo=bar&foo=buz)
1✔
800
                for _, v := range vals {
2✔
801
                        switch redactedQueryParams[k] {
1✔
802
                        case "UD":
1✔
803
                                str = strings.Replace(str, fmt.Sprintf("%s=%s", k, v), fmt.Sprintf("%s=%s", k, UD(v).Redact()), 1)
1✔
804
                        case "MD":
×
805
                                str = strings.Replace(str, fmt.Sprintf("%s=%s", k, v), fmt.Sprintf("%s=%s", k, MD(v).Redact()), 1)
×
806
                        case "SD":
×
807
                                str = strings.Replace(str, fmt.Sprintf("%s=%s", k, v), fmt.Sprintf("%s=%s", k, SD(v).Redact()), 1)
×
808
                        }
809
                }
810
        }
811

812
        *urlString = str
1✔
813
}
814

815
// sanitizeRequestURLQueryParams replaces sensitive data from the URL query string with ******.
816
func sanitizeRequestURLQueryParams(urlStr string, values url.Values) string {
1✔
817

1✔
818
        if urlStr == "" || len(values) == 0 {
2✔
819
                return urlStr
1✔
820
        }
1✔
821

822
        // Do a basic contains for the values we care about, to minimize performance impact on other requests.
823
        if strings.Contains(urlStr, "code=") || strings.Contains(urlStr, "token=") {
2✔
824
                // Iterate over the URL values looking for matches, and then do a string replacement of the found value
1✔
825
                // into urlString.  Need to unescapte the urlString, as the values returned by URL.Query() get unescaped.
1✔
826
                urlStr, _ = url.QueryUnescape(urlStr)
1✔
827
                for key, vals := range values {
2✔
828
                        if key == "code" || strings.Contains(key, "token") {
2✔
829
                                // In case there are multiple entries
1✔
830
                                for _, val := range vals {
2✔
831
                                        urlStr = strings.Replace(urlStr, fmt.Sprintf("%s=%s", key, val), fmt.Sprintf("%s=******", key), -1)
1✔
832
                                }
1✔
833
                        }
834
                }
835
        }
836

837
        return urlStr
1✔
838
}
839

840
// Ptr returns a pointer to the given literal.
841
// This is useful for wrapping around function calls that return a value, where you can't just use `&`.
842
func Ptr[T any](v T) *T {
1✔
843
        return &v
1✔
844
}
1✔
845

846
// ValDefault returns ifNil if val is nil, otherwise returns dereferenced value of val
847
func ValDefault[T any](val *T, ifNil T) T {
1✔
848
        if val != nil {
2✔
849
                return *val
1✔
850
        }
1✔
851
        return ifNil
1✔
852
}
853

854
// Add auth credentials to the given urls, since CBGT cannot take auth handlers in certain API calls yet
855
func ServerUrlsWithAuth(urls []string, spec BucketSpec) (urlsWithAuth []string, err error) {
×
856
        urlsWithAuth = make([]string, len(urls))
×
857
        username, password, bucketName := spec.Auth.GetCredentials()
×
858
        for i, url := range urls {
×
859
                urlWithAuth, err := CouchbaseUrlWithAuth(
×
860
                        url,
×
861
                        username,
×
862
                        password,
×
863
                        bucketName,
×
864
                )
×
865
                if err != nil {
×
866
                        return urlsWithAuth, err
×
867
                }
×
868
                urlsWithAuth[i] = urlWithAuth
×
869
        }
870
        return urlsWithAuth, nil
×
871
}
872

873
// Slice a string to be less than or equal to desiredSze
874
func StringPrefix(s string, desiredSize int) string {
1✔
875
        if len(s) <= desiredSize {
2✔
876
                return s
1✔
877
        }
1✔
878

879
        return s[:desiredSize]
×
880
}
881

882
// Retrieves a slice from a byte, but returns error (instead of panic) if range isn't contained by the slice
883
func SafeSlice(data []byte, from int, to int) ([]byte, error) {
×
884
        if from > len(data) || to > len(data) || from > to {
×
885
                return nil, fmt.Errorf("Invalid slice [%d:%d] of []byte with len %d", from, to, len(data))
×
886
        }
×
887
        return data[from:to], nil
×
888
}
889

890
// Returns string representation of an expvar, given map name and key name
891
func GetExpvarAsString(mapName string, name string) string {
1✔
892
        mapVar := expvar.Get(mapName)
1✔
893
        expvarMap, ok := mapVar.(*expvar.Map)
1✔
894
        if !ok {
2✔
895
                return ""
1✔
896
        }
1✔
897
        value := expvarMap.Get(name)
×
898
        if value != nil {
×
899
                return value.String()
×
900
        } else {
×
901
                return ""
×
902
        }
×
903
}
904

905
// Returns int representation of an expvar, given map name and key name
906
func GetExpvarAsInt(mapName string, name string) (int, error) {
1✔
907
        stringVal := GetExpvarAsString(mapName, name)
1✔
908
        if stringVal == "" {
2✔
909
                return 0, nil
1✔
910
        }
1✔
911
        return strconv.Atoi(stringVal)
×
912
}
913

914
// TODO: temporary workaround until https://issues.couchbase.com/browse/MB-27026 is implemented
915
func ExtractExpiryFromDCPMutation(rq *gomemcached.MCRequest) (expiry uint32) {
×
916
        if len(rq.Extras) < 24 {
×
917
                return 0
×
918
        }
×
919
        return binary.BigEndian.Uint32(rq.Extras[20:24])
×
920
}
921

922
func StringSliceContains(set []string, target string) bool {
1✔
923
        for _, val := range set {
2✔
924
                if val == target {
2✔
925
                        return true
1✔
926
                }
1✔
927
        }
928
        return false
1✔
929
}
930

931
func ConvertToEmptyInterfaceSlice(i interface{}) (result []interface{}, err error) {
1✔
932
        switch v := i.(type) {
1✔
933
        case []string:
1✔
934
                result = make([]interface{}, len(v))
1✔
935
                for index, value := range v {
2✔
936
                        result[index] = value
1✔
937
                }
1✔
938
                return result, nil
1✔
939
        case []interface{}:
1✔
940
                return v, nil
1✔
941
        default:
×
942
                return nil, fmt.Errorf("Unexpected type passed to ConvertToEmptyInterfaceSlice: %T", i)
×
943
        }
944

945
}
946

947
func encodeClusterVersion(major, minor int) int {
1✔
948
        return major*0x10000 + minor
1✔
949
}
1✔
950

951
func decodeClusterVersion(combined int) (major, minor int) {
1✔
952
        major = combined >> 16
1✔
953
        minor = combined - (major << 16)
1✔
954
        return major, minor
1✔
955
}
1✔
956

957
func HexCasToUint64(cas string) uint64 {
1✔
958
        casBytes, err := hex.DecodeString(strings.TrimPrefix(cas, "0x"))
1✔
959
        if err != nil || len(casBytes) != 8 {
1✔
960
                // Invalid cas - return zero
×
961
                return 0
×
962
        }
×
963

964
        return binary.LittleEndian.Uint64(casBytes[0:8])
1✔
965
}
966

967
func CasToString(cas uint64) string {
1✔
968
        return string(Uint64CASToLittleEndianHex(cas))
1✔
969
}
1✔
970

971
func Uint64CASToLittleEndianHex(cas uint64) []byte {
1✔
972
        littleEndian := make([]byte, 8)
1✔
973
        binary.LittleEndian.PutUint64(littleEndian, cas)
1✔
974
        encodedArray := make([]byte, hex.EncodedLen(8)+2)
1✔
975
        _ = hex.Encode(encodedArray[2:], littleEndian)
1✔
976
        encodedArray[0] = '0'
1✔
977
        encodedArray[1] = 'x'
1✔
978
        return encodedArray
1✔
979
}
1✔
980

981
func Crc32cHash(input []byte) uint32 {
1✔
982
        // crc32.MakeTable already ensures singleton table creation, so shouldn't need to cache.
1✔
983
        table := crc32.MakeTable(crc32.Castagnoli)
1✔
984
        return crc32.Checksum(input, table)
1✔
985
}
1✔
986

987
// Crc32cHashString returns a zero padded version of a crc32 hash to always be hexadecimal prefixed 8 character string
988
func Crc32cHashString(input []byte) string {
1✔
989
        return fmt.Sprintf("0x%08x", Crc32cHash(input))
1✔
990
}
1✔
991

992
func SplitHostPort(hostport string) (string, string, error) {
×
993
        host, port, err := net.SplitHostPort(hostport)
×
994
        if err != nil {
×
995
                return "", "", err
×
996
        }
×
997

998
        // If this is an IPv6 address, we need to rewrap it in []
999
        if strings.Contains(host, ":") {
×
1000
                host = fmt.Sprintf("[%s]", host)
×
1001
        }
×
1002

1003
        return host, port, nil
×
1004
}
1005

1006
var backquoteStringRegexp = regexp.MustCompile("`((?s).*?)[^\\\\]`")
1007

1008
// ConvertBackQuotedStrings sanitises a string containing `...`-delimited strings.
1009
// - Converts the backquotes into double-quotes
1010
// - Escapes literal backslashes, newlines or double-quotes with backslashes.
1011
func ConvertBackQuotedStrings(data []byte) []byte {
1✔
1012
        return backquoteStringRegexp.ReplaceAllFunc(data, func(b []byte) []byte {
2✔
1013

1✔
1014
                b = bytes.Replace(b, []byte(`\`), []byte(`\\`), -1)
1✔
1015
                b = bytes.Replace(b, []byte("\r"), []byte(""), -1)
1✔
1016
                b = bytes.Replace(b, []byte("\n"), []byte(`\n`), -1)
1✔
1017
                b = bytes.Replace(b, []byte("\t"), []byte(`\t`), -1)
1✔
1018
                b = bytes.Replace(b, []byte(`"`), []byte(`\"`), -1)
1✔
1019

1✔
1020
                // Replace the backquotes with double-quotes
1✔
1021
                b[0] = '"'
1✔
1022
                b[len(b)-1] = '"'
1✔
1023
                return b
1✔
1024
        })
1✔
1025
}
1026

1027
// FindPrimaryAddr returns the primary outbound IP of this machine.
1028
// This is the same as find_primary_addr in sgcollect_info.
1029
func FindPrimaryAddr() (net.IP, error) {
1✔
1030
        conn, err := net.Dial("udp", "8.8.8.8:56")
1✔
1031
        if err != nil {
1✔
1032
                return nil, err
×
1033
        }
×
1034

1035
        localAddr := conn.LocalAddr().(*net.UDPAddr)
1✔
1036
        return localAddr.IP, conn.Close()
1✔
1037
}
1038

1039
// ReplaceAll returns a string with all of the given chars replaced by new
1040
func ReplaceAll(s, chars, new string) string {
1✔
1041
        for _, r := range chars {
2✔
1042
                s = strings.Replace(s, string(r), new, -1)
1✔
1043
        }
1✔
1044
        return s
1✔
1045
}
1046

1047
// Convert an int into an *expvar.Int
1048
func ExpvarIntVal(val int) *expvar.Int {
×
1049
        value := expvar.Int{}
×
1050
        value.Set(int64(val))
×
1051
        return &value
×
1052
}
×
1053

1054
func ExpvarInt64Val(val int64) *expvar.Int {
×
1055
        value := expvar.Int{}
×
1056
        value.Set(val)
×
1057
        return &value
×
1058
}
×
1059

1060
func ExpvarUInt64Val(val uint64) *expvar.Int {
×
1061
        value := expvar.Int{}
×
1062
        if val > math.MaxInt64 {
×
1063
                value.Set(math.MaxInt64) // lossy, but expvar doesn't provide an alternative
×
1064
        } else {
×
1065
                value.Set(int64(val))
×
1066
        }
×
1067
        return &value
×
1068
}
1069

1070
// Convert a float into an *expvar.Float
1071
func ExpvarFloatVal(val float64) *expvar.Float {
×
1072
        value := expvar.Float{}
×
1073
        value.Set(val)
×
1074
        return &value
×
1075
}
×
1076

1077
// Convert an expvar.Var to an int64.  Return 0 if the expvar var is nil.
1078
func ExpvarVar2Int(ctx context.Context, expvarVar expvar.Var) int64 {
1✔
1079
        if expvarVar == nil {
2✔
1080
                return 0
1✔
1081
        }
1✔
1082
        asInt, ok := expvarVar.(*expvar.Int)
1✔
1083
        if !ok {
1✔
1084
                WarnfCtx(ctx, "ExpvarVar2Int could not convert %v to *expvar.Int", expvarVar)
×
1085
                return 0
×
1086
        }
×
1087
        return asInt.Value()
1✔
1088
}
1089

1090
// DefaultHTTPTransport returns a new HTTP Transport that copies values from http.DefaultTransport
1091
func DefaultHTTPTransport() *http.Transport {
1✔
1092
        // This type assertion will panic if http.DefaultTransport ever changes to not be a http.Transport
1✔
1093
        // We'll catch this in development/unit testing pretty quickly if it does happen.
1✔
1094
        return http.DefaultTransport.(*http.Transport).Clone()
1✔
1095
}
1✔
1096

1097
// IsFleeceDeltaError returns true if the given error originates from go-fleecedelta.
1098
func IsFleeceDeltaError(err error) bool { return errors.As(err, &FleeceDeltaError{}) }
1✔
1099

1100
// FleeceDeltaError is a typed error wrapped around any error returned from go-fleecedelta.
1101
type FleeceDeltaError struct{ e error }
1102

1103
func (e FleeceDeltaError) Error() string { return e.e.Error() }
1✔
1104
func (e FleeceDeltaError) Unwrap() error { return e.e }
×
1105

1106
func ContainsString(s []string, e string) bool {
1✔
1107
        for _, a := range s {
2✔
1108
                if a == e {
2✔
1109
                        return true
1✔
1110
                }
1✔
1111
        }
1112
        return false
1✔
1113
}
1114

1115
// AtomicBool is a bool that can be set or read atomically
1116
type AtomicBool struct {
1117
        value int32
1118
}
1119

1120
func NewAtomicBool(val bool) *AtomicBool {
1✔
1121
        v := int32(0)
1✔
1122
        if val {
1✔
1123
                v = 1
×
1124
        }
×
1125
        return &AtomicBool{value: v}
1✔
1126
}
1127

1128
func (ab *AtomicBool) Set(flag bool) {
1✔
1129
        if flag {
2✔
1130
                atomic.StoreInt32(&ab.value, 1)
1✔
1131
        } else {
2✔
1132
                atomic.StoreInt32(&ab.value, 0)
1✔
1133
        }
1✔
1134
}
1135

1136
func (ab *AtomicBool) IsTrue() bool {
1✔
1137
        return atomic.LoadInt32(&ab.value) == 1
1✔
1138
}
1✔
1139

1140
func (ab *AtomicBool) CompareAndSwap(old bool, new bool) bool {
1✔
1141
        var oldint32 int32
1✔
1142
        var newint32 int32
1✔
1143
        if old {
2✔
1144
                oldint32 = 1
1✔
1145
        }
1✔
1146
        if new {
2✔
1147
                newint32 = 1
1✔
1148
        }
1✔
1149
        return atomic.CompareAndSwapInt32(&ab.value, oldint32, newint32)
1✔
1150
}
1151

1152
// CASRetry attempts to retry CompareAndSwap for up to 1 second before returning the result.
1153
func (ab *AtomicBool) CASRetry(old bool, new bool) bool {
1✔
1154
        for i := 0; i < 100; i++ {
2✔
1155
                if ab.CompareAndSwap(old, new) {
2✔
1156
                        return true
1✔
1157
                }
1✔
1158
                time.Sleep(time.Millisecond * 10)
1✔
1159
        }
1160
        return false
1✔
1161
}
1162

1163
type AtomicInt struct {
1164
        val int64
1165
}
1166

1167
// Set sets the value of the atomic int. Callers must ensure that invocations of this are protected by a mutex or are otherwise safe to call concurrently, since value can overwrite any previous value.
1168
func (ai *AtomicInt) Set(value int64) {
1✔
1169
        atomic.StoreInt64(&ai.val, value)
1✔
1170
}
1✔
1171

1172
// SetIfMax sets the value of the atomic int to the given value if the given value is greater than the current value.
1173
func (ai *AtomicInt) SetIfMax(value int64) {
1✔
1174
        for {
2✔
1175
                cur := atomic.LoadInt64(&ai.val)
1✔
1176
                if cur >= value {
2✔
1177
                        return
1✔
1178
                }
1✔
1179

1180
                if atomic.CompareAndSwapInt64(&ai.val, cur, value) {
2✔
1181
                        return
1✔
1182
                }
1✔
1183
        }
1184
}
1185

1186
// Add adds the given value to the atomic int.
1187
func (ai *AtomicInt) Add(value int64) {
1✔
1188
        atomic.AddInt64(&ai.val, value)
1✔
1189
}
1✔
1190

1191
func (ai *AtomicInt) Value() int64 {
1✔
1192
        return atomic.LoadInt64(&ai.val)
1✔
1193
}
1✔
1194

1195
type SafeTerminator struct {
1196
        terminator       chan struct{}
1197
        terminatorClosed AtomicBool
1198
}
1199

1200
func NewSafeTerminator() *SafeTerminator {
1✔
1201
        return &SafeTerminator{
1✔
1202
                terminator: make(chan struct{}),
1✔
1203
        }
1✔
1204
}
1✔
1205

1206
func (t *SafeTerminator) IsClosed() bool {
×
1207
        return t.terminatorClosed.IsTrue()
×
1208
}
×
1209

1210
func (t *SafeTerminator) Done() <-chan struct{} {
1✔
1211
        return t.terminator
1✔
1212
}
1✔
1213

1214
func (t *SafeTerminator) Close() {
1✔
1215
        if t.terminatorClosed.CompareAndSwap(false, true) {
2✔
1216
                close(t.terminator)
1✔
1217
        }
1✔
1218
}
1219

1220
func Sha1HashString(str string, salt string) string {
1✔
1221
        h := sha1.New()
1✔
1222
        h.Write([]byte(salt + str))
1✔
1223
        hashedKey := h.Sum(nil)
1✔
1224
        return fmt.Sprintf("%x", hashedKey)
1✔
1225
}
1✔
1226

1227
// KVPair represents a single KV pair to be used in InjectJSONProperties
1228
type KVPair struct {
1229
        Key string
1230
        Val interface{}
1231
}
1232

1233
// InjectJSONProperties takes the given JSON byte slice, and for each KV pair, marshals the value and inserts into
1234
// the returned byte slice under the given key, without modifying the given byte slice.
1235
//
1236
// This has the potential to create duplicate keys, which whilst adhering to the spec, are ambiguous with how they get read...
1237
// usually "last key wins" - although there is no standardized way of handling JSON with non-unique keys.
1238
func InjectJSONProperties(b []byte, kvPairs ...KVPair) (new []byte, err error) {
1✔
1239
        if len(kvPairs) == 0 {
2✔
1240
                // noop
1✔
1241
                return b, nil
1✔
1242
        }
1✔
1243

1244
        b = bytes.TrimSpace(b)
1✔
1245

1✔
1246
        bIsJSONObject, bIsEmpty := isJSONObject(b)
1✔
1247
        if !bIsJSONObject {
2✔
1248
                return nil, errors.New("b is not a JSON object")
1✔
1249
        }
1✔
1250

1251
        kvPairsBytes := make([]KVPairBytes, len(kvPairs))
1✔
1252
        for i, kv := range kvPairs {
2✔
1253
                var valBytes []byte
1✔
1254
                var err error
1✔
1255

1✔
1256
                switch v := kv.Val.(type) {
1✔
1257
                case int:
1✔
1258
                        valBytes = []byte(strconv.FormatInt(int64(v), 10))
1✔
1259
                case int8:
×
1260
                        valBytes = []byte(strconv.FormatInt(int64(v), 10))
×
1261
                case int16:
×
1262
                        valBytes = []byte(strconv.FormatInt(int64(v), 10))
×
1263
                case int32:
×
1264
                        valBytes = []byte(strconv.FormatInt(int64(v), 10))
×
1265
                case int64:
×
1266
                        valBytes = []byte(strconv.FormatInt(v, 10))
×
1267
                case uint:
×
1268
                        valBytes = []byte(strconv.FormatUint(uint64(v), 10))
×
1269
                case uint8:
×
1270
                        valBytes = []byte(strconv.FormatUint(uint64(v), 10))
×
1271
                case uint16:
×
1272
                        valBytes = []byte(strconv.FormatUint(uint64(v), 10))
×
1273
                case uint32:
×
1274
                        valBytes = []byte(strconv.FormatUint(uint64(v), 10))
×
1275
                case uint64:
1✔
1276
                        valBytes = []byte(strconv.FormatUint(v, 10))
1✔
1277
                case bool:
1✔
1278
                        valBytes = []byte(strconv.FormatBool(v))
1✔
1279
                // case string:
1280
                // it's not safe to use strings without marshalling
1281
                // fall through to default below
1282
                default:
1✔
1283
                        valBytes, err = JSONMarshal(kv.Val)
1✔
1284
                }
1285
                if err != nil {
1✔
1286
                        return nil, err
×
1287
                }
×
1288
                kvPairsBytes[i] = KVPairBytes{Key: kv.Key, Val: valBytes}
1✔
1289
        }
1290

1291
        return injectJSONPropertyFromBytes(b, bIsEmpty, kvPairsBytes), nil
1✔
1292
}
1293

1294
// KVPairBytes represents a single KV pair to be used in InjectJSONPropertiesFromBytes
1295
type KVPairBytes struct {
1296
        Key string
1297
        Val []byte
1298
}
1299

1300
// InjectJSONPropertiesFromBytes takes the given JSON byte slice, and for each KV pair, inserts into b under the given key.
1301
//
1302
// This has the potential to create duplicate keys, which whilst adhering to the spec, are ambiguous with how they get read...
1303
// usually "last key wins" - although there is no standardized way of handling JSON with non-unique keys.
1304
func InjectJSONPropertiesFromBytes(b []byte, kvPairs ...KVPairBytes) (new []byte, err error) {
1✔
1305
        if len(kvPairs) == 0 {
1✔
1306
                // noop
×
1307
                return b, nil
×
1308
        }
×
1309

1310
        b = bytes.TrimSpace(b)
1✔
1311

1✔
1312
        bIsJSONObject, bIsEmpty := isJSONObject(b)
1✔
1313
        if !bIsJSONObject {
2✔
1314
                return nil, errors.New("b is not a JSON object")
1✔
1315
        }
1✔
1316

1317
        return injectJSONPropertyFromBytes(b, bIsEmpty, kvPairs), nil
1✔
1318
}
1319

1320
// isJSONObject checks if the given bytes are a JSON object,
1321
// and also whether it's an empty object or not.
1322
func isJSONObject(b []byte) (isJSONObject, isEmpty bool) {
1✔
1323

1✔
1324
        // Check if the byte slice starts with { and ends with }
1✔
1325
        if len(b) < 2 || b[0] != 0x7b || b[len(b)-1] != 0x7d {
2✔
1326
                return false, false
1✔
1327
        }
1✔
1328

1329
        return true, len(b) == 2
1✔
1330
}
1331

1332
// injectJSONPropertyFromBytes injects val under the given key into b.
1333
func injectJSONPropertyFromBytes(b []byte, bIsEmpty bool, kvPairs []KVPairBytes) (newJSON []byte) {
1✔
1334

1✔
1335
        newJSONLength := len(b)
1✔
1336
        for _, kv := range kvPairs {
2✔
1337
                newJSONLength += len(kv.Key) + len(kv.Val) + 4 // json overhead for comma, quoted key, and colon
1✔
1338
        }
1✔
1339
        if bIsEmpty {
2✔
1340
                // Take off the length of a comma when the starting body is empty
1✔
1341
                newJSONLength--
1✔
1342
        }
1✔
1343

1344
        // Create the new byte slice with the required capacity
1345
        newJSON = make([]byte, newJSONLength)
1✔
1346

1✔
1347
        // copy almost all of b, except the last closing brace
1✔
1348
        offset := copy(newJSON, b[0:len(b)-1])
1✔
1349

1✔
1350
        for i, kv := range kvPairs {
2✔
1351
                // if the body isn't empty, or we're not on our first value, prepend a comma before our property
1✔
1352
                if i > 0 || !bIsEmpty {
2✔
1353
                        offset += copy(newJSON[offset:], ",")
1✔
1354
                }
1✔
1355

1356
                // inject valBytes as the last property
1357
                offset += copy(newJSON[offset:], `"`+kv.Key+`":`)
1✔
1358
                offset += copy(newJSON[offset:], kv.Val)
1✔
1359
        }
1360

1361
        // closing brace
1362
        _ = copy(newJSON[offset:], "}")
1✔
1363

1✔
1364
        return newJSON
1✔
1365
}
1366

1367
// WrapJSONUnknownFieldErr wraps JSON unknown field errors with ErrUnknownField for later checking via errors.Cause
1368
func WrapJSONUnknownFieldErr(err error) error {
1✔
1369
        if err != nil && strings.Contains(err.Error(), "unknown field") {
2✔
1370
                return pkgerrors.WithMessage(ErrUnknownField, err.Error())
1✔
1371
        }
1✔
1372
        return err
1✔
1373
}
1374

1375
// UseStdlibJSON if true, uses the stdlib JSON package.
1376
// This variable is not thread-safe, and should be set only once on startup.
1377
var UseStdlibJSON bool
1378

1379
// JSONIterError is returned by the JSON wrapper functions, whenever jsoniter returns a non-nil error.
1380
type JSONIterError struct {
1381
        E error
1382
}
1383

1384
func (iterErr *JSONIterError) Error() string {
1✔
1385
        return iterErr.E.Error()
1✔
1386
}
1✔
1387

1388
// JSONDecoderI is the common interface between json.Decoder and jsoniter.Decoder
1389
type JSONDecoderI interface {
1390
        UseNumber()
1391
        DisallowUnknownFields()
1392
        Decode(v interface{}) error
1393
        Buffered() io.Reader
1394
        // Token() (json.Token, error) // Not implemented by jsoniter
1395
        More() bool
1396
}
1397

1398
// JSONEncoderI is the common interface between json.Encoder and jsoniter.Encoder
1399
type JSONEncoderI interface {
1400
        Encode(v interface{}) error
1401
        SetIndent(prefix, indent string)
1402
        SetEscapeHTML(on bool)
1403
}
1404

1405
func FatalPanicHandler() {
1✔
1406
        // Log any panics using the built-in loggers so that the stacktraces end up in SG log files before exiting.
1✔
1407
        if r := recover(); r != nil {
1✔
1408
                FatalfCtx(context.TODO(), "Unexpected panic: %v - stopping process\n%v", r, string(debug.Stack()))
×
1409
        }
×
1410
}
1411

1412
func Min[T constraints.Ordered](a, b T) T {
1✔
1413
        if a < b {
2✔
1414
                return a
1✔
1415
        }
1✔
1416
        return b
1✔
1417
}
1418

1419
func Max[T constraints.Ordered](a, b T) T {
1✔
1420
        if a > b {
2✔
1421
                return a
1✔
1422
        }
1✔
1423
        return b
1✔
1424
}
1425

1426
func MaxUint64(x, y uint64) uint64 {
×
1427
        if x > y {
×
1428
                return x
×
1429
        }
×
1430
        return y
×
1431
}
1432

1433
func DiffUint32(x, y uint32) uint32 {
1✔
1434
        if x > y {
1✔
1435
                return x - y
×
1436
        }
×
1437
        return y - x
1✔
1438
}
1439

1440
// GetRestrictedIntQuery returns the integer value of a URL query, restricted to a min and max value,
1441
// but returning 0 if missing or unparseable.  If allowZero is true, values coming in
1442
// as zero will stay zero, instead of being set to the minValue.
1443
func GetRestrictedIntQuery(values url.Values, query string, defaultValue, minValue, maxValue uint64, allowZero bool) uint64 {
1✔
1444
        return GetRestrictedIntFromString(
1✔
1445
                values.Get(query),
1✔
1446
                defaultValue,
1✔
1447
                minValue,
1✔
1448
                maxValue,
1✔
1449
                allowZero,
1✔
1450
        )
1✔
1451
}
1✔
1452

1453
func GetRestrictedIntFromString(rawValue string, defaultValue, minValue, maxValue uint64, allowZero bool) uint64 {
1✔
1454
        var value *uint64
1✔
1455
        if rawValue != "" {
2✔
1456
                intValue, err := strconv.ParseUint(rawValue, 10, 64)
1✔
1457
                if err != nil {
2✔
1458
                        value = nil
1✔
1459
                } else {
2✔
1460
                        value = &intValue
1✔
1461
                }
1✔
1462
        }
1463

1464
        return GetRestrictedInt(
1✔
1465
                value,
1✔
1466
                defaultValue,
1✔
1467
                minValue,
1✔
1468
                maxValue,
1✔
1469
                allowZero,
1✔
1470
        )
1✔
1471
}
1472

1473
func GetRestrictedInt(rawValue *uint64, defaultValue, minValue, maxValue uint64, allowZero bool) uint64 {
1✔
1474

1✔
1475
        var value uint64
1✔
1476

1✔
1477
        // Only use the defaultValue if rawValue isn't specified.
1✔
1478
        if rawValue == nil {
2✔
1479
                value = defaultValue
1✔
1480
        } else {
2✔
1481
                value = *rawValue
1✔
1482
        }
1✔
1483

1484
        // If value is zero and allowZero=true, leave value at zero rather than forcing it to the minimum value
1485
        validZero := (value == 0 && allowZero)
1✔
1486
        if value < minValue && !validZero {
2✔
1487
                value = minValue
1✔
1488
        }
1✔
1489

1490
        if value > maxValue && maxValue > 0 {
2✔
1491
                value = maxValue
1✔
1492
        }
1✔
1493

1494
        return value
1✔
1495
}
1496

1497
// GetHttpClient returns a new HTTP client with TLS certificate verification
1498
// disabled when insecureSkipVerify is true and enabled otherwise.
1499
func GetHttpClient(insecureSkipVerify bool) *http.Client {
1✔
1500
        if insecureSkipVerify {
2✔
1501
                transport := DefaultHTTPTransport()
1✔
1502
                if transport.TLSClientConfig == nil {
1✔
1503
                        transport.TLSClientConfig = new(tls.Config)
×
1504
                }
×
1505
                transport.TLSClientConfig.InsecureSkipVerify = true
1✔
1506
                return &http.Client{Transport: transport}
1✔
1507
        }
1508
        return &http.Client{}
1✔
1509
}
1510

1511
// Like GetHttpClient, and also turns off the NODELAY option on the underlying TCP socket.
1512
// This is better for WebSockets and BLIP, as it allows multiple small messages to be combined in
1513
// one IP packet instead of generating lots of tiny packets.
1514
func GetHttpClientForWebSocket(insecureSkipVerify bool) *http.Client {
1✔
1515
        transport := DefaultHTTPTransport()
1✔
1516
        dial := transport.DialContext
1✔
1517
        transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
2✔
1518
                conn, err := dial(ctx, network, addr)
1✔
1519
                if err == nil {
2✔
1520
                        turnOffNoDelay(ctx, conn)
1✔
1521
                }
1✔
1522
                return conn, err
1✔
1523
        }
1524

1525
        if insecureSkipVerify {
2✔
1526
                if transport.TLSClientConfig == nil {
1✔
1527
                        transport.TLSClientConfig = new(tls.Config)
×
1528
                }
×
1529
                transport.TLSClientConfig.InsecureSkipVerify = true
1✔
1530
        }
1531
        return &http.Client{Transport: transport}
1✔
1532
}
1533

1534
// Turns off TCP NODELAY on a TCP connection.
1535
// On success returns true; on failure logs a warning and returns false.
1536
// (There's really no reason for a caller to take note of the return value.)
1537
func turnOffNoDelay(ctx context.Context, conn net.Conn) {
1✔
1538
        netConnection := conn
1✔
1539
        var tcpConn *net.TCPConn
1✔
1540

1✔
1541
        if tlsConn, ok := netConnection.(*tls.Conn); ok {
1✔
1542
                netConnection = tlsConn.NetConn()
×
1543
        }
×
1544

1545
        if netTCPConn, isTCPConn := netConnection.(*net.TCPConn); isTCPConn {
2✔
1546
                tcpConn = netTCPConn
1✔
1547
        } else {
1✔
1548
                WarnfCtx(ctx, "Couldn't turn off NODELAY for %v: %T is not type *net.TCPConn", conn, conn)
×
1549
                return
×
1550
        }
×
1551

1552
        if err := tcpConn.SetNoDelay(false); err != nil {
1✔
1553
                WarnfCtx(ctx, "Couldn't turn off NODELAY for %v: %v", conn, err)
×
1554
        }
×
1555
}
1556

1557
// IsConnectionRefusedError returns true if the given error is due to a connection being actively refused.
1558
func IsConnectionRefusedError(err error) bool {
1✔
1559
        if err == nil {
1✔
1560
                return false
×
1561
        }
×
1562

1563
        var errorMessage string
1✔
1564
        switch runtime.GOOS {
1✔
1565
        case "linux", "darwin":
1✔
1566
                errorMessage = "connection refused"
1✔
1567
        case "windows":
×
1568
                errorMessage = "target machine actively refused"
×
1569
        }
1570

1571
        return strings.Contains(err.Error(), errorMessage)
1✔
1572
}
1573

1574
// ConfigDuration is a time.Duration that supports JSON marshalling/unmarshalling.
1575
// Underlying duration cannot be embedded due to requiring reflect access for mergo.
1576
type ConfigDuration struct {
1577
        D *time.Duration
1578
}
1579

1580
func (d *ConfigDuration) Value() time.Duration {
1✔
1581
        if d == nil || d.D == nil {
2✔
1582
                return 0
1✔
1583
        }
1✔
1584
        return *d.D
1✔
1585
}
1586

1587
// NewConfigDuration returns a *ConfigDuration from a time.Duration
1588
func NewConfigDuration(d time.Duration) *ConfigDuration {
1✔
1589
        return &ConfigDuration{D: &d}
1✔
1590
}
1✔
1591

1592
func (d ConfigDuration) MarshalJSON() ([]byte, error) {
1✔
1593
        duration := d.D
1✔
1594
        if duration == nil {
1✔
1595
                return nil, fmt.Errorf("cannot marshal nil duration")
×
1596
        }
×
1597
        return json.Marshal(duration.String())
1✔
1598
}
1599

1600
func (d *ConfigDuration) UnmarshalJSON(b []byte) error {
1✔
1601
        var v interface{}
1✔
1602
        if err := JSONUnmarshal(b, &v); err != nil {
1✔
1603
                return err
×
1604
        }
×
1605

1606
        val, ok := v.(string)
1✔
1607
        if !ok {
1✔
1608
                return fmt.Errorf("invalid duration type %T - must be string", v)
×
1609
        }
×
1610

1611
        dur, err := time.ParseDuration(val)
1✔
1612
        if err != nil {
1✔
1613
                return err
×
1614
        }
×
1615

1616
        *d = ConfigDuration{D: &dur}
1✔
1617
        return nil
1✔
1618
}
1619

1620
// TerminateAndWaitForClose will close the given terminator, and wait up to timeout for the done channel to be closed in response.
1621
func TerminateAndWaitForClose(terminator chan struct{}, done chan struct{}, timeout time.Duration) error {
1✔
1622
        if terminator == nil && done == nil {
2✔
1623
                // noop
1✔
1624
                return nil
1✔
1625
        }
1✔
1626

1627
        if terminator == nil || done == nil {
1✔
1628
                return errors.New("terminateAndWaitForClose requires both terminator and done channels")
×
1629
        }
×
1630

1631
        close(terminator)
1✔
1632
        t := time.NewTimer(timeout)
1✔
1633
        select {
1✔
1634
        case <-done:
1✔
1635
                t.Stop()
1✔
1636
        case <-t.C:
1✔
1637
                return fmt.Errorf("terminateAndWaitForClose timed out waiting for done channel after %v", timeout)
1✔
1638
        }
1639

1640
        return nil
1✔
1641
}
1642

1643
// Coalesce returns the first non-nil argument, or nil if both are nil.
1644
func Coalesce[T any](a, b *T) *T {
1✔
1645
        if a != nil {
2✔
1646
                return a
1✔
1647
        }
1✔
1648
        if b != nil {
2✔
1649
                return b
1✔
1650
        }
1✔
1651
        return nil
1✔
1652
}
1653

1654
// Coalesce cannot be used with sets because though they are pointer types, they aren't pointers.
1655

1656
// CoalesceSets returns the first non-nil argument, or nil if both are nil.
1657
func CoalesceSets(a, b Set) Set {
1✔
1658
        if a != nil {
2✔
1659
                return a
1✔
1660
        }
1✔
1661
        if b != nil {
1✔
1662
                return b
×
1663
        }
×
1664
        return nil
1✔
1665
}
1666

1667
// safeCutBefore returns the value up to the first instance of sep if it exists, and the remaining part of the string after sep.
1668
func safeCutBefore(s, sep string) (value, remainder string) {
1✔
1669
        val, after, ok := strings.Cut(s, sep)
1✔
1670
        if !ok {
2✔
1671
                return "", s
1✔
1672
        }
1✔
1673
        return val, after
1✔
1674
}
1675

1676
// safeCutAfter returns the value after the first instance of sep if it exists, and the remaining part of the string before sep.
1677
func safeCutAfter(s, sep string) (value, remainder string) {
1✔
1678
        before, val, ok := strings.Cut(s, sep)
1✔
1679
        if !ok {
2✔
1680
                return "", s
1✔
1681
        }
1✔
1682
        return val, before
1✔
1683
}
1684

1685
// AllOrNoneNil returns true if either all of its arguments are nil, or none are.
1686
func AllOrNoneNil(vals ...interface{}) bool {
1✔
1687
        nonNil := 0
1✔
1688
        for _, val := range vals {
2✔
1689
                // slow path reflect for typed nils
1✔
1690
                // see: https://codefibershq.com/blog/golang-why-nil-is-not-always-nil
1✔
1691
                if val == nil || reflect.ValueOf(val).IsNil() {
2✔
1692
                        continue
1✔
1693
                }
1694
                nonNil++
1✔
1695

1696
        }
1697
        return nonNil == 0 || nonNil == len(vals)
1✔
1698
}
1699

1700
// WaitForNoError runs the callback until it no longer returns an error.
1701
func WaitForNoError(ctx context.Context, callback func() error) error {
1✔
1702
        err, _ := RetryLoop(ctx, "wait for no error", func() (bool, error, interface{}) {
2✔
1703
                callbackErr := callback()
1✔
1704
                return callbackErr != nil, callbackErr, nil
1✔
1705
        }, CreateMaxDoublingSleeperFunc(30, 10, 1000))
1✔
1706
        return err
1✔
1707
}
1708

1709
// EfficientBytesClone will return a copy of the given bytes.
1710
// This implementation is slightly quicker than bytes.Clone and won't over-allocate (golang/go#55905)
1711
func EfficientBytesClone(b []byte) []byte {
1✔
1712
        if b == nil {
1✔
1713
                return nil
×
1714
        }
×
1715
        nb := make([]byte, len(b))
1✔
1716
        copy(nb, b)
1✔
1717
        return nb
1✔
1718
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc