package evaluation import ( "crypto/sha1" //nolint:gosec // SHA1 is cryptographically weak but we are not using it to hash any credentials "encoding/hex" "github.com/launchdarkly/go-server-sdk-evaluation/v2/internal" "github.com/launchdarkly/go-sdk-common/v3/ldattr" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ) const ( longScale = float32(0xFFFFFFFFFFFFFFF) initialHashInputBufferSize = 100 ) type bucketingFailureReason int const ( bucketingFailureInvalidAttrRef bucketingFailureReason = iota + 1 // 0 means no failure bucketingFailureContextLacksDesiredKind bucketingFailureAttributeNotFound bucketingFailureAttributeValueWrongType ) // computeBucketValue is used for rollouts and experiments in flag rules, flag fallthroughs, and segment rules-- // anywhere a rollout/experiment can be. It implements the logic in the flag evaluation spec for computing a // one-way hash from some combination of inputs related to the context and the flag or segment, and converting // that hash into a percentage represented as a floating-point value in the range [0,1]. // // The isExperiment parameter is true if this is an experiment rather than a plain rollout. Experiments can use // the seed parameter in place of the context key and flag key; rollouts cannot. Rollouts can use the attr // parameter to specify a context attribute other than the key, and can include a context's "secondary" key in // the inputs; experiments cannot. Parameters that are irrelevant in either case are simply ignored. // // There are several conditions that could cause this computation to fail. The only one that causes an actual // error value to be returned is if there is an invalid attribute reference, since that indicates malformed // flag/segment data. For all other failure conditions, the method returns a zero bucket value, plus an enum // indicating the type of failure (since these may have somewhat different consequences in different areas of // evaluations). func (es *evaluationScope) computeBucketValue( isExperiment bool, seed ldvalue.OptionalInt, contextKind ldcontext.Kind, key string, attr ldattr.Ref, salt string, ) (float32, bucketingFailureReason, error) { hashInput := internal.LocalBuffer{Data: make([]byte, 0, initialHashInputBufferSize)} // As long as the total length of the append operations below doesn't exceed the initial size, // this byte slice will stay on the stack. But since some of the data we're appending comes from // context attributes created by the application, we can't rule out that they will be longer than // that, in which case the buffer is reallocated automatically. if seed.IsDefined() { hashInput.AppendInt(seed.IntValue()) } else { hashInput.AppendString(key) hashInput.AppendByte('.') hashInput.AppendString(salt) } hashInput.AppendByte('.') if isExperiment || !attr.IsDefined() { // always bucket by key in an experiment attr = ldattr.NewLiteralRef(ldattr.KeyAttr) } else if attr.Err() != nil { return 0, bucketingFailureInvalidAttrRef, badAttrRefError(attr.String()) } selectedContext := es.context.IndividualContextByKind(contextKind) if !selectedContext.IsDefined() { return 0, bucketingFailureContextLacksDesiredKind, nil } uValue := selectedContext.GetValueForRef(attr) if uValue.IsNull() { // attributes can't be null, so null means it doesn't exist return 0, bucketingFailureAttributeNotFound, nil } switch { case uValue.IsString(): hashInput.AppendString(uValue.StringValue()) case uValue.IsInt(): hashInput.AppendInt(uValue.IntValue()) default: // Non-integer numbers, and values of any other JSON type, can't be used for bucketing because they have no // single reliable representation as a string. return 0, bucketingFailureAttributeValueWrongType, nil } if es.owner.enableSecondaryKey && !isExperiment { // secondary key is not supported in experiments if secondary := selectedContext.Secondary(); secondary.IsDefined() { //nolint:staticcheck // the nolint directive is because we're deliberately referencing the deprecated Secondary hashInput.AppendByte('.') hashInput.AppendString(secondary.StringValue()) } } hashOutputBytes := sha1.Sum(hashInput.Data) //nolint:gas // just used for insecure hashing hexEncodedChars := make([]byte, 64) hex.Encode(hexEncodedChars, hashOutputBytes[:]) hash := hexEncodedChars[:15] intVal, _ := internal.ParseHexUint64(hash) bucket := float32(intVal) / longScale return bucket, 0, nil }