From c838e106183d7bfa71499bcb816ac8f89502f6f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:10:41 +0000 Subject: [PATCH] fix(deps): update module buf.build/go/protovalidate to v1.3.0 --- go.mod | 4 +- go.sum | 4 + vendor/buf.build/go/protovalidate/Makefile | 15 +- vendor/buf.build/go/protovalidate/README.md | 6 + vendor/buf.build/go/protovalidate/ast.go | 7 +- vendor/buf.build/go/protovalidate/base.go | 92 ++ vendor/buf.build/go/protovalidate/builder.go | 99 ++ .../buf.build/go/protovalidate/cel/library.go | 1316 +---------------- .../go/protovalidate/internal/rules/ipv4.go | 171 +++ .../go/protovalidate/internal/rules/ipv6.go | 301 ++++ .../go/protovalidate/internal/rules/rules.go | 227 +++ .../go/protovalidate/internal/rules/uri.go | 676 +++++++++ .../buf.build/go/protovalidate/native_bool.go | 73 + .../go/protovalidate/native_bytes.go | 428 ++++++ .../buf.build/go/protovalidate/native_enum.go | 144 ++ .../buf.build/go/protovalidate/native_map.go | 114 ++ .../go/protovalidate/native_numeric.go | 660 +++++++++ .../go/protovalidate/native_repeated.go | 277 ++++ .../go/protovalidate/native_string.go | 671 +++++++++ vendor/buf.build/go/protovalidate/option.go | 14 +- .../buf.build/go/protovalidate/validator.go | 2 + vendor/buf.build/go/protovalidate/wrapper.go | 42 + .../github.com/google/cel-go/cel/BUILD.bazel | 3 + .../google/cel-go/cel/async/BUILD.bazel | 35 + .../google/cel-go/cel/async/async.go | 235 +++ vendor/github.com/google/cel-go/cel/cel.go | 21 + vendor/github.com/google/cel-go/cel/decls.go | 26 + vendor/github.com/google/cel-go/cel/env.go | 56 +- .../github.com/google/cel-go/cel/folding.go | 190 ++- vendor/github.com/google/cel-go/cel/io.go | 28 +- .../github.com/google/cel-go/cel/library.go | 40 +- .../github.com/google/cel-go/cel/options.go | 80 + .../github.com/google/cel-go/cel/program.go | 417 ++++-- vendor/github.com/google/cel-go/cel/prompt.go | 11 +- .../github.com/google/cel-go/cel/validator.go | 123 +- .../github.com/google/cel-go/checker/cost.go | 21 +- .../google/cel-go/common/ast/ast.go | 8 + .../google/cel-go/common/ast/navigable.go | 23 + .../cel-go/common/containers/container.go | 15 +- .../google/cel-go/common/decls/decls.go | 92 +- .../google/cel-go/common/env/BUILD.bazel | 4 + .../google/cel-go/common/env/env.go | 65 +- .../github.com/google/cel-go/common/env/io.go | 271 ++++ .../cel-go/common/functions/functions.go | 32 +- .../cel-go/common/overloads/overloads.go | 1 - .../google/cel-go/common/runes/buffer.go | 80 +- .../github.com/google/cel-go/common/source.go | 23 + .../google/cel-go/common/stdlib/standard.go | 20 +- .../google/cel-go/common/types/bytes.go | 5 +- .../google/cel-go/common/types/string.go | 6 +- .../google/cel-go/common/types/timestamp.go | 78 + .../google/cel-go/common/types/unknown.go | 19 +- .../github.com/google/cel-go/ext/BUILD.bazel | 4 + vendor/github.com/google/cel-go/ext/README.md | 22 + .../github.com/google/cel-go/ext/bindings.go | 52 +- vendor/github.com/google/cel-go/ext/costs.go | 122 ++ .../github.com/google/cel-go/ext/encoders.go | 141 +- vendor/github.com/google/cel-go/ext/lists.go | 336 ++++- vendor/github.com/google/cel-go/ext/math.go | 61 +- vendor/github.com/google/cel-go/ext/native.go | 23 +- .../github.com/google/cel-go/ext/network.go | 810 ++++++++++ vendor/github.com/google/cel-go/ext/regex.go | 52 +- vendor/github.com/google/cel-go/ext/sets.go | 28 +- .../github.com/google/cel-go/ext/strings.go | 302 +++- .../google/cel-go/interpreter/BUILD.bazel | 5 + .../google/cel-go/interpreter/activation.go | 38 +- .../google/cel-go/interpreter/async.go | 530 +++++++ .../cel-go/interpreter/attribute_patterns.go | 3 + .../google/cel-go/interpreter/attributes.go | 26 +- .../google/cel-go/interpreter/decorators.go | 36 +- .../google/cel-go/interpreter/frame.go | 445 ++++++ .../cel-go/interpreter/interpretable.go | 623 +++++--- .../google/cel-go/interpreter/interpreter.go | 125 +- .../google/cel-go/interpreter/planner.go | 84 +- .../google/cel-go/interpreter/runtimecost.go | 133 +- .../github.com/google/cel-go/parser/helper.go | 4 + .../google/cel-go/parser/options.go | 13 + .../github.com/google/cel-go/parser/parser.go | 18 + .../google/cel-go/parser/unparser.go | 2 +- vendor/modules.txt | 8 +- 80 files changed, 9281 insertions(+), 2136 deletions(-) create mode 100644 vendor/buf.build/go/protovalidate/internal/rules/ipv4.go create mode 100644 vendor/buf.build/go/protovalidate/internal/rules/ipv6.go create mode 100644 vendor/buf.build/go/protovalidate/internal/rules/rules.go create mode 100644 vendor/buf.build/go/protovalidate/internal/rules/uri.go create mode 100644 vendor/buf.build/go/protovalidate/native_bool.go create mode 100644 vendor/buf.build/go/protovalidate/native_bytes.go create mode 100644 vendor/buf.build/go/protovalidate/native_enum.go create mode 100644 vendor/buf.build/go/protovalidate/native_map.go create mode 100644 vendor/buf.build/go/protovalidate/native_numeric.go create mode 100644 vendor/buf.build/go/protovalidate/native_repeated.go create mode 100644 vendor/buf.build/go/protovalidate/native_string.go create mode 100644 vendor/buf.build/go/protovalidate/wrapper.go create mode 100644 vendor/github.com/google/cel-go/cel/async/BUILD.bazel create mode 100644 vendor/github.com/google/cel-go/cel/async/async.go create mode 100644 vendor/github.com/google/cel-go/common/env/io.go create mode 100644 vendor/github.com/google/cel-go/ext/costs.go create mode 100644 vendor/github.com/google/cel-go/ext/network.go create mode 100644 vendor/github.com/google/cel-go/interpreter/async.go create mode 100644 vendor/github.com/google/cel-go/interpreter/frame.go diff --git a/go.mod b/go.mod index 7198ee3e..24080e4f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 - buf.build/go/protovalidate v1.2.0 + buf.build/go/protovalidate v1.3.0 github.com/Oudwins/zog v0.22.2 github.com/amacneil/dbmate/v2 v2.34.1 github.com/aws/aws-sdk-go-v2 v1.43.4 @@ -122,7 +122,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/google/cel-go v0.28.0 // indirect + github.com/google/cel-go v0.30.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect diff --git a/go.sum b/go.sum index 6b5951f8..d4cc4bbd 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-202607092007 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= +buf.build/go/protovalidate v1.3.0 h1:8ITcnZGkAHx6TyhZvro+iET/AyqU8gEWQJK2WsT62ms= +buf.build/go/protovalidate v1.3.0/go.mod h1:82s5g+rFRj1CZPiLv6OTA31jBu2fpq7mLXHwa9mZfEs= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -220,6 +222,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= +github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/vendor/buf.build/go/protovalidate/Makefile b/vendor/buf.build/go/protovalidate/Makefile index 39caf13d..2cdb74c7 100644 --- a/vendor/buf.build/go/protovalidate/Makefile +++ b/vendor/buf.build/go/protovalidate/Makefile @@ -18,6 +18,8 @@ GOLANGCI_LINT_VERSION ?= v2.9.0 # Should be kept in sync with the version referenced in buf.yaml and # 'buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go' in go.mod. CONFORMANCE_VERSION ?= v1.2.0 +LICENSE_IGNORE := -e .github/ -e .golangci.yml -e buf.gen.yaml -e buf.yaml -e conformance/expected_failures.yaml +BUF_VERSION := 1.69.0 .PHONY: help help: ## Describe useful make targets @@ -32,7 +34,7 @@ clean: ## Delete intermediate build artifacts git clean -Xdf .PHONY: test -test: ## Run all unit tests +test: ## Run all unit tests with and without native rules $(GO) test -race -cover ./... .PHONY: test-opaque @@ -59,6 +61,7 @@ lint-fix: .PHONY: conformance conformance: $(BIN)/protovalidate-conformance protovalidate-conformance-go ## Run conformance tests $(BIN)/protovalidate-conformance $(ARGS) $(BIN)/protovalidate-conformance-go --expected_failures=conformance/expected_failures.yaml + DISABLE_NATIVE_RULES=true $(BIN)/protovalidate-conformance $(ARGS) $(BIN)/protovalidate-conformance-go --expected_failures=conformance/expected_failures.yaml .PHONY: conformance-hyperpb conformance-hyperpb: ## Run conformance tests against hyperpb @@ -79,7 +82,8 @@ generate-license: $(BIN)/license-header $(BIN)/license-header \ --license-type apache \ --copyright-holder "Buf Technologies, Inc." \ - --year-range "$(COPYRIGHT_YEARS)" + --year-range "$(COPYRIGHT_YEARS)" \ + $(LICENSE_IGNORE) .PHONY: checkgenerate checkgenerate: generate @@ -98,6 +102,9 @@ bench: $(BENCH_TMP) -count $(BENCH_COUNT) \ | tee "$(BENCH_TMP)/$(BENCH_NAME).bench.txt" +.PHONY: bench-cel +bench-cel: $(BENCH_TMP) + DISABLE_NATIVE_RULES=true $(MAKE) bench .PHONY: upgrade-go upgrade-go: @@ -110,11 +117,11 @@ $(BIN): @mkdir -p $(BIN) $(BIN)/buf: $(BIN) Makefile - GOBIN=$(abspath $(@D)) $(GO) install github.com/bufbuild/buf/cmd/buf@v1.67.0 + GOBIN=$(abspath $(@D)) $(GO) install github.com/bufbuild/buf/cmd/buf@v$(BUF_VERSION) $(BIN)/license-header: $(BIN) Makefile GOBIN=$(abspath $(@D)) $(GO) install \ - github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.67.0 + github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v$(BUF_VERSION) $(BIN)/golangci-lint: $(BIN) Makefile GOBIN=$(abspath $(@D)) $(GO) install \ diff --git a/vendor/buf.build/go/protovalidate/README.md b/vendor/buf.build/go/protovalidate/README.md index 47c6bcda..d0144454 100644 --- a/vendor/buf.build/go/protovalidate/README.md +++ b/vendor/buf.build/go/protovalidate/README.md @@ -65,6 +65,12 @@ Highlights for Go developers include: API documentation for Go is available on [pkg.go.dev][pkg-go]. +### Native standard validation rules +protovalidate-go provides native support for standard validation rule processing. They are enabled by default and are disabled by setting the ValidatorOption `WithDisableNativeRules`. + +We continue to validate that the native rules and the CEL rules produce identical results. The `compliance` Makefile target has been updated to run twice, +once with the native rules enabled, and once with the CEL rules enabled. + ## Additional languages and repositories Protovalidate isn't just for Go! You might be interested in sibling repositories for other languages: diff --git a/vendor/buf.build/go/protovalidate/ast.go b/vendor/buf.build/go/protovalidate/ast.go index 002e2466..d4a6c9d1 100644 --- a/vendor/buf.build/go/protovalidate/ast.go +++ b/vendor/buf.build/go/protovalidate/ast.go @@ -61,7 +61,12 @@ func (set astSet) ReduceResiduals(rules protoreflect.Message, opts ...cel.Progra residuals = append(residuals, ast) continue } - val, details, _ := program.Program.Eval(activation) + partialAct, err := cel.PartialVars(activation, cel.AttributePattern("this")) + if err != nil { + residuals = append(residuals, ast) + continue + } + val, details, _ := program.Program.Eval(partialAct) if val != nil { switch value := val.Value().(type) { case bool: diff --git a/vendor/buf.build/go/protovalidate/base.go b/vendor/buf.build/go/protovalidate/base.go index b31cdec3..7e1f0fd5 100644 --- a/vendor/buf.build/go/protovalidate/base.go +++ b/vendor/buf.build/go/protovalidate/base.go @@ -18,6 +18,7 @@ import ( "slices" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -60,6 +61,97 @@ func (b *base) rulePath(suffix *validate.FieldPath) *validate.FieldPath { return prefixRulePath(b.RulePrefix, suffix) } +// ruleSite is a compile-time pre-built bundle for a single rule site: the +// 2-element rule path suffix and the leaf descriptor. Using ruleSite with +// newViolationAt avoids re-allocating FieldPathElement proto messages on +// every violation (each FieldPathElement rebuild allocates ~4 sub-objects). +type ruleSite struct { + // pathElements is the rule path suffix, e.g. + // [FieldRules.int32 element, Int32Rules.gt element]. + // + // Safe to share across violations: only the containing *FieldPath is + // rebuilt per violation, which is what updateViolationPaths mutates. + pathElements []*validate.FieldPathElement + // desc is the leaf rule field descriptor (e.g. Int32Rules.gt); it is + // stored on the returned *Violation's RuleDescriptor field. + desc protoreflect.FieldDescriptor + + // if there are constant values for ruleID or message, specify them once so they can be reused + ruleID *string + message *string +} + +// makeRuleSite pre-builds a ruleSite for a rule at compile time. +// if the ruleID or message are NOT constant, pass in an empty string and supply it when calling newViolation. +func makeRuleSite(ruleDesc, desc protoreflect.FieldDescriptor, ruleID string, message string) ruleSite { + var ruleIDPtr *string + if ruleID != "" { + ruleIDPtr = proto.String(ruleID) + } + var messagePtr *string + if message != "" { + messagePtr = proto.String(message) + } + return ruleSite{ + pathElements: []*validate.FieldPathElement{ + fieldPathElement(ruleDesc), + fieldPathElement(desc), + }, + desc: desc, + ruleID: ruleIDPtr, + message: messagePtr, + } +} + +// newViolation constructs a Violation. +// ruleDesc is the top-level rule descriptor (e.g., FieldRules.int32), +// desc is the specific constraint descriptor (e.g., Int32Rules.gt). +// +// it uses pre-built path elements in site instead of rebuilding them each call. +func (b *base) newViolation( + site ruleSite, + ruleID string, + message string, + fieldValue protoreflect.Value, + ruleValue protoreflect.Value, +) *Violation { + ruleIDPtr := site.ruleID + if ruleIDPtr == nil { + ruleIDPtr = proto.String(ruleID) + } + messagePtr := site.message + if messagePtr == nil { + messagePtr = proto.String(message) + } + return &Violation{ + Proto: validate.Violation_builder{ + Field: b.fieldPath(), + Rule: b.rulePath(validate.FieldPath_builder{ + Elements: site.pathElements, + }.Build()), + RuleId: ruleIDPtr, + Message: messagePtr, + }.Build(), + FieldValue: fieldValue, + FieldDescriptor: b.Descriptor, + RuleValue: ruleValue, + RuleDescriptor: site.desc, + } +} + +func sliceToListValue[T any]( + msg proto.Message, + desc protoreflect.FieldDescriptor, + vals []T, + conv func(T) protoreflect.Value, +) protoreflect.Value { + list := msg.ProtoReflect().Mutable(desc).List() + for _, val := range vals { + list.Append(conv(val)) + } + return protoreflect.ValueOfList(list) +} + func prefixRulePath(prefix *validate.FieldPath, suffix *validate.FieldPath) *validate.FieldPath { if len(prefix.GetElements()) > 0 { return validate.FieldPath_builder{ diff --git a/vendor/buf.build/go/protovalidate/builder.go b/vendor/buf.build/go/protovalidate/builder.go index 36cc9a97..48330cf8 100644 --- a/vendor/buf.build/go/protovalidate/builder.go +++ b/vendor/buf.build/go/protovalidate/builder.go @@ -49,6 +49,7 @@ type builder struct { extensionTypeResolver protoregistry.ExtensionTypeResolver allowUnknownFields bool Load func(desc protoreflect.MessageDescriptor) messageEvaluator + disableNativeRules bool } // newBuilder initializes a new Builder. @@ -57,6 +58,7 @@ func newBuilder( disableLazy bool, extensionTypeResolver protoregistry.ExtensionTypeResolver, allowUnknownFields bool, + disableNativeRules bool, seedDesc ...protoreflect.MessageDescriptor, ) *builder { bldr := &builder{ @@ -64,6 +66,7 @@ func newBuilder( rules: newCache(), extensionTypeResolver: extensionTypeResolver, allowUnknownFields: allowUnknownFields, + disableNativeRules: disableNativeRules, } if disableLazy { @@ -464,6 +467,37 @@ func (bldr *builder) processStandardRules( } } + // make a copy of the rules because we're going to clear anything handled by native rules, leaving + // anything else to be handled by CEL rules. this allows us to fall back to CEL rules if there is no + // native rule (for example, when a new rule is added to the validate proto, but the native code hasn't + // been updated. we are making a copy because we don't want to modify the original rules in case they are + // reused (happens in some test cases, could happen in production code with dynamic messages). + rules = proto.CloneOf[*validate.FieldRules](rules) + + // put behind a feature flag to allow for testing. + // it's easier to follow like this, don't break it up + //nolint:nestif + if !bldr.disableNativeRules { + // Try native Go evaluators for repeated list-level rules (min_items, max_items, unique). + if fdesc.IsList() && valEval.NestedRule == nil { + if native := tryNativeRepeatedRules(newBase(valEval), rules.GetRepeated()); native != nil { + valEval.Append(native) + } + } + // Try native Go evaluators for map-level rules (min_pairs, max_pairs). + if fdesc.IsMap() && valEval.NestedRule == nil { + if native := tryNativeMapRules(newBase(valEval), rules.GetMap()); native != nil { + valEval.Append(native) + } + } + // Try native Go evaluators for known simple rules before falling back to CEL. + if !fdesc.IsMap() && !fdesc.IsList() { + if native := bldr.tryNativeRules(fdesc, rules, valEval); native != nil { + valEval.Append(native) + } + } + } + stdRules, err := bldr.rules.Build( bldr.env, fdesc, @@ -482,6 +516,71 @@ func (bldr *builder) processStandardRules( return nil } +func (bldr *builder) tryNativeRules( + fdesc protoreflect.FieldDescriptor, + rules *validate.FieldRules, + valEval *value, +) evaluator { + if rules == nil { + return nil + } + base := newBase(valEval) + var native evaluator + switch fdesc.Kind() { + case protoreflect.Int32Kind: + native = tryBuildNativeInt32Rules(base, rules.GetInt32()) + case protoreflect.Sint32Kind: + native = tryBuildNativeSint32Rules(base, rules.GetSint32()) + case protoreflect.Sfixed32Kind: + native = tryBuildNativeSfixed32Rules(base, rules.GetSfixed32()) + case protoreflect.Int64Kind: + native = tryBuildNativeInt64Rules(base, rules.GetInt64()) + case protoreflect.Sint64Kind: + native = tryBuildNativeSint64Rules(base, rules.GetSint64()) + case protoreflect.Sfixed64Kind: + native = tryBuildNativeSfixed64Rules(base, rules.GetSfixed64()) + case protoreflect.Uint32Kind: + native = tryBuildNativeUint32Rules(base, rules.GetUint32()) + case protoreflect.Fixed32Kind: + native = tryBuildNativeFixed32Rules(base, rules.GetFixed32()) + case protoreflect.Uint64Kind: + native = tryBuildNativeUint64Rules(base, rules.GetUint64()) + case protoreflect.Fixed64Kind: + native = tryBuildNativeFixed64Rules(base, rules.GetFixed64()) + case protoreflect.FloatKind: + native = tryBuildNativeFloatRules(base, rules.GetFloat()) + case protoreflect.DoubleKind: + native = tryBuildNativeDoubleRules(base, rules.GetDouble()) + case protoreflect.StringKind: + native = tryBuildNativeStringRules(base, rules.GetString()) + case protoreflect.BoolKind: + native = tryBuildNativeBoolRules(base, rules.GetBool()) + case protoreflect.EnumKind: + native = tryBuildNativeEnumRules(base, rules.GetEnum()) + case protoreflect.BytesKind: + native = tryBuildNativeBytesRules(base, rules.GetBytes()) + default: + return nil + } + if native == nil { + return nil + } + // processWrapperRules swaps in the inner "value" field as fdesc when + // building rules for a wrapper WKT (Int32Value, StringValue, ...), but + // leaves valEval.Descriptor pointing at the outer wrapper message field. + // Detect that here and wrap the native eval so it unwraps the wrapper + // message at runtime before calling val.Int()/Bytes()/etc. + if valEval.Descriptor != nil && + (valEval.Descriptor.Kind() == protoreflect.MessageKind || + valEval.Descriptor.Kind() == protoreflect.GroupKind) { + native = wrappedValueEval{ + innerField: fdesc, + inner: native, + } + } + return native +} + func (bldr *builder) processAnyRules( fdesc protoreflect.FieldDescriptor, fieldRules *validate.FieldRules, diff --git a/vendor/buf.build/go/protovalidate/cel/library.go b/vendor/buf.build/go/protovalidate/cel/library.go index e29696e3..b9f262e8 100644 --- a/vendor/buf.build/go/protovalidate/cel/library.go +++ b/vendor/buf.build/go/protovalidate/cel/library.go @@ -16,15 +16,11 @@ package cel import ( "bytes" - "errors" "math" - "regexp" - "slices" - "strconv" "strings" "sync" - "unicode/utf8" + "buf.build/go/protovalidate/internal/rules" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types" @@ -36,11 +32,6 @@ import ( "google.golang.org/protobuf/types/dynamicpb" ) -var ( - // See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -) - // NewLibrary creates a new CEL library that specifies all of the functions and // settings required by protovalidate beyond the standard definitions of the CEL // Specification: @@ -161,7 +152,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isHostname(host)) + return types.Bool(rules.IsHostname(host)) }), ), ), @@ -175,7 +166,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isEmail(addr)) + return types.Bool(rules.IsEmail(addr)) }), ), ), @@ -189,7 +180,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isIP(addr, 0)) + return types.Bool(rules.IsIP(addr, 0)) }), ), cel.MemberOverload( @@ -202,7 +193,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !aok || !vok { return types.Bool(false) } - return types.Bool(isIP(addr, vers)) + return types.Bool(rules.IsIP(addr, vers)) })), ), cel.Function("isIpPrefix", @@ -215,7 +206,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isIPPrefix(prefix, 0, false)) + return types.Bool(rules.IsIPPrefix(prefix, 0, false)) })), cel.MemberOverload( "string_int_is_ip_prefix_bool", @@ -227,7 +218,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !pok || !vok { return types.Bool(false) } - return types.Bool(isIPPrefix(prefix, vers, false)) + return types.Bool(rules.IsIPPrefix(prefix, vers, false)) })), cel.MemberOverload( "string_bool_is_ip_prefix_bool", @@ -239,7 +230,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !pok || !sok { return types.Bool(false) } - return types.Bool(isIPPrefix(prefix, 0, strict)) + return types.Bool(rules.IsIPPrefix(prefix, 0, strict)) })), cel.MemberOverload( "string_int_bool_is_ip_prefix_bool", @@ -252,7 +243,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !pok || !vok || !sok { return types.Bool(false) } - return types.Bool(isIPPrefix(prefix, vers, strict)) + return types.Bool(rules.IsIPPrefix(prefix, vers, strict)) })), ), cel.Function("isUri", @@ -265,7 +256,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isURI(s)) + return types.Bool(rules.IsURI(s)) }), ), ), @@ -279,7 +270,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !ok { return types.Bool(false) } - return types.Bool(isURIRef(s)) + return types.Bool(rules.IsURIRef(s)) }), ), ), @@ -379,7 +370,7 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo if !vok || !pok { return types.Bool(false) } - return types.Bool(isHostAndPort(val, portReq)) + return types.Bool(rules.IsHostAndPort(val, portReq)) }), ), ), @@ -464,41 +455,6 @@ func (l *library) uniqueBytes(list traits.Lister) ref.Val { return types.Bool(true) } -// isEmail reports whether val is an email address, for example "foo@example.com". -// -// Conforms to the definition for a valid email address from the HTML standard. -// Note that this standard willfully deviates from RFC 5322, which allows many -// unexpected forms of email addresses and will easily match a typographical -// error. -func isEmail(val string) bool { - return emailRegex.MatchString(val) -} - -// isURI reports whether val is a URI, for example "https://example.com/foo/bar?baz=quux#frag". -// -// URI is defined in the internet standard RFC 3986. -// Zone Identifiers in IPv6 address literals are supported (RFC 6874). -func isURI(val string) bool { - uri := &uri{ - str: val, - } - return uri.uri() -} - -// isURIRef reports whether val is a URI Reference - a URI such as -// "https://example.com/foo/bar?baz=quux#frag", or a Relative Reference such as -// "./foo/bar?query". -// -// URI, URI Reference, and Relative Reference are defined in the internet -// standard RFC 3986. Zone Identifiers in IPv6 address literals are supported -// (RFC 6874). -func isURIRef(val string) bool { - uri := &uri{ - str: val, - } - return uri.uriReference() -} - // RequiredEnvOptions returns the options required to have expressions which // rely on the provided descriptor. func RequiredEnvOptions(fieldDesc protoreflect.FieldDescriptor) []cel.EnvOption { @@ -516,1251 +472,3 @@ func RequiredEnvOptions(fieldDesc protoreflect.FieldDescriptor) []cel.EnvOption } return nil } - -type ipv4 struct { - str string - index int - octets []uint8 - prefixLen int64 -} - -// getBits returns the 32-bit value of an address parsed through address() or addressPrefix(). -// Returns 0 if no address was parsed successfully. -func (i *ipv4) getBits() uint32 { - if len(i.octets) != 4 { - return 0 - } - return (uint32(i.octets[0]) << 24) | (uint32(i.octets[1]) << 16) | (uint32(i.octets[2]) << 8) | uint32(i.octets[3]) -} - -// isPrefixOnly returns true if all bits to the right of the prefix-length are -// all zeros. Behavior is undefined if addressPrefix() has not been called before, -// or has returned false. -func (i *ipv4) isPrefixOnly() bool { - bits := i.getBits() - var mask uint32 - if i.prefixLen == 32 { - mask = 0xffffffff - } else { - mask = ^(0xffffffff >> i.prefixLen) - } - masked := bits & mask - return bits == masked -} - -// address parses an IPv4 Address in dotted decimal notation. -func (i *ipv4) address() bool { - return i.addressPart() && i.index == len(i.str) -} - -// addressPrefix parses an IPv4 Address prefix. -func (i *ipv4) addressPrefix() bool { - return i.addressPart() && - i.take('/') && - i.prefixLength() && - i.index == len(i.str) -} - -// prefixLength parses the length of the prefix and stores the value in prefixLen. -func (i *ipv4) prefixLength() bool { - start := i.index - for i.digit() { - if i.index-start > 2 { - // max prefix-length is 32 bits, so anything more than 2 digits is invalid - return false - } - } - str := i.str[start:i.index] - if len(str) == 0 { - // too short - return false - } - if len(str) > 1 && str[0] == '0' { - // bad leading 0 - return false - } - value, err := strconv.ParseInt(str, 0, 32) - if err != nil { - // Error converting to number - return false - } - if value > 32 { - // max 32 bits - return false - } - i.prefixLen = value - return true -} - -// addressPart parses str from the current index to determine an address part. -func (i *ipv4) addressPart() bool { - start := i.index - if i.decOctet() && - i.take('.') && - i.decOctet() && - i.take('.') && - i.decOctet() && - i.take('.') && - i.decOctet() { - return true - } - i.index = start - return false -} - -// decOctet parses str from the current index to determine a decimal octet. -func (i *ipv4) decOctet() bool { - start := i.index - for i.digit() { - if i.index-start > 3 { - // decimal octet can be three characters at most - return false - } - } - str := i.str[start:i.index] - if len(str) == 0 { - // too short - return false - } - if len(str) > 1 && str[0] == '0' { - // bad leading 0 - return false - } - value, err := strconv.ParseInt(str, 10, 32) - if err != nil { - return false - } - if value > 255 { - return false - } - i.octets = append(i.octets, byte(value)) - return true -} - -// digit parses the rule: -// -// DIGIT = %x30-39 ; 0-9 -func (i *ipv4) digit() bool { - if i.index >= len(i.str) { - return false - } - c := i.str[i.index] - if '0' <= c && c <= '9' { - i.index++ - return true - } - return false -} - -// take reports whether the current position in the string is the character char. -func (i *ipv4) take(char byte) bool { - if i.index >= len(i.str) { - return false - } - if i.str[i.index] == char { - i.index++ - return true - } - return false -} - -// newIpv4 creates a new ipv4 based on str. -func newIpv4(str string) *ipv4 { - return &ipv4{ - str: str, - } -} - -type ipv6 struct { - str string - index int - pieces []uint16 // 16-bit pieces found - doubleColonAt int // number of 16-bit pieces found when double colon was found - doubleColonSeen bool - dottedRaw string // dotted notation for right-most 32 bits - dottedAddr *ipv4 // dotted notation successfully parsed as IPv4 - zoneIDFound bool - prefixLen int64 // 0 - 128 -} - -// getBits returns the 128-bit value of an address parsed through address() or -// addressPrefix(), as a 2-tuple of 64-bit values. -// Returns [0,0] if no address was parsed successfully. -func (i *ipv6) getBits() [2]uint64 { - p16 := i.pieces - // handle dotted decimal, add to p16 - if i.dottedAddr != nil { - dotted32 := i.dottedAddr.getBits() // right-most 32 bits - p16 = append(p16, uint16(dotted32>>16)) //nolint:gosec // this is ok, we only want the high 16 bits - p16 = append(p16, uint16(dotted32)) //nolint:gosec // this is ok, we only want the low 16 bits - } - // handle double colon, fill pieces with 0 - if i.doubleColonSeen { - for len(p16) < 8 { - // delete 0 entries at pos, insert a 0 - p16 = slices.Insert(p16, i.doubleColonAt, 0x00000000) - } - } - if len(p16) != 8 { - return [2]uint64{0, 0} - } - return [2]uint64{ - (uint64(p16[0]) << 48) | (uint64(p16[1]) << 32) | (uint64(p16[2]) << 16) | uint64(p16[3]), - (uint64(p16[4]) << 48) | (uint64(p16[5]) << 32) | (uint64(p16[6]) << 16) | uint64(p16[7]), - } -} - -// isPrefixOnly returns true if all bits to the right of the prefix-length are -// all zeros. Behavior is undefined if addressPrefix() has not been called before, -// or has returned false. -func (i *ipv6) isPrefixOnly() bool { - // For each 64-bit piece of the address, require that values to the right of the prefix are zero - for idx, p64 := range i.getBits() { - size := i.prefixLen - 64*int64(idx) - var mask uint64 - if size >= 64 { //nolint:gocritic - mask = 0xFFFFFFFFFFFFFFFF - } else if size < 0 { - mask = 0x0 - } else { - mask = ^(0xFFFFFFFFFFFFFFFF >> size) - } - masked := p64 & mask - if p64 != masked { - return false - } - } - return true -} - -// address parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. -func (i *ipv6) address() bool { - return i.addressPart() && i.index == len(i.str) -} - -// addressPrefix parses an IPv6 Address Prefix following RFC 4291. Zone id is not permitted. -func (i *ipv6) addressPrefix() bool { - return i.addressPart() && - !i.zoneIDFound && - i.take('/') && - i.prefixLength() && - i.index == len(i.str) -} - -// prefixLength parses the length of the prefix and stores the value in prefixLen. -func (i *ipv6) prefixLength() bool { - start := i.index - for i.digit() { - if i.index-start > 3 { - return false - } - } - str := i.str[start:i.index] - if len(str) == 0 { - // too short - return false - } - if len(str) > 1 && str[0] == '0' { - // bad leading 0 - return false - } - value, err := strconv.ParseInt(str, 10, 32) - if err != nil { - return false - } - if value > 128 { - // max 128 bits - return false - } - i.prefixLen = value - return true -} - -// addressPart stores the dotted notation for right-most 32 bits in dottedRaw / dottedAddr if found. -func (i *ipv6) addressPart() bool { - for i.index < len(i.str) { - // dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10 - if (i.doubleColonSeen || len(i.pieces) == 6) && i.dotted() { - dotted := newIpv4(i.dottedRaw) - if dotted.address() { - i.dottedAddr = dotted - return true - } - return false - } - ok, err := i.h16() - if err != nil { - return false - } - if ok { - continue - } - if i.take(':') { //nolint:nestif - if i.take(':') { - if i.doubleColonSeen { - return false - } - i.doubleColonSeen = true - i.doubleColonAt = len(i.pieces) - if i.take(':') { - return false - } - } else if i.index == 1 || i.index == len(i.str) { - // invalid - string cannot start or end on single colon - return false - } - continue - } - if i.str[i.index] == '%' && !i.zoneID() { - return false - } - break - } - if i.doubleColonSeen { - return len(i.pieces) < 8 - } - return len(i.pieces) == 8 -} - -// zoneID parses the rule from RFC 6874: -// -// ZoneID = 1*( unreserved / pct-encoded ) -// -// There is no definition for the character set allowed in the zone -// identifier. RFC 4007 permits basically any non-null string. -func (i *ipv6) zoneID() bool { - start := i.index - if i.take('%') { - if len(i.str)-i.index > 0 { - // permit any non-null string - i.index = len(i.str) - i.zoneIDFound = true - return true - } - } - i.index = start - i.zoneIDFound = false - return false -} - -// dotted parses the rule: -// -// 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT -// -// Stores match in dottedRaw. -func (i *ipv6) dotted() bool { - start := i.index - i.dottedRaw = "" - for i.digit() || i.take('.') { - // Consume '*( DIGIT "." )' - } - if i.index-start >= 7 { - i.dottedRaw = i.str[start:i.index] - return true - } - i.index = start - return false -} - -// h16 parses the rule: -// -// h16 = 1*4HEXDIG -// -// If 1-4 hex digits are found, the parsed 16-bit unsigned integer is stored -// in pieces and true is returned. -// If 0 hex digits are found, returns false. -// If more than 4 hex digits are found, returns an error. -func (i *ipv6) h16() (bool, error) { - start := i.index - for i.hexdig() { - if i.index-start > 4 { - // too long - // this is an error condition, it means we found a string of more than - // four valid hex digits, which is invalid in ipv6 addresses. - return false, errors.New("invalid hex") - } - } - str := i.str[start:i.index] - if len(str) == 0 { - // too short, just return false - // this is not an error condition, it just means we didn't find any - // hex digits at the current position. - return false, nil - } - - value, err := strconv.ParseUint(str, 16, 16) - if err != nil { - // This is also an error condition. It means the parsed hextet we found - // cannot be converted into a number - return false, err - } - i.pieces = append(i.pieces, uint16(value)) - return true, nil -} - -// hexdig parses the rule: -// -// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" -func (i *ipv6) hexdig() bool { - if i.index >= len(i.str) { - return false - } - c := i.str[i.index] - if ('0' <= c && c <= '9') || - ('a' <= c && c <= 'f') || - ('A' <= c && c <= 'F') { - i.index++ - return true - } - return false -} - -// digit parses the rule: -// -// DIGIT = %x30-39 ; 0-9 -func (i *ipv6) digit() bool { - if i.index >= len(i.str) { - return false - } - c := i.str[i.index] - if '0' <= c && c <= '9' { - i.index++ - return true - } - return false -} - -// take reports whether the current position in the string is the character char. -func (i *ipv6) take(char byte) bool { - if i.index >= len(i.str) { - return false - } - if i.str[i.index] == char { - i.index++ - return true - } - return false -} - -// newIpv6 creates a new ipv6 based on str. -func newIpv6(str string) *ipv6 { - return &ipv6{ - str: str, - doubleColonAt: -1, - } -} - -// isIP returns true if the string is an IPv4 or IPv6 address, optionally limited to -// a specific version. -// -// Version 0 means either 4 or 6. Passing a version other than 0, 4, or 6 always -// returns false. -// -// IPv4 addresses are expected in the dotted decimal format, for example "192.168.5.21". -// IPv6 addresses are expected in their text representation, for example "::1", -// or "2001:0DB8:ABCD:0012::0". -// -// Both formats are well-defined in the internet standard RFC 3986. Zone -// identifiers for IPv6 addresses (for example "fe80::a%en1") are supported. -func isIP(str string, version int64) bool { - if version == 6 { - return newIpv6(str).address() - } - if version == 4 { - return newIpv4(str).address() - } - if version == 0 { - return newIpv4(str).address() || newIpv6(str).address() - } - return false -} - -// isIPPrefix returns true if the string is a valid IP with prefix length, optionally -// limited to a specific version (v4 or v6), and optionally requiring the host -// portion to be all zeros. -// -// An address prefix divides an IP address into a network portion, and a host -// portion. The prefix length specifies how many bits the network portion has. -// For example, the IPv6 prefix "2001:db8:abcd:0012::0/64" designates the -// left-most 64 bits as the network prefix. The range of the network is 2**64 -// addresses, from 2001:db8:abcd:0012::0 to 2001:db8:abcd:0012:ffff:ffff:ffff:ffff. -// -// An address prefix may include a specific host address, for example -// "2001:db8:abcd:0012::1f/64". With strict = true, this is not permitted. The -// host portion must be all zeros, as in "2001:db8:abcd:0012::0/64". -// -// The same principle applies to IPv4 addresses. "192.168.1.0/24" designates -// the first 24 bits of the 32-bit IPv4 as the network prefix. -func isIPPrefix( - str string, - version int64, - strict bool, -) bool { - if version == 6 { - ip := newIpv6(str) - return ip.addressPrefix() && (!strict || ip.isPrefixOnly()) - } - if version == 4 { - ip := newIpv4(str) - return ip.addressPrefix() && (!strict || ip.isPrefixOnly()) - } - if version == 0 { - return isIPPrefix(str, 6, strict) || isIPPrefix(str, 4, strict) - } - return false -} - -// isHostname returns true if the string is a valid hostname, for example "foo.example.com". -// -// A valid hostname follows the rules below: -// - The name consists of one or more labels, separated by a dot ("."). -// - Each label can be 1 to 63 alphanumeric characters. -// - A label can contain hyphens ("-"), but must not start or end with a hyphen. -// - The right-most label must not be digits only. -// - The name can have a trailing dot, for example "foo.example.com.". -// - The name can be 253 characters at most, excluding the optional trailing dot. -func isHostname(val string) bool { - if len(val) > 253 { - return false - } - var str string - if strings.HasSuffix(val, ".") { - str = val[0 : len(val)-1] - } else { - str = val - } - - allDigits := false - - // split hostname on '.' and validate each part - for part := range strings.SplitSeq(str, ".") { - allDigits = true - // if part is empty, longer than 63 chars, or starts/ends with '-', it is invalid - l := len(part) - if l == 0 || l > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") { - return false - } - // for each character in part - for i := range len(part) { - c := part[i] - // if the character is not a-z, A-Z, 0-9, or '-', it is invalid - if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' { - return false - } - allDigits = allDigits && c >= '0' && c <= '9' - } - } - // the last part cannot be all numbers - return !allDigits -} - -// isHostAndPort returns true if the string is a valid host/port pair, for example -// "example.com:8080". -// -// If the argument portRequired is true, the port is required. If the argument -// is false, the port is optional. -// -// The host can be one of: -// - An IPv4 address in dotted decimal format, for example "192.168.0.1". -// - An IPv6 address enclosed in square brackets, for example "[::1]". -// - A hostname, for example "example.com". -// -// The port is separated by a colon. It must be non-empty, with a decimal number -// in the range of 0-65535, inclusive. -func isHostAndPort(str string, portRequired bool) bool { - if len(str) == 0 { - return false - } - splitIdx := strings.LastIndex(str, ":") - if str[0] == '[' { - end := strings.LastIndex(str, "]") - switch end + 1 { - case len(str): // no port - return !portRequired && isIP(str[1:end], 6) - case splitIdx: // port - return isIP(str[1:end], 6) && isPort(str[splitIdx+1:]) - default: // malformed - return false - } - } - if splitIdx < 0 { - return !portRequired && (isHostname(str) || isIP(str, 4)) - } - host := str[0:splitIdx] - port := str[splitIdx+1:] - return (isHostname(host) || isIP(host, 4)) && isPort(port) -} - -// isPort returns true if the string is a valid port for isHostAndPort. -func isPort(str string) bool { - if len(str) == 0 { - return false - } - for i := range len(str) { - c := str[i] - if '0' <= c && c <= '9' { - continue - } - return false - } - if len(str) > 1 && str[0] == '0' { - // bad leading 0 - return false - } - val, err := strconv.ParseUint(str, 0, 32) - if err != nil { - return false - } - return val <= 65535 -} - -type uri struct { - str string - index int - pctEncodedFound bool -} - -// uri parses the rule: -// -// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] -func (u *uri) uri() bool { - start := u.index - if !(u.scheme() && u.take(':') && u.hierPart()) { - u.index = start - return false - } - if u.take('?') && !u.query() { - return false - } - if u.take('#') && !u.fragment() { - return false - } - if u.index != len(u.str) { - u.index = start - return false - } - return true -} - -// uriReference parses the rule: -// -// URI-reference = URI / relative-ref. -func (u *uri) uriReference() bool { - return u.uri() || u.relativeRef() -} - -// hierPart parses the rule: -// -// hier-part = "//" authority path-abempty. -// / path-absolute -// / path-rootless -// / path-empty. -func (u *uri) hierPart() bool { - start := u.index - if u.takeDoubleSlash() && - u.authority() && - u.pathAbempty() { - return true - } - u.index = start - return u.pathAbsolute() || u.pathRootless() || u.pathEmpty() -} - -// relativeRef parses the rule: -// -// relative-ref = relative-part [ "?" query ] [ "#" fragment ]. -func (u *uri) relativeRef() bool { - start := u.index - if !u.relativePart() { - return false - } - if u.take('?') && !u.query() { - u.index = start - return false - } - if u.take('#') && !u.fragment() { - u.index = start - return false - } - if u.index != len(u.str) { - u.index = start - return false - } - return true -} - -// relativePart parses the rule: -// -// relative-part = "//" authority path-abempty -// / path-absolute -// / path-noscheme -// / path-empty -func (u *uri) relativePart() bool { - start := u.index - if u.takeDoubleSlash() && - u.authority() && - u.pathAbempty() { - return true - } - u.index = start - return u.pathAbsolute() || u.pathNoscheme() || u.pathEmpty() -} - -// scheme parses the rule: -// -// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) -// -// Terminated by ":". -func (u *uri) scheme() bool { - start := u.index - if u.alpha() { - for u.alpha() || u.digit() || u.take('+') || u.take('-') || u.take('.') { - // Consume '*( ALPHA / DIGIT / "+" / "-" / "." )' - } - if u.peek(':') { - return true - } - } - u.index = start - return false -} - -// authority parses the rule: -// -// authority = [ userinfo "@" ] host [ ":" port ] -// -// Lead by double slash ("") and terminated by "/", "?", "#", or end of URI. -func (u *uri) authority() bool { - start := u.index - if u.userinfo() { - if !u.take('@') { - u.index = start - return false - } - } - if !u.host() { - u.index = start - return false - } - if u.take(':') { - if !u.port() { - u.index = start - return false - } - } - if !u.isAuthorityEnd() { - u.index = start - return false - } - return true -} - -// isAuthorityEnd reports whether the current position is the end of the authority. -// -// The authority component [...] is terminated by the next slash ("/"), -// question mark ("?"), or number sign ("#") character, or by the -// end of the URI. -func (u *uri) isAuthorityEnd() bool { - return u.index >= len(u.str) || - u.str[u.index] == '?' || - u.str[u.index] == '#' || - u.str[u.index] == '/' -} - -// userinfo parses the rule: -// -// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) -// -// Terminated by "@" in authority. -func (u *uri) userinfo() bool { - start := u.index - for { - if u.unreserved() || - u.pctEncoded() || - u.subDelims() || - u.take(':') { - continue - } - if u.index < len(u.str) { - if u.str[u.index] == '@' { - return true - } - } - u.index = start - return false - } -} - -// checkHostPctEncoded verifies that str is correctly percent-encoded. -func (u *uri) checkHostPctEncoded(str string) bool { - unhex := func(char byte) byte { - switch { - case '0' <= char && char <= '9': - return char - '0' - case 'a' <= char && char <= 'f': - return char - 'a' + 10 - case 'A' <= char && char <= 'F': - return char - 'A' + 10 - } - return 0 - } - escaped := make([]byte, 0, len(str)) - for i := 0; i < len(str); { - switch str[i] { - case '%': - escaped = append(escaped, unhex(str[i+1])<<4|unhex(str[i+2])) - i += 3 - default: - escaped = append(escaped, str[i]) - i++ - } - } - return utf8.Valid(escaped) -} - -// host parses the rule: -// -// host = IP-literal / IPv4address / reg-name -func (u *uri) host() bool { - start := u.index - u.pctEncodedFound = false - // Note: IPv4address is a subset of reg-name - if (u.peek('[') && u.ipLiteral()) || u.regName() { - if u.pctEncodedFound { - rawHost := u.str[start:u.index] - // RFC 3986: - // > URI producing applications must not use percent-encoding in host - // > unless it is used to represent a UTF-8 character sequence. - if !u.checkHostPctEncoded(rawHost) { - return false - } - } - return true - } - return false -} - -// port parses the rule: -// -// port = *DIGIT -// -// Terminated by end of authority. -func (u *uri) port() bool { - start := u.index - for u.digit() { - // Consume '*DIGIT' - } - if u.isAuthorityEnd() { - return true - } - u.index = start - return false -} - -// ipLiteral parses the rule from RFC 6874: -// -// IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]" -func (u *uri) ipLiteral() bool { - start := u.index - if u.take('[') { - currIdx := u.index - if u.ipv6Address() && u.take(']') { - return true - } - u.index = currIdx - if u.ipv6addrz() && u.take(']') { - return true - } - u.index = currIdx - if u.ipvFuture() && u.take(']') { - return true - } - } - u.index = start - return false -} - -// ipv6Address parses the rule "IPv6address". -// -// Relies on the implementation of isIP. -func (u *uri) ipv6Address() bool { - start := u.index - for u.hexdig() || u.take(':') { - // Consume '*( HEXDIG / ":" )' - } - if isIP(u.str[start:u.index], 6) { - return true - } - u.index = start - return false -} - -// ipv6addrz parses the rule from RFC 6874: -// -// IPv6addrz = IPv6address "%25" ZoneID -func (u *uri) ipv6addrz() bool { - start := u.index - if u.ipv6Address() && - u.take('%') && - u.take('2') && - u.take('5') && - u.zoneID() { - return true - } - u.index = start - return false -} - -// zoneID parses the rule from RFC 6874: -// -// ZoneID = 1*( unreserved / pct-encoded ) -func (u *uri) zoneID() bool { - start := u.index - for u.unreserved() || u.pctEncoded() { - // Consume '*( unreserved / pct-encoded )' - } - if u.index-start > 0 { - return true - } - u.index = start - return false -} - -// ipvFuture parses the rule: -// -// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) -func (u *uri) ipvFuture() bool { - start := u.index - if u.take('v') && u.hexdig() { - for u.hexdig() { - // Consume '*HEXDIG' - } - if u.take('.') { - counter := 0 - for u.unreserved() || u.subDelims() || u.take(':') { - counter++ - } - if counter >= 1 { - return true - } - } - } - u.index = start - return false -} - -// regName parses the rule: -// -// reg-name = *( unreserved / pct-encoded / sub-delims ) -// -// Terminates on start of port (":") or end of authority. -func (u *uri) regName() bool { - start := u.index - for { - if u.unreserved() || u.pctEncoded() || u.subDelims() { - continue - } - if u.isAuthorityEnd() { - // End of authority - return true - } - if u.str[u.index] == ':' { - return true - } - u.index = start - return false - } -} - -// isPathEnd reports whether the current position is the end of the path. -// -// The path is terminated by the first question mark ("?") or -// number sign ("#") character, or by the end of the URI. -func (u *uri) isPathEnd() bool { - return u.index >= len(u.str) || u.str[u.index] == '?' || u.str[u.index] == '#' -} - -// pathAbempty parses the rule: -// -// path-abempty = *( "/" segment ) -// -// Terminated by end of path: "?", "#", or end of URI. -func (u *uri) pathAbempty() bool { - start := u.index - for u.take('/') && u.segment() { - // Consume '*( "/" segment )' - } - if u.isPathEnd() { - return true - } - u.index = start - return false -} - -// pathAbsolute parses the rule: -// -// path-absolute = "/" [ segment-nz *( "/" segment ) ] -// -// Terminated by end of path: "?", "#", or end of URI. -func (u *uri) pathAbsolute() bool { - start := u.index - if u.take('/') { - if u.segmentNz() { - for u.take('/') && u.segment() { - // Consume '*( "/" segment )' - } - } - if u.isPathEnd() { - return true - } - } - u.index = start - return false -} - -// pathNoscheme parses the rule: -// -// path-noscheme = segment-nz-nc *( "/" segment ) -// -// Terminated by end of path: "?", "#", or end of URI. -func (u *uri) pathNoscheme() bool { - start := u.index - if u.segmentNzNc() { - for u.take('/') && u.segment() { - // Consume *( "/" segment ) - } - if u.isPathEnd() { - return true - } - } - u.index = start - return false -} - -// pathRootless parses the rule: -// -// path-rootless = segment-nz *( "/" segment ) -// -// Terminated by end of path: "?", "#", or end of URI. -func (u *uri) pathRootless() bool { - start := u.index - if u.segmentNz() { - for u.take('/') && u.segment() { - // Consume *( '/' segment ) - } - if u.isPathEnd() { - return true - } - } - u.index = start - return false -} - -// pathEmpty parses the rule: -// -// path-empty = 0 -// -// Terminated by end of path: "?", "#", or end of URI. -func (u *uri) pathEmpty() bool { - return u.isPathEnd() -} - -// segment parses the rule: -// -// segment = *pchar -func (u *uri) segment() bool { - for u.pchar() { - // Consume '*pchar' - } - return true -} - -// segmentNz parses the rule: -// -// segment-nz = 1*pchar -func (u *uri) segmentNz() bool { - start := u.index - if u.pchar() { - return u.segment() - } - u.index = start - return false -} - -// segmentNzNc parses the rule: -// -// segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) -// ; non-zero-length segment without any colon ":" -func (u *uri) segmentNzNc() bool { - start := u.index - for u.unreserved() || u.pctEncoded() || u.subDelims() || u.take('@') { - // Consume '*( unreserved / pct-encoded / sub-delims / "@" )' - } - if u.index-start > 0 { - return true - } - u.index = start - return false -} - -// pchar parses the rule: -// -// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -func (u *uri) pchar() bool { - return u.unreserved() || - u.pctEncoded() || - u.subDelims() || - u.take(':') || - u.take('@') -} - -// query parses the rule: -// -// query = *( pchar / "/" / "?" ) -// -// Terminated by "#" or end of URI. -func (u *uri) query() bool { - start := u.index - for { - if u.pchar() || u.take('/') || u.take('?') { - continue - } - if u.index == len(u.str) || u.str[u.index] == '#' { - return true - } - u.index = start - return false - } -} - -// fragment parses the rule: -// -// fragment = *( pchar / "/" / "?" ) -// -// Terminated by end of URI. -func (u *uri) fragment() bool { - start := u.index - for { - if u.pchar() || u.take('/') || u.take('?') { - continue - } - if u.index == len(u.str) { - return true - } - u.index = start - return false - } -} - -// pctEncoded parses the rule: -// -// pct-encoded = "%"+HEXDIG+HEXDIG -// -// Sets `pctEncodedFound` to true if a valid triplet was found. -func (u *uri) pctEncoded() bool { - start := u.index - if u.take('%') && u.hexdig() && u.hexdig() { - u.pctEncodedFound = true - return true - } - u.index = start - return false -} - -// unreserved parses the rule: -// -// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -func (u *uri) unreserved() bool { - return u.alpha() || - u.digit() || - u.take('-') || - u.take('_') || - u.take('.') || - u.take('~') -} - -// subDelims parses the rule: -// -// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" -// / "*" / "+" / "," / ";" / "=" -func (u *uri) subDelims() bool { - return u.take('!') || - u.take('$') || - u.take('&') || - u.take('\'') || - u.take('(') || - u.take(')') || - u.take('*') || - u.take('+') || - u.take(',') || - u.take(';') || - u.take('=') -} - -// alpha parses the rule: -// -// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z -func (u *uri) alpha() bool { - if u.index >= len(u.str) { - return false - } - c := u.str[u.index] - if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') { - u.index++ - return true - } - return false -} - -// digit parses the rule: -// -// DIGIT = %x30-39 ; 0-9 -func (u *uri) digit() bool { - if u.index >= len(u.str) { - return false - } - c := u.str[u.index] - if '0' <= c && c <= '9' { - u.index++ - return true - } - return false -} - -// hexdig parses the rule: -// -// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" -func (u *uri) hexdig() bool { - if u.index >= len(u.str) { - return false - } - c := u.str[u.index] - if ('0' <= c && c <= '9') || - ('a' <= c && c <= 'f') || - ('A' <= c && c <= 'F') { - u.index++ - return true - } - return false -} - -// take reports whether the current position in the string is the character char. -func (u *uri) take(char byte) bool { - if u.index >= len(u.str) { - return false - } - if u.str[u.index] == char { - u.index++ - return true - } - return false -} - -func (u *uri) takeDoubleSlash() bool { - first := u.take('/') - return first && u.take('/') -} - -func (u *uri) peek(char byte) bool { - return u.index < len(u.str) && u.str[u.index] == char -} diff --git a/vendor/buf.build/go/protovalidate/internal/rules/ipv4.go b/vendor/buf.build/go/protovalidate/internal/rules/ipv4.go new file mode 100644 index 00000000..32e1faf6 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/internal/rules/ipv4.go @@ -0,0 +1,171 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import "strconv" + +// newIpv4 creates a new ipv4 based on str. +func newIpv4(str string) *ipv4 { + return &ipv4{ + str: str, + } +} + +type ipv4 struct { + str string + index int + octets []uint8 + prefixLen int64 +} + +// getBits returns the 32-bit value of an address parsed through address() or addressPrefix(). +// Returns 0 if no address was parsed successfully. +func (i *ipv4) getBits() uint32 { + if len(i.octets) != 4 { + return 0 + } + return (uint32(i.octets[0]) << 24) | (uint32(i.octets[1]) << 16) | (uint32(i.octets[2]) << 8) | uint32(i.octets[3]) +} + +// isPrefixOnly returns true if all bits to the right of the prefix-length are +// all zeros. Behavior is undefined if addressPrefix() has not been called before, +// or has returned false. +func (i *ipv4) isPrefixOnly() bool { + bits := i.getBits() + var mask uint32 + if i.prefixLen == 32 { + mask = 0xffffffff + } else { + mask = ^(0xffffffff >> i.prefixLen) + } + masked := bits & mask + return bits == masked +} + +// address parses an IPv4 Address in dotted decimal notation. +func (i *ipv4) address() bool { + return i.addressPart() && i.index == len(i.str) +} + +// addressPrefix parses an IPv4 Address prefix. +func (i *ipv4) addressPrefix() bool { + return i.addressPart() && + i.take('/') && + i.prefixLength() && + i.index == len(i.str) +} + +// prefixLength parses the length of the prefix and stores the value in prefixLen. +func (i *ipv4) prefixLength() bool { + start := i.index + for i.digit() { + if i.index-start > 2 { + // max prefix-length is 32 bits, so anything more than 2 digits is invalid + return false + } + } + str := i.str[start:i.index] + if len(str) == 0 { + // too short + return false + } + if len(str) > 1 && str[0] == '0' { + // bad leading 0 + return false + } + value, err := strconv.ParseInt(str, 0, 32) + if err != nil { + // Error converting to number + return false + } + if value > 32 { + // max 32 bits + return false + } + i.prefixLen = value + return true +} + +// addressPart parses str from the current index to determine an address part. +func (i *ipv4) addressPart() bool { + start := i.index + if i.decOctet() && + i.take('.') && + i.decOctet() && + i.take('.') && + i.decOctet() && + i.take('.') && + i.decOctet() { + return true + } + i.index = start + return false +} + +// decOctet parses str from the current index to determine a decimal octet. +func (i *ipv4) decOctet() bool { + start := i.index + for i.digit() { + if i.index-start > 3 { + // decimal octet can be three characters at most + return false + } + } + str := i.str[start:i.index] + if len(str) == 0 { + // too short + return false + } + if len(str) > 1 && str[0] == '0' { + // bad leading 0 + return false + } + value, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return false + } + if value > 255 { + return false + } + i.octets = append(i.octets, byte(value)) + return true +} + +// digit parses the rule: +// +// DIGIT = %x30-39 ; 0-9 +func (i *ipv4) digit() bool { + if i.index >= len(i.str) { + return false + } + c := i.str[i.index] + if '0' <= c && c <= '9' { + i.index++ + return true + } + return false +} + +// take reports whether the current position in the string is the character char. +func (i *ipv4) take(char byte) bool { + if i.index >= len(i.str) { + return false + } + if i.str[i.index] == char { + i.index++ + return true + } + return false +} diff --git a/vendor/buf.build/go/protovalidate/internal/rules/ipv6.go b/vendor/buf.build/go/protovalidate/internal/rules/ipv6.go new file mode 100644 index 00000000..e14ff585 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/internal/rules/ipv6.go @@ -0,0 +1,301 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "errors" + "slices" + "strconv" +) + +// newIpv6 creates a new ipv6 based on str. +func newIpv6(str string) *ipv6 { + return &ipv6{ + str: str, + doubleColonAt: -1, + } +} + +type ipv6 struct { + str string + index int + pieces []uint16 // 16-bit pieces found + doubleColonAt int // number of 16-bit pieces found when double colon was found + doubleColonSeen bool + dottedRaw string // dotted notation for right-most 32 bits + dottedAddr *ipv4 // dotted notation successfully parsed as IPv4 + zoneIDFound bool + prefixLen int64 // 0 - 128 +} + +// getBits returns the 128-bit value of an address parsed through address() or +// addressPrefix(), as a 2-tuple of 64-bit values. +// Returns [0,0] if no address was parsed successfully. +func (i *ipv6) getBits() [2]uint64 { + p16 := i.pieces + // handle dotted decimal, add to p16 + if i.dottedAddr != nil { + dotted32 := i.dottedAddr.getBits() // right-most 32 bits + p16 = append(p16, uint16(dotted32>>16)) //nolint:gosec // this is ok, we only want the high 16 bits + p16 = append(p16, uint16(dotted32)) //nolint:gosec // this is ok, we only want the low 16 bits + } + // handle double colon, fill pieces with 0 + if i.doubleColonSeen { + for len(p16) < 8 { + // delete 0 entries at pos, insert a 0 + p16 = slices.Insert(p16, i.doubleColonAt, 0x00000000) + } + } + if len(p16) != 8 { + return [2]uint64{0, 0} + } + return [2]uint64{ + (uint64(p16[0]) << 48) | (uint64(p16[1]) << 32) | (uint64(p16[2]) << 16) | uint64(p16[3]), + (uint64(p16[4]) << 48) | (uint64(p16[5]) << 32) | (uint64(p16[6]) << 16) | uint64(p16[7]), + } +} + +// isPrefixOnly returns true if all bits to the right of the prefix-length are +// all zeros. Behavior is undefined if addressPrefix() has not been called before, +// or has returned false. +func (i *ipv6) isPrefixOnly() bool { + // For each 64-bit piece of the address, require that values to the right of the prefix are zero + for idx, p64 := range i.getBits() { + size := i.prefixLen - 64*int64(idx) + var mask uint64 + if size >= 64 { //nolint:gocritic + mask = 0xFFFFFFFFFFFFFFFF + } else if size < 0 { + mask = 0x0 + } else { + mask = ^(0xFFFFFFFFFFFFFFFF >> size) + } + masked := p64 & mask + if p64 != masked { + return false + } + } + return true +} + +// address parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. +func (i *ipv6) address() bool { + return i.addressPart() && i.index == len(i.str) +} + +// addressPrefix parses an IPv6 Address Prefix following RFC 4291. Zone id is not permitted. +func (i *ipv6) addressPrefix() bool { + return i.addressPart() && + !i.zoneIDFound && + i.take('/') && + i.prefixLength() && + i.index == len(i.str) +} + +// prefixLength parses the length of the prefix and stores the value in prefixLen. +func (i *ipv6) prefixLength() bool { + start := i.index + for i.digit() { + if i.index-start > 3 { + return false + } + } + str := i.str[start:i.index] + if len(str) == 0 { + // too short + return false + } + if len(str) > 1 && str[0] == '0' { + // bad leading 0 + return false + } + value, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return false + } + if value > 128 { + // max 128 bits + return false + } + i.prefixLen = value + return true +} + +// addressPart stores the dotted notation for right-most 32 bits in dottedRaw / dottedAddr if found. +func (i *ipv6) addressPart() bool { + for i.index < len(i.str) { + // dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10 + if (i.doubleColonSeen || len(i.pieces) == 6) && i.dotted() { + dotted := newIpv4(i.dottedRaw) + if dotted.address() { + i.dottedAddr = dotted + return true + } + return false + } + ok, err := i.h16() + if err != nil { + return false + } + if ok { + continue + } + if i.take(':') { //nolint:nestif + if i.take(':') { + if i.doubleColonSeen { + return false + } + i.doubleColonSeen = true + i.doubleColonAt = len(i.pieces) + if i.take(':') { + return false + } + } else if i.index == 1 || i.index == len(i.str) { + // invalid - string cannot start or end on single colon + return false + } + continue + } + if i.str[i.index] == '%' && !i.zoneID() { + return false + } + break + } + if i.doubleColonSeen { + return len(i.pieces) < 8 + } + return len(i.pieces) == 8 +} + +// zoneID parses the rule from RFC 6874: +// +// ZoneID = 1*( unreserved / pct-encoded ) +// +// There is no definition for the character set allowed in the zone +// identifier. RFC 4007 permits basically any non-null string. +func (i *ipv6) zoneID() bool { + start := i.index + if i.take('%') { + if len(i.str)-i.index > 0 { + // permit any non-null string + i.index = len(i.str) + i.zoneIDFound = true + return true + } + } + i.index = start + i.zoneIDFound = false + return false +} + +// dotted parses the rule: +// +// 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT +// +// Stores match in dottedRaw. +func (i *ipv6) dotted() bool { + start := i.index + i.dottedRaw = "" + //nolint:revive + for i.digit() || i.take('.') { + // Consume '*( DIGIT "." )' + } + if i.index-start >= 7 { + i.dottedRaw = i.str[start:i.index] + return true + } + i.index = start + return false +} + +// h16 parses the rule: +// +// h16 = 1*4HEXDIG +// +// If 1-4 hex digits are found, the parsed 16-bit unsigned integer is stored +// in pieces and true is returned. +// If 0 hex digits are found, returns false. +// If more than 4 hex digits are found, returns an error. +func (i *ipv6) h16() (bool, error) { + start := i.index + for i.hexdig() { + if i.index-start > 4 { + // too long + // this is an error condition, it means we found a string of more than + // four valid hex digits, which is invalid in ipv6 addresses. + return false, errors.New("invalid hex") + } + } + str := i.str[start:i.index] + if len(str) == 0 { + // too short, just return false + // this is not an error condition, it just means we didn't find any + // hex digits at the current position. + return false, nil + } + + value, err := strconv.ParseUint(str, 16, 16) + if err != nil { + // This is also an error condition. It means the parsed hextet we found + // cannot be converted into a number + return false, err + } + i.pieces = append(i.pieces, uint16(value)) + return true, nil +} + +// hexdig parses the rule: +// +// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +func (i *ipv6) hexdig() bool { + if i.index >= len(i.str) { + return false + } + c := i.str[i.index] + if ('0' <= c && c <= '9') || + ('a' <= c && c <= 'f') || + ('A' <= c && c <= 'F') { + i.index++ + return true + } + return false +} + +// digit parses the rule: +// +// DIGIT = %x30-39 ; 0-9 +func (i *ipv6) digit() bool { + if i.index >= len(i.str) { + return false + } + c := i.str[i.index] + if '0' <= c && c <= '9' { + i.index++ + return true + } + return false +} + +// take reports whether the current position in the string is the character char. +func (i *ipv6) take(char byte) bool { + if i.index >= len(i.str) { + return false + } + if i.str[i.index] == char { + i.index++ + return true + } + return false +} diff --git a/vendor/buf.build/go/protovalidate/internal/rules/rules.go b/vendor/buf.build/go/protovalidate/internal/rules/rules.go new file mode 100644 index 00000000..9fee9159 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/internal/rules/rules.go @@ -0,0 +1,227 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "regexp" + "strconv" + "strings" +) + +var ( + // See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") +) + +// IsEmail reports whether val is an email address, for example "foo@example.com". +// +// Conforms to the definition for a valid email address from the HTML standard. +// Note that this standard willfully deviates from RFC 5322, which allows many +// unexpected forms of email addresses and will easily match a typographical +// error. +func IsEmail(val string) bool { + return emailRegex.MatchString(val) +} + +// IsURI reports whether val is a URI, for example "https://example.com/foo/bar?baz=quux#frag". +// +// URI is defined in the internet standard RFC 3986. +// Zone Identifiers in IPv6 address literals are supported (RFC 6874). +func IsURI(val string) bool { + uri := &uri{ + str: val, + } + return uri.uri() +} + +// IsURIRef reports whether val is a URI Reference - a URI such as +// "https://example.com/foo/bar?baz=quux#frag", or a Relative Reference such as +// "./foo/bar?query". +// +// URI, URI Reference, and Relative Reference are defined in the internet +// standard RFC 3986. Zone Identifiers in IPv6 address literals are supported +// (RFC 6874). +func IsURIRef(val string) bool { + uri := &uri{ + str: val, + } + return uri.uriReference() +} + +// IsIP returns true if the string is an IPv4 or IPv6 address, optionally limited to +// a specific version. +// +// Version 0 means either 4 or 6. Passing a version other than 0, 4, or 6 always +// returns false. +// +// IPv4 addresses are expected in the dotted decimal format, for example "192.168.5.21". +// IPv6 addresses are expected in their text representation, for example "::1", +// or "2001:0DB8:ABCD:0012::0". +// +// Both formats are well-defined in the internet standard RFC 3986. Zone +// identifiers for IPv6 addresses (for example "fe80::a%en1") are supported. +func IsIP(str string, version int64) bool { + if version == 6 { + return newIpv6(str).address() + } + if version == 4 { + return newIpv4(str).address() + } + if version == 0 { + return newIpv4(str).address() || newIpv6(str).address() + } + return false +} + +// IsIPPrefix returns true if the string is a valid IP with prefix length, optionally +// limited to a specific version (v4 or v6), and optionally requiring the host +// portion to be all zeros. +// +// An address prefix divides an IP address into a network portion, and a host +// portion. The prefix length specifies how many bits the network portion has. +// For example, the IPv6 prefix "2001:db8:abcd:0012::0/64" designates the +// left-most 64 bits as the network prefix. The range of the network is 2**64 +// addresses, from 2001:db8:abcd:0012::0 to 2001:db8:abcd:0012:ffff:ffff:ffff:ffff. +// +// An address prefix may include a specific host address, for example +// "2001:db8:abcd:0012::1f/64". With strict = true, this is not permitted. The +// host portion must be all zeros, as in "2001:db8:abcd:0012::0/64". +// +// The same principle applies to IPv4 addresses. "192.168.1.0/24" designates +// the first 24 bits of the 32-bit IPv4 as the network prefix. +func IsIPPrefix( + str string, + version int64, + strict bool, +) bool { + if version == 6 { + ip := newIpv6(str) + return ip.addressPrefix() && (!strict || ip.isPrefixOnly()) + } + if version == 4 { + ip := newIpv4(str) + return ip.addressPrefix() && (!strict || ip.isPrefixOnly()) + } + if version == 0 { + return IsIPPrefix(str, 6, strict) || IsIPPrefix(str, 4, strict) + } + return false +} + +// IsHostname returns true if the string is a valid hostname, for example "foo.example.com". +// +// A valid hostname follows the rules below: +// - The name consists of one or more labels, separated by a dot ("."). +// - Each label can be 1 to 63 alphanumeric characters. +// - A label can contain hyphens ("-"), but must not start or end with a hyphen. +// - The right-most label must not be digits only. +// - The name can have a trailing dot, for example "foo.example.com.". +// - The name can be 253 characters at most, excluding the optional trailing dot. +func IsHostname(val string) bool { + var str string + if strings.HasSuffix(val, ".") { + str = val[0 : len(val)-1] + } else { + str = val + } + // The 253-character limit excludes the optional trailing dot, so measure + // after stripping it. + if len(str) > 253 { + return false + } + + allDigits := false + + // split hostname on '.' and validate each part + for part := range strings.SplitSeq(str, ".") { + allDigits = true + // if part is empty, longer than 63 chars, or starts/ends with '-', it is invalid + l := len(part) + if l == 0 || l > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") { + return false + } + // for each character in part + for i := range len(part) { + c := part[i] + // if the character is not a-z, A-Z, 0-9, or '-', it is invalid + if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' { + return false + } + allDigits = allDigits && c >= '0' && c <= '9' + } + } + // the last part cannot be all numbers + return !allDigits +} + +// IsHostAndPort returns true if the string is a valid host/port pair, for example +// "example.com:8080". +// +// If the argument portRequired is true, the port is required. If the argument +// is false, the port is optional. +// +// The host can be one of: +// - An IPv4 address in dotted decimal format, for example "192.168.0.1". +// - An IPv6 address enclosed in square brackets, for example "[::1]". +// - A hostname, for example "example.com". +// +// The port is separated by a colon. It must be non-empty, with a decimal number +// in the range of 0-65535, inclusive. +func IsHostAndPort(str string, portRequired bool) bool { + if len(str) == 0 { + return false + } + splitIdx := strings.LastIndex(str, ":") + if str[0] == '[' { + end := strings.LastIndex(str, "]") + switch end + 1 { + case len(str): // no port + return !portRequired && IsIP(str[1:end], 6) + case splitIdx: // port + return IsIP(str[1:end], 6) && isPort(str[splitIdx+1:]) + default: // malformed + return false + } + } + if splitIdx < 0 { + return !portRequired && (IsHostname(str) || IsIP(str, 4)) + } + host := str[0:splitIdx] + port := str[splitIdx+1:] + return (IsHostname(host) || IsIP(host, 4)) && isPort(port) +} + +// isPort returns true if the string is a valid port for IsHostAndPort. +func isPort(str string) bool { + if len(str) == 0 { + return false + } + for i := range len(str) { + c := str[i] + if '0' <= c && c <= '9' { + continue + } + return false + } + if len(str) > 1 && str[0] == '0' { + // bad leading 0 + return false + } + val, err := strconv.ParseUint(str, 0, 32) + if err != nil { + return false + } + return val <= 65535 +} diff --git a/vendor/buf.build/go/protovalidate/internal/rules/uri.go b/vendor/buf.build/go/protovalidate/internal/rules/uri.go new file mode 100644 index 00000000..c303ca3a --- /dev/null +++ b/vendor/buf.build/go/protovalidate/internal/rules/uri.go @@ -0,0 +1,676 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import "unicode/utf8" + +type uri struct { + str string + index int + pctEncodedFound bool +} + +// uri parses the rule: +// +// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] +func (u *uri) uri() bool { + start := u.index + if !(u.scheme() && u.take(':') && u.hierPart()) { + u.index = start + return false + } + if u.take('?') && !u.query() { + return false + } + if u.take('#') && !u.fragment() { + return false + } + if u.index != len(u.str) { + u.index = start + return false + } + return true +} + +// uriReference parses the rule: +// +// URI-reference = URI / relative-ref. +func (u *uri) uriReference() bool { + return u.uri() || u.relativeRef() +} + +// hierPart parses the rule: +// +// hier-part = "//" authority path-abempty. +// / path-absolute +// / path-rootless +// / path-empty. +func (u *uri) hierPart() bool { + start := u.index + if u.takeDoubleSlash() && + u.authority() && + u.pathAbempty() { + return true + } + u.index = start + return u.pathAbsolute() || u.pathRootless() || u.pathEmpty() +} + +// relativeRef parses the rule: +// +// relative-ref = relative-part [ "?" query ] [ "#" fragment ]. +func (u *uri) relativeRef() bool { + start := u.index + if !u.relativePart() { + return false + } + if u.take('?') && !u.query() { + u.index = start + return false + } + if u.take('#') && !u.fragment() { + u.index = start + return false + } + if u.index != len(u.str) { + u.index = start + return false + } + return true +} + +// relativePart parses the rule: +// +// relative-part = "//" authority path-abempty +// / path-absolute +// / path-noscheme +// / path-empty +func (u *uri) relativePart() bool { + start := u.index + if u.takeDoubleSlash() && + u.authority() && + u.pathAbempty() { + return true + } + u.index = start + return u.pathAbsolute() || u.pathNoscheme() || u.pathEmpty() +} + +// scheme parses the rule: +// +// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) +// +// Terminated by ":". +func (u *uri) scheme() bool { + start := u.index + if u.alpha() { + //nolint:revive // this loop is intentionally empty to consume tokens + for u.alpha() || u.digit() || u.take('+') || u.take('-') || u.take('.') { + // Consume '*( ALPHA / DIGIT / "+" / "-" / "." )' + } + if u.peek(':') { + return true + } + } + u.index = start + return false +} + +// authority parses the rule: +// +// authority = [ userinfo "@" ] host [ ":" port ] +// +// Lead by double slash ("") and terminated by "/", "?", "#", or end of URI. +func (u *uri) authority() bool { + start := u.index + if u.userinfo() { + if !u.take('@') { + u.index = start + return false + } + } + if !u.host() { + u.index = start + return false + } + if u.take(':') { + if !u.port() { + u.index = start + return false + } + } + if !u.isAuthorityEnd() { + u.index = start + return false + } + return true +} + +// isAuthorityEnd reports whether the current position is the end of the authority. +// +// The authority component [...] is terminated by the next slash ("/"), +// question mark ("?"), or number sign ("#") character, or by the +// end of the URI. +func (u *uri) isAuthorityEnd() bool { + return u.index >= len(u.str) || + u.str[u.index] == '?' || + u.str[u.index] == '#' || + u.str[u.index] == '/' +} + +// userinfo parses the rule: +// +// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +// +// Terminated by "@" in authority. +func (u *uri) userinfo() bool { + start := u.index + for { + if u.unreserved() || + u.pctEncoded() || + u.subDelims() || + u.take(':') { + continue + } + if u.index < len(u.str) { + if u.str[u.index] == '@' { + return true + } + } + u.index = start + return false + } +} + +// checkHostPctEncoded verifies that str is correctly percent-encoded. +func (u *uri) checkHostPctEncoded(str string) bool { + unhex := func(char byte) byte { + switch { + case '0' <= char && char <= '9': + return char - '0' + case 'a' <= char && char <= 'f': + return char - 'a' + 10 + case 'A' <= char && char <= 'F': + return char - 'A' + 10 + } + return 0 + } + escaped := make([]byte, 0, len(str)) + for i := 0; i < len(str); { + switch str[i] { + case '%': + escaped = append(escaped, unhex(str[i+1])<<4|unhex(str[i+2])) + i += 3 + default: + escaped = append(escaped, str[i]) + i++ + } + } + return utf8.Valid(escaped) +} + +// host parses the rule: +// +// host = IP-literal / IPv4address / reg-name +func (u *uri) host() bool { + start := u.index + u.pctEncodedFound = false + // Note: IPv4address is a subset of reg-name + if (u.peek('[') && u.ipLiteral()) || u.regName() { + if u.pctEncodedFound { + rawHost := u.str[start:u.index] + // RFC 3986: + // > URI producing applications must not use percent-encoding in host + // > unless it is used to represent a UTF-8 character sequence. + if !u.checkHostPctEncoded(rawHost) { + return false + } + } + return true + } + return false +} + +// port parses the rule: +// +// port = *DIGIT +// +// Terminated by end of authority. +func (u *uri) port() bool { + start := u.index + for u.digit() { + // Consume '*DIGIT' + } + if u.isAuthorityEnd() { + return true + } + u.index = start + return false +} + +// ipLiteral parses the rule from RFC 6874: +// +// IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]" +func (u *uri) ipLiteral() bool { + start := u.index + if u.take('[') { + currIdx := u.index + if u.ipv6Address() && u.take(']') { + return true + } + u.index = currIdx + if u.ipv6addrz() && u.take(']') { + return true + } + u.index = currIdx + if u.ipvFuture() && u.take(']') { + return true + } + } + u.index = start + return false +} + +// ipv6Address parses the rule "IPv6address". +// +// Relies on the implementation of IsIP. +func (u *uri) ipv6Address() bool { + start := u.index + //nolint:revive // this loop is intentionally empty to consume tokens + for u.hexdig() || u.take(':') { + // Consume '*( HEXDIG / ":" )' + } + if IsIP(u.str[start:u.index], 6) { + return true + } + u.index = start + return false +} + +// ipv6addrz parses the rule from RFC 6874: +// +// IPv6addrz = IPv6address "%25" ZoneID +func (u *uri) ipv6addrz() bool { + start := u.index + if u.ipv6Address() && + u.take('%') && + u.take('2') && + u.take('5') && + u.zoneID() { + return true + } + u.index = start + return false +} + +// zoneID parses the rule from RFC 6874: +// +// ZoneID = 1*( unreserved / pct-encoded ) +func (u *uri) zoneID() bool { + start := u.index + //nolint:revive // this loop is intentionally empty to consume tokens + for u.unreserved() || u.pctEncoded() { + // Consume '*( unreserved / pct-encoded )' + } + if u.index-start > 0 { + return true + } + u.index = start + return false +} + +// ipvFuture parses the rule: +// +// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) +func (u *uri) ipvFuture() bool { + start := u.index + if u.take('v') && u.hexdig() { + for u.hexdig() { + // Consume '*HEXDIG' + } + if u.take('.') { + counter := 0 + for u.unreserved() || u.subDelims() || u.take(':') { + counter++ + } + if counter >= 1 { + return true + } + } + } + u.index = start + return false +} + +// regName parses the rule: +// +// reg-name = *( unreserved / pct-encoded / sub-delims ) +// +// Terminates on start of port (":") or end of authority. +func (u *uri) regName() bool { + start := u.index + for { + if u.unreserved() || u.pctEncoded() || u.subDelims() { + continue + } + if u.isAuthorityEnd() { + // End of authority + return true + } + if u.str[u.index] == ':' { + return true + } + u.index = start + return false + } +} + +// isPathEnd reports whether the current position is the end of the path. +// +// The path is terminated by the first question mark ("?") or +// number sign ("#") character, or by the end of the URI. +func (u *uri) isPathEnd() bool { + return u.index >= len(u.str) || u.str[u.index] == '?' || u.str[u.index] == '#' +} + +// pathAbempty parses the rule: +// +// path-abempty = *( "/" segment ) +// +// Terminated by end of path: "?", "#", or end of URI. +func (u *uri) pathAbempty() bool { + start := u.index + //nolint:revive // this loop is intentionally empty to consume tokens + for u.take('/') && u.segment() { + // Consume '*( "/" segment )' + } + if u.isPathEnd() { + return true + } + u.index = start + return false +} + +// pathAbsolute parses the rule: +// +// path-absolute = "/" [ segment-nz *( "/" segment ) ] +// +// Terminated by end of path: "?", "#", or end of URI. +func (u *uri) pathAbsolute() bool { + start := u.index + if u.take('/') { + if u.segmentNz() { + //nolint:revive // this loop is intentionally empty to consume tokens + for u.take('/') && u.segment() { + // Consume '*( "/" segment )' + } + } + if u.isPathEnd() { + return true + } + } + u.index = start + return false +} + +// pathNoscheme parses the rule: +// +// path-noscheme = segment-nz-nc *( "/" segment ) +// +// Terminated by end of path: "?", "#", or end of URI. +func (u *uri) pathNoscheme() bool { + start := u.index + if u.segmentNzNc() { + //nolint:revive // this loop is intentionally empty to consume tokens + for u.take('/') && u.segment() { + // Consume *( "/" segment ) + } + if u.isPathEnd() { + return true + } + } + u.index = start + return false +} + +// pathRootless parses the rule: +// +// path-rootless = segment-nz *( "/" segment ) +// +// Terminated by end of path: "?", "#", or end of URI. +func (u *uri) pathRootless() bool { + start := u.index + if u.segmentNz() { + //nolint:revive // this loop is intentionally empty to consume tokens + for u.take('/') && u.segment() { + // Consume *( '/' segment ) + } + if u.isPathEnd() { + return true + } + } + u.index = start + return false +} + +// pathEmpty parses the rule: +// +// path-empty = 0 +// +// Terminated by end of path: "?", "#", or end of URI. +func (u *uri) pathEmpty() bool { + return u.isPathEnd() +} + +// segment parses the rule: +// +// segment = *pchar +func (u *uri) segment() bool { + for u.pchar() { + // Consume '*pchar' + } + return true +} + +// segmentNz parses the rule: +// +// segment-nz = 1*pchar +func (u *uri) segmentNz() bool { + start := u.index + if u.pchar() { + return u.segment() + } + u.index = start + return false +} + +// segmentNzNc parses the rule: +// +// segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) +// ; non-zero-length segment without any colon ":" +func (u *uri) segmentNzNc() bool { + start := u.index + //nolint:revive // this loop is intentionally empty to consume tokens + for u.unreserved() || u.pctEncoded() || u.subDelims() || u.take('@') { + // Consume '*( unreserved / pct-encoded / sub-delims / "@" )' + } + if u.index-start > 0 { + return true + } + u.index = start + return false +} + +// pchar parses the rule: +// +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +func (u *uri) pchar() bool { + return u.unreserved() || + u.pctEncoded() || + u.subDelims() || + u.take(':') || + u.take('@') +} + +// query parses the rule: +// +// query = *( pchar / "/" / "?" ) +// +// Terminated by "#" or end of URI. +func (u *uri) query() bool { + start := u.index + for { + if u.pchar() || u.take('/') || u.take('?') { + continue + } + if u.index == len(u.str) || u.str[u.index] == '#' { + return true + } + u.index = start + return false + } +} + +// fragment parses the rule: +// +// fragment = *( pchar / "/" / "?" ) +// +// Terminated by end of URI. +func (u *uri) fragment() bool { + start := u.index + for { + if u.pchar() || u.take('/') || u.take('?') { + continue + } + if u.index == len(u.str) { + return true + } + u.index = start + return false + } +} + +// pctEncoded parses the rule: +// +// pct-encoded = "%"+HEXDIG+HEXDIG +// +// Sets `pctEncodedFound` to true if a valid triplet was found. +func (u *uri) pctEncoded() bool { + start := u.index + if u.take('%') && u.hexdig() && u.hexdig() { + u.pctEncodedFound = true + return true + } + u.index = start + return false +} + +// unreserved parses the rule: +// +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +func (u *uri) unreserved() bool { + return u.alpha() || + u.digit() || + u.take('-') || + u.take('_') || + u.take('.') || + u.take('~') +} + +// subDelims parses the rule: +// +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +func (u *uri) subDelims() bool { + return u.take('!') || + u.take('$') || + u.take('&') || + u.take('\'') || + u.take('(') || + u.take(')') || + u.take('*') || + u.take('+') || + u.take(',') || + u.take(';') || + u.take('=') +} + +// alpha parses the rule: +// +// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +func (u *uri) alpha() bool { + if u.index >= len(u.str) { + return false + } + c := u.str[u.index] + if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') { + u.index++ + return true + } + return false +} + +// digit parses the rule: +// +// DIGIT = %x30-39 ; 0-9 +func (u *uri) digit() bool { + if u.index >= len(u.str) { + return false + } + c := u.str[u.index] + if '0' <= c && c <= '9' { + u.index++ + return true + } + return false +} + +// hexdig parses the rule: +// +// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +func (u *uri) hexdig() bool { + if u.index >= len(u.str) { + return false + } + c := u.str[u.index] + if ('0' <= c && c <= '9') || + ('a' <= c && c <= 'f') || + ('A' <= c && c <= 'F') { + u.index++ + return true + } + return false +} + +// take reports whether the current position in the string is the character char. +func (u *uri) take(char byte) bool { + if u.index >= len(u.str) { + return false + } + if u.str[u.index] == char { + u.index++ + return true + } + return false +} + +func (u *uri) takeDoubleSlash() bool { + first := u.take('/') + return first && u.take('/') +} + +func (u *uri) peek(char byte) bool { + return u.index < len(u.str) && u.str[u.index] == char +} diff --git a/vendor/buf.build/go/protovalidate/native_bool.go b/vendor/buf.build/go/protovalidate/native_bool.go new file mode 100644 index 00000000..d2c4e6b0 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_bool.go @@ -0,0 +1,73 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "fmt" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/reflect/protoreflect" +) + +//nolint:gochecknoglobals +var ( + boolConstSite = makeRuleSite( + fieldRulesDesc.Fields().ByName("bool"), + (*validate.BoolRules)(nil).ProtoReflect().Descriptor().Fields().ByName("const"), + "bool.const", "", + ) +) + +// tryBuildNativeBoolRules attempts to build a native Go evaluator for +// bool rules. Returns nil if the rules can't be handled natively. +func tryBuildNativeBoolRules(base base, rules *validate.BoolRules) evaluator { + if rules == nil { + return nil + } + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + if !rules.HasConst() { + return nil + } + constVal := rules.GetConst() + rules.ProtoReflect().Clear(boolConstSite.desc) + return nativeBoolEval{ + base: base, + constVal: constVal, + } +} + +var _ evaluator = nativeBoolEval{} + +// nativeBoolEval is a native Go evaluator for bool const rules. +type nativeBoolEval struct { + base + constVal bool +} + +func (n nativeBoolEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { + if val.Bool() != n.constVal { + return &ValidationError{Violations: []*Violation{n.newViolation(boolConstSite, + "bool.const", fmt.Sprintf("must equal %t", n.constVal), + val, protoreflect.ValueOfBool(n.constVal)), + }} + } + return nil +} + +func (n nativeBoolEval) Tautology() bool { + return false +} diff --git a/vendor/buf.build/go/protovalidate/native_bytes.go b/vendor/buf.build/go/protovalidate/native_bytes.go new file mode 100644 index 00000000..023117b4 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_bytes.go @@ -0,0 +1,428 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "bytes" + "errors" + "fmt" + "math" + "regexp" + "slices" + "strings" + "unicode/utf8" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// tryBuildNativeBytesRules attempts to build a native Go evaluator for +// bytes rules. Returns nil if the rules can't be handled natively. +func tryBuildNativeBytesRules(base base, rules *validate.BytesRules) evaluator { + if rules == nil { + return nil + } + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + // Detect well-known format constraint (ip, ipv4, ipv6, uuid). + // Check both presence and value — setting ip=false means no check. + var wellKnown *bytesWellKnown + switch { + case rules.GetIp(): + wellKnown = &bytesWellKnownIP + rules.ProtoReflect().Clear(bytesDescs.ipDesc) + hasRule = true + case rules.GetIpv4(): + wellKnown = &bytesWellKnownIPv4 + rules.ProtoReflect().Clear(bytesDescs.ipv4Desc) + hasRule = true + case rules.GetIpv6(): + wellKnown = &bytesWellKnownIPv6 + rules.ProtoReflect().Clear(bytesDescs.ipv6Desc) + hasRule = true + case rules.GetUuid(): + wellKnown = &bytesWellKnownUUID + rules.ProtoReflect().Clear(bytesDescs.uuidDesc) + hasRule = true + } + + var constVal []byte + var hasConst bool + if rules.HasConst() { + constVal = rules.GetConst() + hasConst = true + rules.ProtoReflect().Clear(bytesDescs.constSite.desc) + hasRule = true + } + + var exactLen *uint64 + if rules.HasLen() { + exactLen = ptr(rules.GetLen()) + rules.ProtoReflect().Clear(bytesDescs.lenSite.desc) + hasRule = true + } + + var minLen uint64 + if rules.HasMinLen() { + minLen = rules.GetMinLen() + rules.ProtoReflect().Clear(bytesDescs.minLenSite.desc) + hasRule = true + } + + var maxLen uint64 = math.MaxUint64 + if rules.HasMaxLen() { + maxLen = rules.GetMaxLen() + rules.ProtoReflect().Clear(bytesDescs.maxLenSite.desc) + hasRule = true + } + + var compiledPattern *regexp.Regexp + var patternStr string + if rules.HasPattern() { + patternStr = rules.GetPattern() + var err error + compiledPattern, err = regexp.Compile(patternStr) + if err != nil { + return nil // bail to CEL + } + rules.ProtoReflect().Clear(bytesDescs.patternSite.desc) + hasRule = true + } + + var prefix []byte + var hasPrefix bool + if rules.HasPrefix() { + prefix = rules.GetPrefix() + hasPrefix = true + rules.ProtoReflect().Clear(bytesDescs.prefixSite.desc) + hasRule = true + } + + var suffix []byte + var hasSuffix bool + if rules.HasSuffix() { + suffix = rules.GetSuffix() + hasSuffix = true + rules.ProtoReflect().Clear(bytesDescs.suffixSite.desc) + hasRule = true + } + + var contains []byte + var hasContains bool + if rules.HasContains() { + contains = rules.GetContains() + hasContains = true + rules.ProtoReflect().Clear(bytesDescs.containsSite.desc) + hasRule = true + } + + var inVals [][]byte + if inVals = rules.GetIn(); len(inVals) > 0 { + rules.ProtoReflect().Clear(bytesDescs.inSite.desc) + hasRule = true + } + + var notInVals [][]byte + if notInVals = rules.GetNotIn(); len(notInVals) > 0 { + rules.ProtoReflect().Clear(bytesDescs.notInSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeBytesEval{ + base: base, + constVal: constVal, + hasConst: hasConst, + exactLen: exactLen, + minLen: minLen, + maxLen: maxLen, + pattern: compiledPattern, + patternStr: patternStr, + prefix: prefix, + hasPrefix: hasPrefix, + suffix: suffix, + hasSuffix: hasSuffix, + contains: contains, + hasContains: hasContains, + inVals: inVals, + notInVals: notInVals, + wellKnown: wellKnown, + } +} + +// bytesWellKnown identifies which well-known bytes format constraint is active. +type bytesWellKnown struct { + site ruleSite // pre-built rule path site for the error path + emptySite ruleSite // pre-built rule path site for the empty value + validSizes []int +} + +var ( + //nolint:gochecknoglobals + bytesWellKnownIP = bytesWellKnown{ + site: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipDesc, "bytes.ip", "must be a valid IP address"), + emptySite: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipDesc, "bytes.ip_empty", "value is empty, which is not a valid IP address"), + validSizes: []int{4, 16}, + } + //nolint:gochecknoglobals + bytesWellKnownIPv4 = bytesWellKnown{ + site: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipv4Desc, "bytes.ipv4", "must be a valid IPv4 address"), + emptySite: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipv4Desc, "bytes.ipv4_empty", "value is empty, which is not a valid IPv4 address"), + validSizes: []int{4}, + } + //nolint:gochecknoglobals + bytesWellKnownIPv6 = bytesWellKnown{ + site: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipv6Desc, "bytes.ipv6", "must be a valid IPv6 address"), + emptySite: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.ipv6Desc, "bytes.ipv6_empty", "value is empty, which is not a valid IPv6 address"), + validSizes: []int{16}, + } + //nolint:gochecknoglobals + bytesWellKnownUUID = bytesWellKnown{ + site: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.uuidDesc, "bytes.uuid", "must be a valid UUID"), + emptySite: makeRuleSite(bytesDescs.ruleDesc, bytesDescs.uuidDesc, "bytes.uuid_empty", "value is empty, which is not a valid UUID"), + validSizes: []int{16}, + } +) + +// bytesDescriptors bundles the field descriptors for BytesRules. +type bytesDescriptors struct { + ruleDesc protoreflect.FieldDescriptor + ipDesc protoreflect.FieldDescriptor + ipv4Desc protoreflect.FieldDescriptor + ipv6Desc protoreflect.FieldDescriptor + uuidDesc protoreflect.FieldDescriptor + + // Pre-built rule sites for the error path. + constSite ruleSite + lenSite ruleSite + minLenSite ruleSite + maxLenSite ruleSite + patternSite ruleSite + prefixSite ruleSite + suffixSite ruleSite + containsSite ruleSite + inSite ruleSite + notInSite ruleSite +} + +func makeBytesDescriptors() bytesDescriptors { + rulesDesc := (*validate.BytesRules)(nil).ProtoReflect().Descriptor() + descriptors := bytesDescriptors{ + ruleDesc: fieldRulesDesc.Fields().ByName("bytes"), + ipDesc: rulesDesc.Fields().ByName("ip"), + ipv4Desc: rulesDesc.Fields().ByName("ipv4"), + ipv6Desc: rulesDesc.Fields().ByName("ipv6"), + uuidDesc: rulesDesc.Fields().ByName("uuid"), + } + descriptors.constSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("const"), "bytes.const", "") + descriptors.lenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("len"), "bytes.len", "") + descriptors.minLenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("min_len"), "bytes.min_len", "") + descriptors.maxLenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("max_len"), "bytes.max_len", "") + descriptors.patternSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("pattern"), "bytes.pattern", "") + descriptors.prefixSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("prefix"), "bytes.prefix", "") + descriptors.suffixSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("suffix"), "bytes.suffix", "") + descriptors.containsSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("contains"), "bytes.contains", "") + descriptors.inSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("in"), "bytes.in", "") + descriptors.notInSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("not_in"), "bytes.not_in", "") + return descriptors +} + +//nolint:gochecknoglobals +var bytesDescs = makeBytesDescriptors() + +var _ evaluator = nativeBytesEval{} + +// nativeBytesEval is a native Go evaluator for bytes rules. +type nativeBytesEval struct { + base + constVal []byte + hasConst bool + exactLen *uint64 + minLen uint64 + maxLen uint64 + pattern *regexp.Regexp + patternStr string + prefix []byte + hasPrefix bool + suffix []byte + hasSuffix bool + contains []byte + hasContains bool + inVals [][]byte + notInVals [][]byte + wellKnown *bytesWellKnown +} + +var errNotUTF8 = errors.New("must be valid UTF-8 to apply regexp") + +//nolint:gocyclo // this code has nested ifs but it's not hard to follow. +func (n nativeBytesEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + bytesVal := val.Bytes() + byteLen := uint64(len(bytesVal)) + var violations []*Violation + + if n.hasConst && !bytes.Equal(bytesVal, n.constVal) { + violations = append(violations, n.newViolation(bytesDescs.constSite, + "bytes.const", fmt.Sprintf("must be %x", n.constVal), + val, protoreflect.ValueOfBytes(n.constVal))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.exactLen != nil && byteLen != *n.exactLen { + violations = append(violations, n.newViolation(bytesDescs.lenSite, + "bytes.len", fmt.Sprintf("must be %d bytes", *n.exactLen), + val, protoreflect.ValueOfUint64(*n.exactLen))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if byteLen < n.minLen { + violations = append(violations, n.newViolation(bytesDescs.minLenSite, + "bytes.min_len", fmt.Sprintf("must be at least %d bytes", n.minLen), + val, protoreflect.ValueOfUint64(n.minLen))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if byteLen > n.maxLen { + violations = append(violations, n.newViolation(bytesDescs.maxLenSite, + "bytes.max_len", fmt.Sprintf("must be at most %d bytes", n.maxLen), + val, protoreflect.ValueOfUint64(n.maxLen))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.pattern != nil { + if !utf8.Valid(bytesVal) { + // the bytes.pattern rule requires the value to be UTF-8. Surface + // this as a RuntimeError to match CEL behavior / conformance tests. + return &RuntimeError{cause: errNotUTF8} + } + if !n.pattern.MatchString(string(bytesVal)) { + violations = append(violations, n.newViolation(bytesDescs.patternSite, + "bytes.pattern", fmt.Sprintf("must match regex pattern `%s`", n.patternStr), + val, protoreflect.ValueOfString(n.patternStr))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + } + + if n.hasPrefix && !bytes.HasPrefix(bytesVal, n.prefix) { + violations = append(violations, n.newViolation(bytesDescs.prefixSite, + "bytes.prefix", fmt.Sprintf("does not have prefix %x", n.prefix), + val, protoreflect.ValueOfBytes(n.prefix))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.hasSuffix && !bytes.HasSuffix(bytesVal, n.suffix) { + violations = append(violations, n.newViolation(bytesDescs.suffixSite, + "bytes.suffix", fmt.Sprintf("does not have suffix %x", n.suffix), + val, protoreflect.ValueOfBytes(n.suffix))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.hasContains && !bytes.Contains(bytesVal, n.contains) { + violations = append(violations, n.newViolation(bytesDescs.containsSite, + "bytes.contains", fmt.Sprintf("does not contain %x", n.contains), + val, protoreflect.ValueOfBytes(n.contains))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(n.inVals) > 0 && !slices.ContainsFunc(n.inVals, func(v []byte) bool { return bytes.Equal(v, bytesVal) }) { + violations = append(violations, n.newViolation(bytesDescs.inSite, + "bytes.in", "must be in list "+formatBytesList(n.inVals), + val, sliceToListValue(&validate.BytesRules{}, bytesDescs.inSite.desc, n.inVals, protoreflect.ValueOfBytes))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(n.notInVals) > 0 && slices.ContainsFunc(n.notInVals, func(v []byte) bool { return bytes.Equal(v, bytesVal) }) { + violations = append(violations, n.newViolation(bytesDescs.notInSite, + "bytes.not_in", "must not be in list "+formatBytesList(n.notInVals), + val, sliceToListValue(&validate.BytesRules{}, bytesDescs.notInSite.desc, n.notInVals, protoreflect.ValueOfBytes))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.wellKnown != nil { + if v := n.evaluateWellKnown(bytesVal, val); v != nil { + violations = append(violations, v) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + } + + if len(violations) > 0 { + return &ValidationError{ + Violations: violations, + } + } + return nil +} + +func (n nativeBytesEval) evaluateWellKnown(bytesVal []byte, val protoreflect.Value) *Violation { + size := len(bytesVal) + wellKnown := n.wellKnown + + if size == 0 { + return n.newViolation(wellKnown.emptySite, + "", "", + val, protoreflect.ValueOfBool(true)) + } + + if slices.Contains(wellKnown.validSizes, size) { + return nil + } + + return n.newViolation(wellKnown.site, + "", "", + val, protoreflect.ValueOfBool(true)) +} + +func (n nativeBytesEval) Tautology() bool { + return false +} + +// formatBytesList formats a [][]byte to match CEL's list formatting. +func formatBytesList(vals [][]byte) string { + parts := make([]string, len(vals)) + for i, v := range vals { + // this is what CEL does for a byte slice; displays it as a string + parts[i] = string(v) + } + return "[" + strings.Join(parts, ", ") + "]" +} diff --git a/vendor/buf.build/go/protovalidate/native_enum.go b/vendor/buf.build/go/protovalidate/native_enum.go new file mode 100644 index 00000000..01d3a605 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_enum.go @@ -0,0 +1,144 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "fmt" + "slices" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/reflect/protoreflect" +) + +//nolint:gochecknoglobals +var ( + enumConstSite = makeRuleSite(enumRuleDescriptor, (*validate.EnumRules)(nil).ProtoReflect().Descriptor().Fields().ByName("const"), "enum.const", "") + enumInSite = makeRuleSite(enumRuleDescriptor, (*validate.EnumRules)(nil).ProtoReflect().Descriptor().Fields().ByName("in"), "enum.in", "") + enumNotInSite = makeRuleSite(enumRuleDescriptor, (*validate.EnumRules)(nil).ProtoReflect().Descriptor().Fields().ByName("not_in"), "enum.not_in", "") +) + +// tryBuildNativeEnumRules attempts to build a native Go evaluator for +// enum const/in/not_in rules. Returns nil if the rules can't be handled +// natively. Note: defined_only is handled separately in enum.go. +func tryBuildNativeEnumRules(base base, rules *validate.EnumRules) evaluator { + if rules == nil { + return nil + } + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + var constVal *int32 + if rules.HasConst() { + constVal = ptr(rules.GetConst()) + rules.ProtoReflect().Clear(enumConstSite.desc) + hasRule = true + } + + var inVals []int32 + if inVals = rules.GetIn(); len(inVals) > 0 { + rules.ProtoReflect().Clear(enumInSite.desc) + hasRule = true + } + + var notInVals []int32 + if notInVals = rules.GetNotIn(); len(notInVals) > 0 { + rules.ProtoReflect().Clear(enumNotInSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeEnumEval{ + base: base, + constVal: constVal, + inVals: inVals, + notInVals: notInVals, + } +} + +var _ evaluator = nativeEnumEval{} + +// nativeEnumEval is a native Go evaluator for enum const/in/not_in rules. +type nativeEnumEval struct { + base + constVal *int32 + inVals []int32 + notInVals []int32 +} + +type enumProcessor func(n nativeEnumEval, val protoreflect.Value, enumVal int32) *Violation + +//nolint:gochecknoglobals // slice of all the processors that are used, value never modified, effectively immutable +var enumProcessors = []enumProcessor{ + // const + func(n nativeEnumEval, val protoreflect.Value, enumVal int32) *Violation { + if n.constVal != nil && enumVal != *n.constVal { + return n.newViolation(enumConstSite, + "enum.const", fmt.Sprintf("must equal %d", *n.constVal), + val, protoreflect.ValueOfInt32(*n.constVal)) + } + return nil + }, + // in + func(n nativeEnumEval, val protoreflect.Value, enumVal int32) *Violation { + if len(n.inVals) > 0 && !slices.Contains(n.inVals, enumVal) { + return n.newViolation(enumInSite, + "enum.in", "must be in list "+formatList(n.inVals), + val, sliceToListValue(&validate.EnumRules{}, enumInSite.desc, n.inVals, protoreflect.ValueOfInt32)) + } + return nil + }, + // not_in + func(n nativeEnumEval, val protoreflect.Value, enumVal int32) *Violation { + if len(n.notInVals) > 0 && slices.Contains(n.notInVals, enumVal) { + return n.newViolation(enumNotInSite, + "enum.not_in", "must not be in list "+formatList(n.notInVals), + val, sliceToListValue(&validate.EnumRules{}, enumNotInSite.desc, n.notInVals, protoreflect.ValueOfInt32)) + } + return nil + }, +} + +func (n nativeEnumEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + enumVal := int32(val.Enum()) + + var violations []*Violation + + for _, enumProcessor := range enumProcessors { + violation := enumProcessor(n, val, enumVal) + if violation != nil { + violations = append(violations, violation) + if cfg.failFast { + break + } + } + } + + if len(violations) > 0 { + return &ValidationError{ + Violations: violations, + } + } + return nil +} + +func (n nativeEnumEval) Tautology() bool { + return false +} diff --git a/vendor/buf.build/go/protovalidate/native_map.go b/vendor/buf.build/go/protovalidate/native_map.go new file mode 100644 index 00000000..cb49d9de --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_map.go @@ -0,0 +1,114 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "fmt" + "math" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/reflect/protoreflect" +) + +//nolint:gochecknoglobals +var ( + mapMinPairsSite = makeRuleSite( + mapFieldRulesDesc, + (*validate.MapRules)(nil).ProtoReflect().Descriptor().Fields().ByName("min_pairs"), + "map.min_pairs", "", + ) + mapMaxPairsSite = makeRuleSite( + mapFieldRulesDesc, + (*validate.MapRules)(nil).ProtoReflect().Descriptor().Fields().ByName("max_pairs"), + "map.max_pairs", "", + ) +) + +// tryNativeMapRules attempts to build a native Go evaluator for +// map-level rules (min_pairs, max_pairs). +// Returns nil if the rules can't be handled natively. +func tryNativeMapRules(base base, rules *validate.MapRules) evaluator { + if rules == nil { + return nil + } + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + var minPairs uint64 + if rules.HasMinPairs() { + minPairs = rules.GetMinPairs() + rules.ProtoReflect().Clear(mapMinPairsSite.desc) + hasRule = true + } + + var maxPairs uint64 = math.MaxUint64 + if rules.HasMaxPairs() { + maxPairs = rules.GetMaxPairs() + rules.ProtoReflect().Clear(mapMaxPairsSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeMapEval{ + base: base, + minPairs: minPairs, + maxPairs: maxPairs, + } +} + +var _ evaluator = nativeMapEval{} + +// nativeMapEval is a native Go evaluator for map-level rules +// (min_pairs, max_pairs). Key/value rules are handled separately +// by the kvPairs evaluator in map.go. +type nativeMapEval struct { + base + minPairs uint64 + maxPairs uint64 +} + +func (n nativeMapEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { + size := uint64(val.Map().Len()) //nolint:gosec // int will never be negative or out of uint64 range + + // min_pairs + if size < n.minPairs { + return &ValidationError{Violations: []*Violation{n.newViolation(mapMinPairsSite, + "map.min_pairs", + fmt.Sprintf("map must be at least %d entries", n.minPairs), + val, protoreflect.ValueOfUint64(n.minPairs)), + }} + } + + // max_pairs + if size > n.maxPairs { + return &ValidationError{Violations: []*Violation{n.newViolation(mapMaxPairsSite, + "map.max_pairs", + fmt.Sprintf("map must be at most %d entries", n.maxPairs), + val, protoreflect.ValueOfUint64(n.maxPairs)), + }} + } + + return nil +} + +func (n nativeMapEval) Tautology() bool { + return n.minPairs == 0 && n.maxPairs == math.MaxUint64 +} diff --git a/vendor/buf.build/go/protovalidate/native_numeric.go b/vendor/buf.build/go/protovalidate/native_numeric.go new file mode 100644 index 00000000..2b51874c --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_numeric.go @@ -0,0 +1,660 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "fmt" + "math" + "slices" + "strconv" + "strings" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// numericValue is the set of Go types that back protobuf numeric field kinds. +type numericValue interface { + ~int32 | ~int64 | ~uint32 | ~uint64 | ~float32 | ~float64 +} + +// Per-kind builder wrappers. Each handles nil check and type-specific +// concerns before delegating to the generic builder. + +func tryBuildNativeInt32Rules(base base, rules *validate.Int32Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &int32Config) +} + +func tryBuildNativeSint32Rules(base base, rules *validate.SInt32Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &sint32Config) +} + +func tryBuildNativeSfixed32Rules(base base, rules *validate.SFixed32Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &sfixed32Config) +} + +func tryBuildNativeInt64Rules(base base, rules *validate.Int64Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &int64Config) +} + +func tryBuildNativeSint64Rules(base base, rules *validate.SInt64Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &sint64Config) +} + +func tryBuildNativeSfixed64Rules(base base, rules *validate.SFixed64Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &sfixed64Config) +} + +func tryBuildNativeUint32Rules(base base, rules *validate.UInt32Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &uint32Config) +} + +func tryBuildNativeFixed32Rules(base base, rules *validate.Fixed32Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &fixed32Config) +} + +func tryBuildNativeUint64Rules(base base, rules *validate.UInt64Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &uint64Config) +} + +func tryBuildNativeFixed64Rules(base base, rules *validate.Fixed64Rules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &fixed64Config) +} + +func tryBuildNativeFloatRules(base base, rules *validate.FloatRules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &floatConfig) +} + +func tryBuildNativeDoubleRules(base base, rules *validate.DoubleRules) evaluator { + if rules == nil { + return nil + } + return tryBuildNativeNumericRules(base, rules, &doubleConfig) +} + +// tryBuildNativeNumericRules attempts to build a native Go evaluator for +// numeric rules. Returns nil if the rules can't be handled natively, +// including cases with unknown fields (custom predefined extensions). +func tryBuildNativeNumericRules[T numericValue, R numericRules[T]]( + base base, + rules R, + config *numericTypeConfig[T], +) evaluator { + // Bail out if the rules message has unknown fields, which indicate + // custom predefined extensions that we can't handle natively. + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + // bail out if there's a gt/lt and the value is NaN + // (it's an invalid protovalidate rule; let CEL return the error) + var lowerValue T + lower := lowerBoundNone + switch { + case rules.HasGt(): + lower = lowerBoundGt + lowerValue = rules.GetGt() + if math.IsNaN(float64(lowerValue)) { + return nil + } + rules.ProtoReflect().Clear(config.descs.gtSite.desc) + hasRule = true + case rules.HasGte(): + lower = lowerBoundGte + lowerValue = rules.GetGte() + if math.IsNaN(float64(lowerValue)) { + return nil + } + rules.ProtoReflect().Clear(config.descs.gteSite.desc) + hasRule = true + } + + var upperValue T + upper := upperBoundNone + switch { + case rules.HasLt(): + upper = upperBoundLt + upperValue = rules.GetLt() + if math.IsNaN(float64(upperValue)) { + return nil + } + rules.ProtoReflect().Clear(config.descs.ltSite.desc) + hasRule = true + case rules.HasLte(): + upper = upperBoundLte + upperValue = rules.GetLte() + if math.IsNaN(float64(upperValue)) { + return nil + } + rules.ProtoReflect().Clear(config.descs.lteSite.desc) + hasRule = true + } + + var constVal *T + if rules.HasConst() { + constVal = ptr(rules.GetConst()) + rules.ProtoReflect().Clear(config.descs.constSite.desc) + hasRule = true + } + + var inVals []T + if inVals = rules.GetIn(); len(inVals) > 0 { + rules.ProtoReflect().Clear(config.descs.inSite.desc) + hasRule = true + } + + var notInVals []T + if notInVals = rules.GetNotIn(); len(notInVals) > 0 { + rules.ProtoReflect().Clear(config.descs.notInSite.desc) + hasRule = true + } + + type finiteInterface interface { + HasFinite() bool + GetFinite() bool + } + + finite := false + if fi, ok := (any)(rules).(finiteInterface); ok && fi.HasFinite() { + finite = fi.GetFinite() + rules.ProtoReflect().Clear(config.descs.finiteSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeNumericCompare[T]{ + base: base, + config: config, + lo: lowerValue, + lower: lower, + hi: upperValue, + upper: upper, + constVal: constVal, + inVals: inVals, + notInVals: notInVals, + finite: finite, + } +} + +// numericRules is satisfied by all generated numeric rules types +// (Int32Rules, Int64Rules, UInt32Rules, etc.). +// +//nolint:interfacebloat +type numericRules[T numericValue] interface { + HasGt() bool + GetGt() T + HasGte() bool + GetGte() T + HasLt() bool + GetLt() T + HasLte() bool + GetLte() T + HasConst() bool + GetConst() T + GetIn() []T + GetNotIn() []T + ProtoReflect() protoreflect.Message +} + +// numericDescriptors bundles the pre-built rule sites for a single numeric +// rules type (e.g., Int32Rules). A ruleSite carries both the rule-path +// FieldPathElements and the leaf descriptor, so the individual per-rule +// descriptor fields are not needed. +type numericDescriptors struct { + gtSite ruleSite + gteSite ruleSite + ltSite ruleSite + lteSite ruleSite + constSite ruleSite + inSite ruleSite + notInSite ruleSite + finiteSite ruleSite // zero-valued for non-float kinds +} + +func makeNumericDescriptors( + fieldName string, + rulesMsg protoreflect.ProtoMessage, + typeName string, +) numericDescriptors { + rulesDesc := rulesMsg.ProtoReflect().Descriptor() + ruleDesc := fieldRulesDesc.Fields().ByName(protoreflect.Name(fieldName)) + var finiteDesc protoreflect.FieldDescriptor + if rulesDesc.Name() == "FloatRules" || rulesDesc.Name() == "DoubleRules" { + finiteDesc = rulesDesc.Fields().ByName("finite") + } + descriptors := numericDescriptors{ + gtSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("gt"), "", ""), + gteSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("gte"), "", ""), + ltSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("lt"), "", ""), + lteSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("lte"), "", ""), + constSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("const"), typeName+".const", ""), + inSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("in"), typeName+".in", ""), + notInSite: makeRuleSite(ruleDesc, rulesDesc.Fields().ByName("not_in"), typeName+".not_in", ""), + } + if finiteDesc != nil { + descriptors.finiteSite = makeRuleSite(ruleDesc, finiteDesc, typeName+".finite", "must be finite") + } + return descriptors +} + +// numericTypeConfig holds all type-specific operations and metadata +// for a single proto numeric kind. +type numericTypeConfig[T numericValue] struct { + typeName string // proto rule prefix: "int32", "sint32", "float", etc. + descs numericDescriptors // descriptor bundle for rule path construction + extractVal func(protoreflect.Value) T // val.Int/Uint/Float + cast + makeRuleVal func(T) protoreflect.Value // ValueOfInt32, ValueOfFloat32, etc. + newRules func() proto.Message // fresh rules message, used to build in/not_in rule values + nanFailsRange bool // true only for float32, float64 +} + +//nolint:gochecknoglobals +var ( + int32Config = numericTypeConfig[int32]{ + typeName: "int32", + descs: makeNumericDescriptors("int32", (*validate.Int32Rules)(nil), "int32"), + extractVal: func(v protoreflect.Value) int32 { return int32(v.Int()) }, + makeRuleVal: protoreflect.ValueOfInt32, + newRules: func() proto.Message { return &validate.Int32Rules{} }, + } + sint32Config = numericTypeConfig[int32]{ + typeName: "sint32", + descs: makeNumericDescriptors("sint32", (*validate.SInt32Rules)(nil), "sint32"), + extractVal: func(v protoreflect.Value) int32 { return int32(v.Int()) }, + makeRuleVal: protoreflect.ValueOfInt32, + newRules: func() proto.Message { return &validate.SInt32Rules{} }, + } + sfixed32Config = numericTypeConfig[int32]{ + typeName: "sfixed32", + descs: makeNumericDescriptors("sfixed32", (*validate.SFixed32Rules)(nil), "sfixed32"), + extractVal: func(v protoreflect.Value) int32 { return int32(v.Int()) }, + makeRuleVal: protoreflect.ValueOfInt32, + newRules: func() proto.Message { return &validate.SFixed32Rules{} }, + } + int64Config = numericTypeConfig[int64]{ + typeName: "int64", + descs: makeNumericDescriptors("int64", (*validate.Int64Rules)(nil), "int64"), + extractVal: func(v protoreflect.Value) int64 { return v.Int() }, + makeRuleVal: protoreflect.ValueOfInt64, + newRules: func() proto.Message { return &validate.Int64Rules{} }, + } + sint64Config = numericTypeConfig[int64]{ + typeName: "sint64", + descs: makeNumericDescriptors("sint64", (*validate.SInt64Rules)(nil), "sint64"), + extractVal: func(v protoreflect.Value) int64 { return v.Int() }, + makeRuleVal: protoreflect.ValueOfInt64, + newRules: func() proto.Message { return &validate.SInt64Rules{} }, + } + sfixed64Config = numericTypeConfig[int64]{ + typeName: "sfixed64", + descs: makeNumericDescriptors("sfixed64", (*validate.SFixed64Rules)(nil), "sfixed64"), + extractVal: func(v protoreflect.Value) int64 { return v.Int() }, + makeRuleVal: protoreflect.ValueOfInt64, + newRules: func() proto.Message { return &validate.SFixed64Rules{} }, + } + uint32Config = numericTypeConfig[uint32]{ + typeName: "uint32", + descs: makeNumericDescriptors("uint32", (*validate.UInt32Rules)(nil), "uint32"), + extractVal: func(v protoreflect.Value) uint32 { return uint32(v.Uint()) }, + makeRuleVal: protoreflect.ValueOfUint32, + newRules: func() proto.Message { return &validate.UInt32Rules{} }, + } + fixed32Config = numericTypeConfig[uint32]{ + typeName: "fixed32", + descs: makeNumericDescriptors("fixed32", (*validate.Fixed32Rules)(nil), "fixed32"), + extractVal: func(v protoreflect.Value) uint32 { return uint32(v.Uint()) }, + makeRuleVal: protoreflect.ValueOfUint32, + newRules: func() proto.Message { return &validate.Fixed32Rules{} }, + } + uint64Config = numericTypeConfig[uint64]{ + typeName: "uint64", + descs: makeNumericDescriptors("uint64", (*validate.UInt64Rules)(nil), "uint64"), + extractVal: func(v protoreflect.Value) uint64 { return v.Uint() }, + makeRuleVal: protoreflect.ValueOfUint64, + newRules: func() proto.Message { return &validate.UInt64Rules{} }, + } + fixed64Config = numericTypeConfig[uint64]{ + typeName: "fixed64", + descs: makeNumericDescriptors("fixed64", (*validate.Fixed64Rules)(nil), "fixed64"), + extractVal: func(v protoreflect.Value) uint64 { return v.Uint() }, + makeRuleVal: protoreflect.ValueOfUint64, + newRules: func() proto.Message { return &validate.Fixed64Rules{} }, + } + floatConfig = numericTypeConfig[float32]{ + typeName: "float", + descs: makeNumericDescriptors("float", (*validate.FloatRules)(nil), "float"), + extractVal: func(v protoreflect.Value) float32 { return float32(v.Float()) }, + makeRuleVal: protoreflect.ValueOfFloat32, + newRules: func() proto.Message { return &validate.FloatRules{} }, + nanFailsRange: true, + } + doubleConfig = numericTypeConfig[float64]{ + typeName: "double", + descs: makeNumericDescriptors("double", (*validate.DoubleRules)(nil), "double"), + extractVal: func(v protoreflect.Value) float64 { return v.Float() }, + makeRuleVal: protoreflect.ValueOfFloat64, + newRules: func() proto.Message { return &validate.DoubleRules{} }, + nanFailsRange: true, + } +) + +// lowerBound describes which lower bound constraint is active. +type lowerBound int + +const ( + lowerBoundNone lowerBound = iota + // lowerBoundGte is an inclusive lower bound (>=). + lowerBoundGte + // lowerBoundGt is an exclusive lower bound (>). + lowerBoundGt +) + +// upperBound describes which upper bound constraint is active. +type upperBound int + +const ( + upperBoundNone upperBound = iota + upperBoundLt + upperBoundLte +) + +// nativeNumericCompare is a native Go evaluator for numeric gt/gte/lt/lte/ +// const/in/not_in rules. It replaces CEL evaluation with direct Go comparisons. +// +// config is stored as a pointer so the (globally shared, read-only) config +// struct is not copied into every evaluator (and then again onto the stack +// via the value receiver on Evaluate). +type nativeNumericCompare[T numericValue] struct { + base + config *numericTypeConfig[T] + lo T // lower bound value (gt or gte threshold) + lower lowerBound // gt (exclusive) or gte (inclusive) + hi T // upper bound value (lt or lte threshold) + upper upperBound // none, lt, or lte + constVal *T // constant value for comparison + inVals []T // slice of values for IN comparison + notInVals []T // slice of values for NOT_IN comparison + finite bool // true if the value is finite (not NaN or Infinity) +} + +// belowLo reports whether v violates the lower bound. +func (n nativeNumericCompare[T]) belowLo(v T) bool { + if n.lower == lowerBoundGt { + return v <= n.lo + } + return v < n.lo +} + +// aboveHi reports whether v violates the upper bound. +func (n nativeNumericCompare[T]) aboveHi(v T) bool { + if n.upper == upperBoundLt { + return v >= n.hi + } + return v > n.hi +} + +// isNormalRange reports whether lo and hi form a normal (non-exclusive) range. +func (n nativeNumericCompare[T]) isNormalRange() bool { + return n.hi >= n.lo +} + +func (n nativeNumericCompare[T]) loSite() ruleSite { + if n.lower == lowerBoundGt { + return n.config.descs.gtSite + } + return n.config.descs.gteSite +} + +func (n nativeNumericCompare[T]) hiSite() ruleSite { + if n.upper == upperBoundLt { + return n.config.descs.ltSite + } + return n.config.descs.lteSite +} + +func (n nativeNumericCompare[T]) gtRulePrefix() string { + if n.lower == lowerBoundGt { + return n.config.typeName + ".gt" + } + return n.config.typeName + ".gte" +} + +func (n nativeNumericCompare[T]) ltRulePrefix() string { + if n.upper == upperBoundLt { + return n.config.typeName + ".lt" + } + return n.config.typeName + ".lte" +} + +func (n nativeNumericCompare[T]) gtltRule() string { + if n.lower != lowerBoundNone { + prefix := n.gtRulePrefix() + switch n.upper { + case upperBoundLt: + prefix += "_lt" + if !n.isNormalRange() { + prefix += "_exclusive" + } + case upperBoundLte: + prefix += "_lte" + if !n.isNormalRange() { + prefix += "_exclusive" + } + } + return prefix + } + return n.ltRulePrefix() +} + +func (n nativeNumericCompare[T]) loMessage() string { + if n.lower == lowerBoundGt { + return "greater than " + format(n.lo) + } + return "greater than or equal to " + format(n.lo) +} + +func (n nativeNumericCompare[T]) hiMessage() string { + if n.upper == upperBoundLt { + return "less than " + format(n.hi) + } + return "less than or equal to " + format(n.hi) +} + +func (n nativeNumericCompare[T]) conjunction() string { + if n.isNormalRange() { + return "and" + } + return "or" +} + +func (n nativeNumericCompare[T]) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + valT := n.config.extractVal(val) + var violations []*Violation + + if n.constVal != nil && valT != *n.constVal { + violations = append(violations, n.newViolation(n.config.descs.constSite, + n.config.typeName+".const", + "must equal "+format(*n.constVal), + val, n.config.makeRuleVal(*n.constVal))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(n.inVals) > 0 && !slices.Contains(n.inVals, valT) { + violations = append(violations, n.newViolation(n.config.descs.inSite, + n.config.typeName+".in", + "must be in list "+formatList(n.inVals), + val, sliceToListValue(n.config.newRules(), n.config.descs.inSite.desc, n.inVals, n.config.makeRuleVal))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(n.notInVals) > 0 && slices.Contains(n.notInVals, valT) { + violations = append(violations, n.newViolation(n.config.descs.notInSite, + n.config.typeName+".not_in", + "must not be in list "+formatList(n.notInVals), + val, sliceToListValue(n.config.newRules(), n.config.descs.notInSite.desc, n.notInVals, n.config.makeRuleVal))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.finite && (math.IsNaN(float64(valT)) || math.IsInf(float64(valT), 0)) { + violations = append(violations, n.newViolation(n.config.descs.finiteSite, + n.config.typeName+".finite", + "must be finite", + val, protoreflect.ValueOfBool(true))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if v := n.evaluateRange(valT, val); v != nil { + violations = append(violations, v) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(violations) > 0 { + return &ValidationError{ + Violations: violations, + } + } + return nil +} + +// evaluateRange returns a violation for lower/upper bound checks, or nil. +// Split out of Evaluate so that the hot path (no range rules) stays small +// enough to inline. +func (n nativeNumericCompare[T]) evaluateRange(valT T, val protoreflect.Value) *Violation { + if n.lower == lowerBoundNone && n.upper == upperBoundNone { + return nil + } + + // For float/double, NaN fails all range checks (matches CEL behavior). + isNaN := n.config.nanFailsRange && math.IsNaN(float64(valT)) + + switch { + case n.lower == lowerBoundNone: + if isNaN || n.aboveHi(valT) { + return n.newViolation(n.hiSite(), + n.gtltRule(), "must be "+n.hiMessage(), + val, n.config.makeRuleVal(n.hi)) + } + case n.upper == upperBoundNone: + if isNaN || n.belowLo(valT) { + return n.newViolation(n.loSite(), + n.gtltRule(), "must be "+n.loMessage(), + val, n.config.makeRuleVal(n.lo)) + } + default: + var failure bool + if n.isNormalRange() { + failure = isNaN || n.aboveHi(valT) || n.belowLo(valT) + } else { + failure = isNaN || (n.aboveHi(valT) && n.belowLo(valT)) + } + if failure { + return n.newViolation(n.loSite(), + n.gtltRule(), + fmt.Sprintf("must be %s %s %s", n.loMessage(), n.conjunction(), n.hiMessage()), + val, n.config.makeRuleVal(n.lo)) + } + } + return nil +} + +func (n nativeNumericCompare[T]) Tautology() bool { + return false +} + +var _ evaluator = nativeNumericCompare[int32]{} + +func ptr[T any](v T) *T { return &v } + +// formatList formats a slice as "list [val1, val2]" to match CEL message format. +func formatList[T any](vals []T) string { + parts := make([]string, len(vals)) + for i, v := range vals { + parts[i] = format(v) + } + return "[" + strings.Join(parts, ", ") + "]" +} + +func format(v any) string { + switch val := v.(type) { + case float32: + return printFloat(float64(val)) + case float64: + return printFloat(val) + default: + return fmt.Sprintf("%v", val) + } +} + +func printFloat(argDbl float64) string { + if math.IsNaN(argDbl) { + return "NaN" + } + if math.IsInf(argDbl, -1) { + return "-Infinity" + } + if math.IsInf(argDbl, 1) { + return "Infinity" + } + return strconv.FormatFloat(argDbl, 'f', -1, 64) +} diff --git a/vendor/buf.build/go/protovalidate/native_repeated.go b/vendor/buf.build/go/protovalidate/native_repeated.go new file mode 100644 index 00000000..6abf61f1 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_repeated.go @@ -0,0 +1,277 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "bytes" + "fmt" + "math" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/reflect/protoreflect" +) + +//nolint:gochecknoglobals +var ( + repeatedMinItemsSite = makeRuleSite( + repeatedFieldRulesDesc, + (*validate.RepeatedRules)(nil).ProtoReflect().Descriptor().Fields().ByName("min_items"), + "repeated.min_items", "", + ) + repeatedMaxItemsSite = makeRuleSite( + repeatedFieldRulesDesc, + (*validate.RepeatedRules)(nil).ProtoReflect().Descriptor().Fields().ByName("max_items"), + "repeated.max_items", "", + ) + repeatedUniqueSite = makeRuleSite( + repeatedFieldRulesDesc, + (*validate.RepeatedRules)(nil).ProtoReflect().Descriptor().Fields().ByName("unique"), + "repeated.unique", + "repeated value must contain unique items", + ) +) + +// tryNativeRepeatedRules attempts to build a native Go evaluator for +// repeated list-level rules (min_items, max_items, unique). +// Returns nil if the rules can't be handled natively. +func tryNativeRepeatedRules(base base, rules *validate.RepeatedRules) evaluator { + if rules == nil { + return nil + } + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + var minItems uint64 + if rules.HasMinItems() { + minItems = rules.GetMinItems() + rules.ProtoReflect().Clear(repeatedMinItemsSite.desc) + hasRule = true + } + + var maxItems uint64 = math.MaxUint64 + if rules.HasMaxItems() { + maxItems = rules.GetMaxItems() + rules.ProtoReflect().Clear(repeatedMaxItemsSite.desc) + hasRule = true + } + + var uniqueFn uniqueChecker + if rules.GetUnique() { + uniqueFn = uniqueCheckerForKind(base.Descriptor) + if uniqueFn == nil { + // message/list/map elements can't be checked for uniqueness + // natively; fall through to CEL. + return nil + } + rules.ProtoReflect().Clear(repeatedUniqueSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeRepeatedEval{ + base: base, + minItems: minItems, + maxItems: maxItems, + uniqueFn: uniqueFn, + } +} + +// uniqueChecker tests whether all elements in a repeated list are distinct. +// A nil uniqueChecker means the `unique` rule is not active for this field. +type uniqueChecker func(protoreflect.List) bool + +// uniqueCheckerForKind returns the concrete uniqueness check for the element +// kind of a repeated field, or nil if the kind isn't supported (message, list, +// map — none of which are valid element kinds for a repeated scalar field +// with `unique`). +func uniqueCheckerForKind(desc protoreflect.FieldDescriptor) uniqueChecker { + if desc == nil { + return nil + } + switch desc.Kind() { + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return isUniqueList[int32] + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return isUniqueList[int64] + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return isUniqueList[uint32] + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return isUniqueList[uint64] + case protoreflect.FloatKind: + return isUniqueList[float32] + case protoreflect.DoubleKind: + return isUniqueList[float64] + case protoreflect.StringKind: + return isUniqueList[string] + case protoreflect.BoolKind: + return isUniqueList[bool] + case protoreflect.EnumKind: + return isUniqueList[protoreflect.EnumNumber] + case protoreflect.BytesKind: + return isUniqueBytes + case protoreflect.MessageKind, protoreflect.GroupKind: + return nil + default: + return nil + } +} + +var _ evaluator = nativeRepeatedEval{} + +// nativeRepeatedEval is a native Go evaluator for repeated list-level rules +// (min_items, max_items, unique). Item-level rules are handled separately +// by the listItems evaluator in repeated.go. +type nativeRepeatedEval struct { + base + minItems uint64 + maxItems uint64 + // uniqueFn is nil when the `unique` rule is not active. When set, it is + // specialized for the field's element kind at compile time. + uniqueFn uniqueChecker +} + +func (n nativeRepeatedEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + list := val.List() + size := uint64(list.Len()) //nolint:gosec // len can't be < 0 and is always within uint64 range + var violations []*Violation + + if size < n.minItems { + violations = append(violations, n.newViolation(repeatedMinItemsSite, + "repeated.min_items", + fmt.Sprintf("must contain at least %d item(s)", n.minItems), + val, protoreflect.ValueOfUint64(n.minItems))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if size > n.maxItems { + violations = append(violations, n.newViolation(repeatedMaxItemsSite, + "repeated.max_items", + fmt.Sprintf("must contain no more than %d item(s)", n.maxItems), + val, protoreflect.ValueOfUint64(n.maxItems))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if n.uniqueFn != nil && !n.uniqueFn(list) { + violations = append(violations, n.newViolation(repeatedUniqueSite, + "repeated.unique", + "repeated value must contain unique items", + val, protoreflect.ValueOfBool(true))) + if cfg.failFast { + return &ValidationError{Violations: violations} + } + } + + if len(violations) > 0 { + return &ValidationError{ + Violations: violations, + } + } + return nil +} + +// uniqueLinearThreshold is the list length at and below which uniqueness is +// checked with an O(n²) scan over a stack-allocated array instead of a map. +// For small lists the linear scan is faster and avoids the map allocation; +// at larger sizes the map's O(n) lookup wins. +const uniqueLinearThreshold = 16 + +// isUniqueList is the generic uniqueness check used for all comparable scalar +// element kinds (the concrete T is bound at compile time via +// uniqueCheckerForKind). +func isUniqueList[T comparable](list protoreflect.List) bool { + length := list.Len() + if length <= 1 { + return true + } + if length <= uniqueLinearThreshold { + var seen = make([]T, length) + for i := range length { + key, ok := list.Get(i).Interface().(T) + if !ok { + return false + } + for j := range i { + if seen[j] == key { + return false + } + } + seen[i] = key + } + return true + } + + seen := make(map[T]struct{}, length) + for i := range length { + key, ok := list.Get(i).Interface().(T) + if !ok { + // should never happen, but just in case + return false + } + if _, exists := seen[key]; exists { + return false + } + seen[key] = struct{}{} + } + return true +} + +func isUniqueBytes(list protoreflect.List) bool { + length := list.Len() + if length <= 1 { + return true + } + if length <= uniqueLinearThreshold { + // storing []byte directly avoids the []byte→string allocation the + // map path needs for a hashable key. + var seen = make([][]byte, uniqueLinearThreshold) + for i := range length { + byteVal := list.Get(i).Bytes() + for j := range i { + if bytes.Equal(seen[j], byteVal) { + return false + } + } + seen[i] = byteVal + } + return true + } + + seen := make(map[string]struct{}, length) + for i := range length { + byteVal := list.Get(i).Bytes() + // []byte is not comparable; convert to string for use as map key. + // this is the same action performed by CEL in library.uniqueBytes + key := string(byteVal) + if _, exists := seen[key]; exists { + return false + } + seen[key] = struct{}{} + } + return true +} + +func (n nativeRepeatedEval) Tautology() bool { + return false +} diff --git a/vendor/buf.build/go/protovalidate/native_string.go b/vendor/buf.build/go/protovalidate/native_string.go new file mode 100644 index 00000000..5aa828b3 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/native_string.go @@ -0,0 +1,671 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import ( + "errors" + "fmt" + "regexp" + "slices" + "strings" + "unicode/utf8" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "buf.build/go/protovalidate/internal/rules" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// tryBuildNativeStringRules attempts to build a native Go evaluator for +// string rules. Returns nil if the rules can't be handled natively. +func tryBuildNativeStringRules(base base, rules *validate.StringRules) evaluator { + if rules == nil { + return nil + } + + // Bail out for custom predefined extensions. + if len(rules.ProtoReflect().GetUnknown()) > 0 { + return nil + } + + hasRule := false + + var wellKnownRule *stringWellKnownRule + var knownRegex validate.KnownRegex + var strict bool + + if rules.HasWellKnown() { + var err error + wellKnownRule, knownRegex, strict, err = parseStringWellKnown(rules) + if err != nil { + return nil + } + if wellKnownRule != nil { + rules.ProtoReflect().Clear(wellKnownRule.site.desc) + } + if knownRegex != validate.KnownRegex_KNOWN_REGEX_UNSPECIFIED { + rules.ProtoReflect().Clear(strDescs.wellKnownRegexSite.desc) + } + hasRule = true + } + + var constVal *string + if rules.HasConst() { + constVal = ptr(rules.GetConst()) + rules.ProtoReflect().Clear(strDescs.constSite.desc) + hasRule = true + } + + var exactLen *uint64 + if rules.HasLen() { + exactLen = ptr(rules.GetLen()) + rules.ProtoReflect().Clear(strDescs.lenSite.desc) + hasRule = true + } + + var minLen *uint64 + if rules.HasMinLen() { + minLen = ptr(rules.GetMinLen()) + rules.ProtoReflect().Clear(strDescs.minLenSite.desc) + hasRule = true + } + + var maxLen *uint64 + if rules.HasMaxLen() { + maxLen = ptr(rules.GetMaxLen()) + rules.ProtoReflect().Clear(strDescs.maxLenSite.desc) + hasRule = true + } + + var exactBytes *uint64 + if rules.HasLenBytes() { + exactBytes = ptr(rules.GetLenBytes()) + rules.ProtoReflect().Clear(strDescs.lenBytesSite.desc) + hasRule = true + } + + var minBytes *uint64 + if rules.HasMinBytes() { + minBytes = ptr(rules.GetMinBytes()) + rules.ProtoReflect().Clear(strDescs.minBytesSite.desc) + hasRule = true + } + + var maxBytes *uint64 + if rules.HasMaxBytes() { + maxBytes = ptr(rules.GetMaxBytes()) + rules.ProtoReflect().Clear(strDescs.maxBytesSite.desc) + hasRule = true + } + + var compiledPattern *regexp.Regexp + var patternStr string + if rules.HasPattern() { + patternStr = rules.GetPattern() + var err error + compiledPattern, err = regexp.Compile(patternStr) + if err != nil { + // Invalid regex — bail to CEL which will also report a CompilationError. + return nil + } + rules.ProtoReflect().Clear(strDescs.patternSite.desc) + hasRule = true + } + + var prefix *string + if rules.HasPrefix() { + prefix = ptr(rules.GetPrefix()) + rules.ProtoReflect().Clear(strDescs.prefixSite.desc) + hasRule = true + } + + var suffix *string + if rules.HasSuffix() { + suffix = ptr(rules.GetSuffix()) + rules.ProtoReflect().Clear(strDescs.suffixSite.desc) + hasRule = true + } + + var containsVal *string + if rules.HasContains() { + containsVal = ptr(rules.GetContains()) + rules.ProtoReflect().Clear(strDescs.containsSite.desc) + hasRule = true + } + + var notContains *string + if rules.HasNotContains() { + notContains = ptr(rules.GetNotContains()) + rules.ProtoReflect().Clear(strDescs.notContainsSite.desc) + hasRule = true + } + + var inVals []string + if inVals = rules.GetIn(); len(inVals) > 0 { + rules.ProtoReflect().Clear(strDescs.inSite.desc) + hasRule = true + } + + var notInVals []string + if notInVals = rules.GetNotIn(); len(notInVals) > 0 { + rules.ProtoReflect().Clear(strDescs.notInSite.desc) + hasRule = true + } + + if !hasRule { + return nil + } + + return nativeStringEval{ + base: base, + constVal: constVal, + inVals: inVals, + notInVals: notInVals, + exactLen: exactLen, + minLen: minLen, + maxLen: maxLen, + exactBytes: exactBytes, + minBytes: minBytes, + maxBytes: maxBytes, + pattern: compiledPattern, + patternStr: patternStr, + prefix: prefix, + suffix: suffix, + contains: containsVal, + notContains: notContains, + wellKnownRule: wellKnownRule, + knownRegex: knownRegex, + strict: strict, + } +} + +// stringDescriptors bundles the field descriptors for StringRules. +type stringDescriptors struct { + ruleDesc protoreflect.FieldDescriptor // FieldRules.string + + // Pre-built rule sites for the error path. Each pairs ruleDesc with the + // corresponding leaf descriptor, built once at init. + constSite ruleSite + lenSite ruleSite + minLenSite ruleSite + maxLenSite ruleSite + lenBytesSite ruleSite + minBytesSite ruleSite + maxBytesSite ruleSite + patternSite ruleSite + prefixSite ruleSite + suffixSite ruleSite + containsSite ruleSite + notContainsSite ruleSite + inSite ruleSite + notInSite ruleSite + wellKnownRegexSite ruleSite +} + +func makeStringDescriptors() stringDescriptors { + descriptors := stringDescriptors{ + ruleDesc: fieldRulesDesc.Fields().ByName("string"), + } + descriptors.constSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("const"), "string.const", "") + descriptors.lenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("len"), "string.len", "") + descriptors.minLenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("min_len"), "string.min_len", "") + descriptors.maxLenSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("max_len"), "string.max_len", "") + descriptors.lenBytesSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("len_bytes"), "string.len_bytes", "") + descriptors.minBytesSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("min_bytes"), "string.min_bytes", "") + descriptors.maxBytesSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("max_bytes"), "string.max_bytes", "") + descriptors.patternSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("pattern"), "string.pattern", "") + descriptors.prefixSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("prefix"), "string.prefix", "") + descriptors.suffixSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("suffix"), "string.suffix", "") + descriptors.containsSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("contains"), "string.contains", "") + descriptors.notContainsSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("not_contains"), "string.not_contains", "") + descriptors.inSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("in"), "string.in", "") + descriptors.notInSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("not_in"), "string.not_in", "") + descriptors.wellKnownRegexSite = makeRuleSite(descriptors.ruleDesc, rulesDesc.Fields().ByName("well_known_regex"), "", "") + return descriptors +} + +//nolint:gochecknoglobals +var strDescs = makeStringDescriptors() + +var ( + uuidRegexp = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + tuuidRegexp = regexp.MustCompile(`^[0-9a-fA-F]{32}$`) + ulidRegexp = regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`) + looseRegexp = regexp.MustCompile(`^[^\x00\x0A\x0D]+$`) + headerNameRegexp = regexp.MustCompile(`^:?[0-9a-zA-Z!#$%&'*+.\-^_|~\x60]+$`) + headerValueRegexp = regexp.MustCompile(`^[^\x00-\x08\x0A-\x1F\x7F]*$`) +) + +// stringWellKnownRule describes a well-known string format constraint. +// It bundles the field descriptor, rule IDs, messages, and validation +// function so that all well-known checks share a single generic method. +type stringWellKnownRule struct { + site ruleSite // pre-built rule path site for the error path + emptySite ruleSite // pre-built rule path site for the empty value check + validate func(string) bool +} + +//nolint:gochecknoglobals +var ( + rulesDesc = (*validate.StringRules)(nil).ProtoReflect().Descriptor() + + stringRuleEmail = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("email"), "string.email", "must be a valid email address"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("email"), "string.email_empty", "value is empty, which is not a valid email address"), + validate: rules.IsEmail, + } + stringRuleHostname = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("hostname"), "string.hostname", "must be a valid hostname"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("hostname"), "string.hostname_empty", "value is empty, which is not a valid hostname"), + validate: rules.IsHostname, + } + stringRuleIP = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip"), "string.ip", "must be a valid IP address"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip"), "string.ip_empty", "value is empty, which is not a valid IP address"), + validate: func(s string) bool { return rules.IsIP(s, 0) }, + } + stringRuleIPv4 = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4"), "string.ipv4", "must be a valid IPv4 address"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4"), "string.ipv4_empty", "value is empty, which is not a valid IPv4 address"), + validate: func(s string) bool { return rules.IsIP(s, 4) }, + } + stringRuleIPv6 = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6"), "string.ipv6", "must be a valid IPv6 address"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6"), "string.ipv6_empty", "value is empty, which is not a valid IPv6 address"), + validate: func(s string) bool { return rules.IsIP(s, 6) }, + } + stringRuleURI = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("uri"), "string.uri", "must be a valid URI"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("uri"), "string.uri_empty", "value is empty, which is not a valid URI"), + validate: rules.IsURI, + } + stringRuleURIRef = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("uri_ref"), "string.uri_ref", "must be a valid URI Reference"), + // emptySite is unused + validate: rules.IsURIRef, + } + stringRuleAddress = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("address"), "string.address", "must be a valid hostname, or ip address"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("address"), "string.address_empty", "value is empty, which is not a valid hostname, or ip address"), + validate: func(s string) bool { return rules.IsHostname(s) || rules.IsIP(s, 0) }, + } + stringRuleUUID = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("uuid"), "string.uuid", "must be a valid UUID"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("uuid"), "string.uuid_empty", "value is empty, which is not a valid UUID"), + validate: uuidRegexp.MatchString, + } + stringRuleTUUID = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("tuuid"), "string.tuuid", "must be a valid trimmed UUID"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("tuuid"), "string.tuuid_empty", "value is empty, which is not a valid trimmed UUID"), + validate: tuuidRegexp.MatchString, + } + stringRuleIPPrefixLen = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip_with_prefixlen"), "string.ip_with_prefixlen", "must be a valid IP prefix"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip_with_prefixlen"), "string.ip_with_prefixlen_empty", "value is empty, which is not a valid IP prefix"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 0, false) }, + } + stringRuleIPv4PrefixLen = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4_with_prefixlen"), "string.ipv4_with_prefixlen", "must be a valid IPv4 address with prefix length"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4_with_prefixlen"), "string.ipv4_with_prefixlen_empty", "value is empty, which is not a valid IPv4 address with prefix length"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 4, false) }, + } + stringRuleIPv6PrefixLen = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6_with_prefixlen"), "string.ipv6_with_prefixlen", "must be a valid IPv6 address with prefix length"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6_with_prefixlen"), "string.ipv6_with_prefixlen_empty", "value is empty, which is not a valid IPv6 address with prefix length"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 6, false) }, + } + stringRuleIPPrefix = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip_prefix"), "string.ip_prefix", "must be a valid IP prefix"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ip_prefix"), "string.ip_prefix_empty", "value is empty, which is not a valid IP prefix"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 0, true) }, + } + stringRuleIPv4Prefix = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4_prefix"), "string.ipv4_prefix", "must be a valid IPv4 prefix"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv4_prefix"), "string.ipv4_prefix_empty", "value is empty, which is not a valid IPv4 prefix"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 4, true) }, + } + stringRuleIPv6Prefix = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6_prefix"), "string.ipv6_prefix", "must be a valid IPv6 prefix"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ipv6_prefix"), "string.ipv6_prefix_empty", "value is empty, which is not a valid IPv6 prefix"), + validate: func(s string) bool { return rules.IsIPPrefix(s, 6, true) }, + } + stringRuleHostAndPort = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("host_and_port"), "string.host_and_port", "must be a valid host (hostname or IP address) and port pair"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("host_and_port"), "string.host_and_port_empty", "value is empty, which is not a valid host and port pair"), + validate: func(s string) bool { return rules.IsHostAndPort(s, true) }, + } + stringRuleULID = stringWellKnownRule{ + site: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ulid"), "string.ulid", "must be a valid ULID"), + emptySite: makeRuleSite(strDescs.ruleDesc, rulesDesc.Fields().ByName("ulid"), "string.ulid_empty", "value is empty, which is not a valid ULID"), + validate: ulidRegexp.MatchString, + } +) + +// nativeStringEval is a native Go evaluator for string rules. +// It replaces CEL evaluation with direct Go operations for +// const, in, not_in, len, min_len, max_len, len_bytes, min_bytes, +// max_bytes, pattern, prefix, suffix, contains, and not_contains. +type nativeStringEval struct { + base + constVal *string + inVals []string + notInVals []string + exactLen *uint64 + minLen *uint64 + maxLen *uint64 + exactBytes *uint64 + minBytes *uint64 + maxBytes *uint64 + pattern *regexp.Regexp + patternStr string + prefix *string + suffix *string + contains *string + notContains *string + wellKnownRule *stringWellKnownRule + knownRegex validate.KnownRegex + strict bool +} + +//nolint:gocyclo // this code has nested ifs but it's not hard to follow. +func (n nativeStringEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + strVal := val.String() + var violations []*Violation + + if n.exactLen != nil || n.minLen != nil || n.maxLen != nil { + runeCount := uint64(utf8.RuneCountInString(strVal)) //nolint:gosec // cannot be negative + if vs := n.evaluateLength(runeCount, val); len(vs) > 0 { + violations = append(violations, vs...) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + } + + if n.exactBytes != nil || n.minBytes != nil || n.maxBytes != nil { + byteCount := uint64(len(strVal)) + if vs := n.evaluateByteLength(byteCount, val); len(vs) > 0 { + violations = append(violations, vs...) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + } + + if n.constVal != nil && strVal != *n.constVal { + violations = append(violations, n.newViolation(strDescs.constSite, + "string.const", fmt.Sprintf("must equal `%s`", *n.constVal), + val, protoreflect.ValueOfString(*n.constVal))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if n.pattern != nil && !n.pattern.MatchString(strVal) { + violations = append(violations, n.newViolation(strDescs.patternSite, + "string.pattern", fmt.Sprintf("does not match regex pattern `%s`", n.patternStr), + val, protoreflect.ValueOfString(n.patternStr))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if n.prefix != nil && !strings.HasPrefix(strVal, *n.prefix) { + violations = append(violations, n.newViolation(strDescs.prefixSite, + "string.prefix", fmt.Sprintf("does not have prefix `%s`", *n.prefix), + val, protoreflect.ValueOfString(*n.prefix))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if n.suffix != nil && !strings.HasSuffix(strVal, *n.suffix) { + violations = append(violations, n.newViolation(strDescs.suffixSite, + "string.suffix", fmt.Sprintf("does not have suffix `%s`", *n.suffix), + val, protoreflect.ValueOfString(*n.suffix))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if n.contains != nil && !strings.Contains(strVal, *n.contains) { + violations = append(violations, n.newViolation(strDescs.containsSite, + "string.contains", fmt.Sprintf("does not contain substring `%s`", *n.contains), + val, protoreflect.ValueOfString(*n.contains))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if n.notContains != nil && strings.Contains(strVal, *n.notContains) { + violations = append(violations, n.newViolation(strDescs.notContainsSite, + "string.not_contains", fmt.Sprintf("contains substring `%s`", *n.notContains), + val, protoreflect.ValueOfString(*n.notContains))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if len(n.inVals) > 0 && !slices.Contains(n.inVals, strVal) { + violations = append(violations, n.newViolation(strDescs.inSite, + "string.in", "must be in list "+formatStringList(n.inVals), + val, sliceToListValue(&validate.StringRules{}, strDescs.inSite.desc, n.inVals, protoreflect.ValueOfString))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + if len(n.notInVals) > 0 && slices.Contains(n.notInVals, strVal) { + violations = append(violations, n.newViolation(strDescs.notInSite, + "string.not_in", "must not be in list "+formatStringList(n.notInVals), + val, sliceToListValue(&validate.StringRules{}, strDescs.notInSite.desc, n.notInVals, protoreflect.ValueOfString))) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + + //nolint:nestif // there are levels of nested ifs, but it's not hard to follow. + if n.wellKnownRule != nil { + if vs := n.checkWellKnown(strVal, val); len(vs) > 0 { + violations = append(violations, vs...) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + } else if n.knownRegex != 0 { + if vs := n.checkKnownRegex(strVal, val); len(vs) > 0 { + violations = append(violations, vs...) + if cfg.failFast { + return &ValidationError{Violations: violations[:1]} + } + } + } + + if len(violations) > 0 { + return &ValidationError{ + Violations: violations, + } + } + return nil +} + +func (n nativeStringEval) checkWellKnown(strVal string, val protoreflect.Value) []*Violation { + rule := n.wellKnownRule + if rule.emptySite.ruleID != nil && strVal == "" { + return []*Violation{n.newViolation(rule.emptySite, + "", "", + val, protoreflect.ValueOfBool(true))} + } + if !rule.validate(strVal) { + return []*Violation{n.newViolation(rule.site, + "", "", + val, protoreflect.ValueOfBool(true))} + } + return nil +} + +func (n nativeStringEval) checkKnownRegex(strVal string, val protoreflect.Value) []*Violation { + // check if strict is set (it is on by default) + // if not, just validate against the loose rule (^[^\u0000\u000A\u000D]+$) + // if yes, check whether this is a name or value and use the correct strict rule + // ^:?[0-9a-zA-Z!#$%&\\'*+-.^_|~\\x60]+$ for name + // ^[^\u0000-\u0008\u000A-\u001F\u007F]*$ for value + ruleValue := protoreflect.ValueOfEnum(protoreflect.EnumNumber(n.knownRegex)) + var matcher *regexp.Regexp + var rule string + var msg string + switch n.knownRegex { + case validate.KnownRegex_KNOWN_REGEX_HTTP_HEADER_NAME: + if strVal == "" { + return []*Violation{n.newViolation(strDescs.wellKnownRegexSite, + "string.well_known_regex.header_name_empty", "value is empty, which is not a valid HTTP header name", + val, ruleValue)} + } + matcher = headerNameRegexp + rule = "string.well_known_regex.header_name" + msg = "must be a valid HTTP header name" + case validate.KnownRegex_KNOWN_REGEX_HTTP_HEADER_VALUE: + matcher = headerValueRegexp + rule = "string.well_known_regex.header_value" + msg = "must be a valid HTTP header value" + default: + return nil // should never happen, but just in case + } + if !n.strict { + matcher = looseRegexp + } + if !matcher.MatchString(strVal) { + return []*Violation{n.newViolation(strDescs.wellKnownRegexSite, + rule, msg, + val, ruleValue)} + } + return nil +} + +// it would be worse to unify this and evaluateLength than it is to leave them as +// very similar bits of code +// +//nolint:dupl +func (n nativeStringEval) evaluateByteLength(byteCount uint64, val protoreflect.Value) []*Violation { + var out []*Violation + if n.exactBytes != nil && byteCount != *n.exactBytes { + out = append(out, n.newViolation(strDescs.lenBytesSite, + "string.len_bytes", fmt.Sprintf("must be %d bytes", *n.exactBytes), + val, protoreflect.ValueOfUint64(*n.exactBytes))) + } + if n.minBytes != nil && byteCount < *n.minBytes { + out = append(out, n.newViolation(strDescs.minBytesSite, + "string.min_bytes", fmt.Sprintf("must be at least %d bytes", *n.minBytes), + val, protoreflect.ValueOfUint64(*n.minBytes))) + } + if n.maxBytes != nil && byteCount > *n.maxBytes { + out = append(out, n.newViolation(strDescs.maxBytesSite, + "string.max_bytes", fmt.Sprintf("must be at most %d bytes", *n.maxBytes), + val, protoreflect.ValueOfUint64(*n.maxBytes))) + } + return out +} + +// it would be worse to unify this and evaluateByteLength than it is to leave them as +// very similar bits of code +// +//nolint:dupl +func (n nativeStringEval) evaluateLength(runeCount uint64, val protoreflect.Value) []*Violation { + var out []*Violation + + if n.exactLen != nil && runeCount != *n.exactLen { + out = append(out, n.newViolation(strDescs.lenSite, + "string.len", fmt.Sprintf("must be %d characters", *n.exactLen), + val, protoreflect.ValueOfUint64(*n.exactLen))) + } + if n.minLen != nil && runeCount < *n.minLen { + out = append(out, n.newViolation(strDescs.minLenSite, + "string.min_len", fmt.Sprintf("must be at least %d characters", *n.minLen), + val, protoreflect.ValueOfUint64(*n.minLen))) + } + if n.maxLen != nil && runeCount > *n.maxLen { + out = append(out, n.newViolation(strDescs.maxLenSite, + "string.max_len", fmt.Sprintf("must be at most %d characters", *n.maxLen), + val, protoreflect.ValueOfUint64(*n.maxLen))) + } + return out +} + +func (n nativeStringEval) Tautology() bool { + return false +} + +var _ evaluator = nativeStringEval{} + +var errUnsupportedWellKnown = errors.New("unsupported well-known string constraint") + +// parseStringWellKnown maps a StringRules well-known oneof to a +// *stringWellKnownRule (for format constraints) or a KnownRegex + +// strict flag (for well-known regex constraints). +func parseStringWellKnown(rules *validate.StringRules) ( + *stringWellKnownRule, validate.KnownRegex, bool, error, +) { + switch { + case rules.GetEmail(): + return &stringRuleEmail, 0, false, nil + case rules.GetHostname(): + return &stringRuleHostname, 0, false, nil + case rules.GetIp(): + return &stringRuleIP, 0, false, nil + case rules.GetIpv4(): + return &stringRuleIPv4, 0, false, nil + case rules.GetIpv6(): + return &stringRuleIPv6, 0, false, nil + case rules.GetUri(): + return &stringRuleURI, 0, false, nil + case rules.GetUriRef(): + return &stringRuleURIRef, 0, false, nil + case rules.GetAddress(): + return &stringRuleAddress, 0, false, nil + case rules.GetUuid(): + return &stringRuleUUID, 0, false, nil + case rules.GetTuuid(): + return &stringRuleTUUID, 0, false, nil + case rules.GetIpWithPrefixlen(): + return &stringRuleIPPrefixLen, 0, false, nil + case rules.GetIpv4WithPrefixlen(): + return &stringRuleIPv4PrefixLen, 0, false, nil + case rules.GetIpv6WithPrefixlen(): + return &stringRuleIPv6PrefixLen, 0, false, nil + case rules.GetIpPrefix(): + return &stringRuleIPPrefix, 0, false, nil + case rules.GetIpv4Prefix(): + return &stringRuleIPv4Prefix, 0, false, nil + case rules.GetIpv6Prefix(): + return &stringRuleIPv6Prefix, 0, false, nil + case rules.GetUlid(): + return &stringRuleULID, 0, false, nil + case rules.GetHostAndPort(): + return &stringRuleHostAndPort, 0, false, nil + case rules.GetWellKnownRegex() != validate.KnownRegex_KNOWN_REGEX_UNSPECIFIED: + knownRegex := rules.GetWellKnownRegex() + // strict is on by default or if it is explicitly set to true + strict := !rules.HasStrict() || rules.GetStrict() + // intentionally doesn't return a *stringWellKnownRule, because well known regex is a special case + return nil, knownRegex, strict, nil + default: + return nil, 0, false, errUnsupportedWellKnown + } +} + +// formatStringList formats a []string as [a, b] to match CEL's +// string list formatting (no quoting around elements). +func formatStringList(vals []string) string { + return "[" + strings.Join(vals, ", ") + "]" +} diff --git a/vendor/buf.build/go/protovalidate/option.go b/vendor/buf.build/go/protovalidate/option.go index a132dbac..7eafc764 100644 --- a/vendor/buf.build/go/protovalidate/option.go +++ b/vendor/buf.build/go/protovalidate/option.go @@ -22,7 +22,7 @@ import ( ) // A ValidatorOption modifies the default configuration of a Validator. See the -// individual options for their defaults and affects on the fallibility of +// individual options for their defaults and effect on the fallibility of // configuring a Validator. type ValidatorOption interface { applyToValidator(cfg *config) @@ -105,6 +105,12 @@ func WithFailFast() Option { return &failFastOption{} } +// WithDisableNativeRules specifies whether validation should always use CEL rules. +// By default, native rules are used when they exist. +func WithDisableNativeRules() ValidatorOption { + return &disableNativeRulesOption{} +} + // WithNowFunc specifies the function used to derive the `now` variable in CEL // expressions. By default, [timestamppb.Now] is used. func WithNowFunc(fn func() *timestamppb.Timestamp) Option { @@ -159,6 +165,12 @@ func (o *failFastOption) applyToValidation(cfg *validationConfig) { cfg.failFast = true } +type disableNativeRulesOption struct{} + +func (o *disableNativeRulesOption) applyToValidator(cfg *config) { + cfg.disableNativeRules = true +} + type nowFuncOption func() *timestamppb.Timestamp func (o nowFuncOption) applyToValidator(cfg *config) { diff --git a/vendor/buf.build/go/protovalidate/validator.go b/vendor/buf.build/go/protovalidate/validator.go index 2267d193..b9107a6a 100644 --- a/vendor/buf.build/go/protovalidate/validator.go +++ b/vendor/buf.build/go/protovalidate/validator.go @@ -84,6 +84,7 @@ func New(options ...ValidatorOption) (Validator, error) { cfg.disableLazy, cfg.extensionTypeResolver, cfg.allowUnknownFields, + cfg.disableNativeRules, cfg.desc..., ) @@ -142,6 +143,7 @@ type config struct { extensionTypeResolver protoregistry.ExtensionTypeResolver allowUnknownFields bool nowFn func() *timestamppb.Timestamp + disableNativeRules bool } type validationConfig struct { diff --git a/vendor/buf.build/go/protovalidate/wrapper.go b/vendor/buf.build/go/protovalidate/wrapper.go new file mode 100644 index 00000000..f64db8c3 --- /dev/null +++ b/vendor/buf.build/go/protovalidate/wrapper.go @@ -0,0 +1,42 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protovalidate + +import "google.golang.org/protobuf/reflect/protoreflect" + +// wrappedValueEval adapts a native evaluator built against a wrapper WKT's +// inner scalar field (e.g. google.protobuf.Int32Value.value) so it can run +// when the outer value is the wrapper message itself. At evaluation time it +// extracts the inner scalar via Message().Get(innerField) and delegates. +// +// processWrapperRules calls buildValue with the inner "value" field +// descriptor, but appends the resulting evaluators onto the outer value +// whose Descriptor still points at the wrapper message field. Without this +// adapter, native evaluators that call val.Int()/Float()/etc. would see the +// wrapper message and panic. +type wrappedValueEval struct { + innerField protoreflect.FieldDescriptor + inner evaluator +} + +func (w wrappedValueEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return w.inner.Evaluate(msg, val.Message().Get(w.innerField), cfg) +} + +func (w wrappedValueEval) Tautology() bool { + return w.inner.Tautology() +} + +var _ evaluator = wrappedValueEval{} diff --git a/vendor/github.com/google/cel-go/cel/BUILD.bazel b/vendor/github.com/google/cel-go/cel/BUILD.bazel index 46cb26d6..62a56036 100644 --- a/vendor/github.com/google/cel-go/cel/BUILD.bazel +++ b/vendor/github.com/google/cel-go/cel/BUILD.bazel @@ -26,6 +26,7 @@ go_library( importpath = "github.com/google/cel-go/cel", visibility = ["//visibility:public"], deps = [ + "//cel/async:go_default_library", "//checker:go_default_library", "//checker/decls:go_default_library", "//common:go_default_library", @@ -70,6 +71,7 @@ go_test( "inlining_test.go", "io_test.go", "optimizer_test.go", + "program_async_test.go", "prompt_test.go", "validator_test.go", ], @@ -84,6 +86,7 @@ go_test( "//cel/testdata:test_fds_with_source_info", ], deps = [ + "//cel/async:go_default_library", "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/types:go_default_library", diff --git a/vendor/github.com/google/cel-go/cel/async/BUILD.bazel b/vendor/github.com/google/cel-go/cel/async/BUILD.bazel new file mode 100644 index 00000000..85b28bcd --- /dev/null +++ b/vendor/github.com/google/cel-go/cel/async/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + licenses = ["notice"], # Apache 2.0 +) + +go_library( + name = "go_default_library", + srcs = [ + "async.go", + ], + importpath = "github.com/google/cel-go/cel/async", + visibility = ["//visibility:public"], + deps = [ + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + "//interpreter:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "async_test.go", + ], + deps = [ + ":go_default_library", + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + ], +) diff --git a/vendor/github.com/google/cel-go/cel/async/async.go b/vendor/github.com/google/cel-go/cel/async/async.go new file mode 100644 index 00000000..a011114b --- /dev/null +++ b/vendor/github.com/google/cel-go/cel/async/async.go @@ -0,0 +1,235 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package async provides helpers for configuring and executing asynchronous CEL functions, +// including drain strategies, retry, timeout, concurrency limiting, and caching wrappers. +package async + +import ( + "context" + "errors" + "time" + + "github.com/google/cel-go/common/decls" + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" +) + +// Call describes a pending or completed asynchronous function call. +// This interface exposes a safe, read-only view of the internal interpreter state. +type Call = interpreter.AsyncCall + +// Observer provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: the start and finish callbacks run on different +// goroutines, and finish callbacks for distinct calls may run concurrently. See +// interpreter.AsyncObserver for details. +type Observer = interpreter.AsyncObserver + +// BlockingOp is a blocking asynchronous function operation. +type BlockingOp = functions.BlockingAsyncOp + +// DrainAction dictates what ConcurrentEval should do after inspecting completions. +type DrainAction struct { + // Reevaluate indicates that the AST should be re-evaluated immediately. + // If true, WaitDuration is ignored. + Reevaluate bool + // WaitDuration indicates how long the evaluator should wait for additional + // completions before deciding to re-evaluate. A duration of 0 means wait + // indefinitely (block on the next completion). + WaitDuration time.Duration +} + +// DrainStrategy controls when ConcurrentEval re-evaluates after async completions. +// +// The evaluator consults the strategy each time a completion is received. +type DrainStrategy interface { + // NextAction evaluates the current state of asynchronous evaluation and + // determines the next step. + // + // - completed: The set of completions accumulated in the current batch. + // - active: The number of async calls currently launched but unresolved. + NextAction(completed []Call, active int) DrainAction +} + +// DrainNone returns a strategy that re-evaluates after every single completion. +// This is the default strategy. +func DrainNone() DrainStrategy { + return drainNone{} +} + +type drainNone struct{} + +func (drainNone) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0 || len(completed) > 0} +} + +// DrainReady returns a strategy that waits for a short duration after the first +// completion to batch any other functions that complete at roughly the same time. +func DrainReady(debounce time.Duration) DrainStrategy { + return drainReady{debounce: debounce} +} + +type drainReady struct { + debounce time.Duration +} + +func (d drainReady) NextAction(completed []Call, active int) DrainAction { + if active == 0 { + return DrainAction{Reevaluate: true} // Nothing left to wait for + } + if len(completed) == 0 { + return DrainAction{Reevaluate: false, WaitDuration: 0} // Wait indefinitely for first + } + return DrainAction{Reevaluate: false, WaitDuration: d.debounce} // Wait for debounce period +} + +// DrainAll returns a strategy that waits for all currently pending calls to +// complete before re-evaluating. +// +// Note: This strategy is optimal for independent async calls, but will over-wait +// if some calls depend on the results of others. +func DrainAll() DrainStrategy { + return drainAll{} +} + +type drainAll struct{} + +func (drainAll) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0} +} + +// Timeout wraps a BlockingAsyncOp with a per-call timeout. +// +// The timeout is enforced even when the wrapped function ignores its context: the function runs on +// its own goroutine and Timeout selects on the deadline, returning a timeout error when it +// fires. A function that ignores cancellation cannot be forcibly stopped (Go cannot kill a +// goroutine), so its goroutine continues running in the background until it returns on its own; +// only its result is abandoned. This is the recommended way to bound functions that may hang or +// are not under the caller's control. The extra goroutine is incurred only by Timeout-wrapped +// calls, not by async evaluation in general. +func Timeout(fn functions.BlockingAsyncOp, timeout time.Duration) functions.BlockingAsyncOp { + return func(ctx context.Context, args ...ref.Val) ref.Val { + tCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + resCh := make(chan ref.Val, 1) + go func() { resCh <- fn(tCtx, args...) }() + select { + case res := <-resCh: + return res + case <-tCtx.Done(): + return types.NewErr("operation timed out after %v: %v", timeout, tCtx.Err()) + } + } +} + +// TimeoutBinding wraps a BlockingAsyncOp with a per-call timeout and returns an OverloadOpt. +func TimeoutBinding(fn functions.BlockingAsyncOp, timeout time.Duration) decls.OverloadOpt { + return decls.AsyncBinding(Timeout(fn, timeout)) +} + +// RetryOption configures the behavior of RetryBinding. +type RetryOption func(*retryConfig) + +type retryConfig struct { + maxAttempts int + backoff time.Duration +} + +// RetryAttempts sets the maximum number of attempts (including the first one). +func RetryAttempts(attempts int) RetryOption { + return func(c *retryConfig) { + c.maxAttempts = attempts + } +} + +// RetryBackoff sets the fixed backoff duration between attempts. +func RetryBackoff(backoff time.Duration) RetryOption { + return func(c *retryConfig) { + c.backoff = backoff + } +} + +// RetryableError is an interface that errors can implement to signal whether they are retryable. +type RetryableError interface { + error + IsRetryable() bool +} + +// Retry wraps a BlockingAsyncOp with a retry policy. +// It will retry the operation if it returns a types.Err that wraps a RetryableError returning true for IsRetryable. +func Retry(fn functions.BlockingAsyncOp, opts ...RetryOption) functions.BlockingAsyncOp { + config := &retryConfig{ + maxAttempts: 3, + backoff: 100 * time.Millisecond, + } + for _, opt := range opts { + opt(config) + } + + return func(ctx context.Context, args ...ref.Val) ref.Val { + var lastErr ref.Val + var backoff *time.Timer + defer func() { + if backoff != nil { + backoff.Stop() + } + }() + for i := 0; i < config.maxAttempts; i++ { + if i > 0 { + // Reuse a single timer across attempts and stop it on cancellation so the + // pending timer is not left to fire after the call returns. + if backoff == nil { + backoff = time.NewTimer(config.backoff) + } else { + backoff.Reset(config.backoff) + } + select { + case <-backoff.C: + case <-ctx.Done(): + backoff.Stop() + return types.NewErr("operation cancelled during retry: %v", ctx.Err()) + } + } + + res := fn(ctx, args...) + if !types.IsError(res) { + return res + } + + err := res.(*types.Err) + lastErr = res + + if !isRetryable(err) { + return res + } + } + return lastErr + } +} + +// RetryBinding wraps a BlockingAsyncOp with a retry policy and returns an OverloadOpt. +func RetryBinding(fn functions.BlockingAsyncOp, opts ...RetryOption) decls.OverloadOpt { + return decls.AsyncBinding(Retry(fn, opts...)) +} + +func isRetryable(err *types.Err) bool { + var re RetryableError + if errors.As(err, &re) { + return re.IsRetryable() + } + return false +} diff --git a/vendor/github.com/google/cel-go/cel/cel.go b/vendor/github.com/google/cel-go/cel/cel.go index eb5a9f4c..9ba957a7 100644 --- a/vendor/github.com/google/cel-go/cel/cel.go +++ b/vendor/github.com/google/cel-go/cel/cel.go @@ -17,3 +17,24 @@ // CEL is a non-Turing complete expression language designed to parse, check, and evaluate // expressions against user-defined environments. package cel + +// Compile is a convenience function that constructs a new Env using the provided EnvOption values, +// compiles the expression string, and plans an executable Program. +// +// Warning: Creating a new environment for every compilation is expensive. Environment setup should be done once +// and shared across expression compilations when the options remain the same. +func Compile(expression string, opts ...EnvOption) (Program, error) { + env, err := NewEnv(opts...) + if err != nil { + return nil, err + } + ast, iss := env.Compile(expression) + if iss.Err() != nil { + return nil, iss.Err() + } + prg, err := env.Program(ast, EvalOptions(OptOptimize)) + if err != nil { + return nil, err + } + return prg, nil +} diff --git a/vendor/github.com/google/cel-go/cel/decls.go b/vendor/github.com/google/cel-go/cel/decls.go index 4d4873bd..c7c23fd5 100644 --- a/vendor/github.com/google/cel-go/cel/decls.go +++ b/vendor/github.com/google/cel-go/cel/decls.go @@ -346,6 +346,32 @@ func LateFunctionBinding() OverloadOpt { return decls.LateFunctionBinding() } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +// +// Context contract: the function MUST return promptly once its context is cancelled. The +// framework cannot forcibly terminate the goroutine running the function, so a function that +// ignores cancellation will leak its goroutine and hold a concurrency slot (see +// AsyncMaxConcurrency) until it returns on its own. For functions that may hang or that are not +// under your control, wrap them with async.TimeoutBinding to bound their runtime. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return decls.AsyncBinding(fn) +} + +// SingletonAsyncBinding creates a singleton async function definition from a blocking function, +// to be used with all function overloads. The provided function is called in its own goroutine +// with the provided context. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + return decls.SingletonAsyncBinding(fn, traits...) +} + // OverloadIsNonStrict enables the function to be called with error and unknown argument values. // // Note: do not use this option unless absoluately necessary as it should be an uncommon feature. diff --git a/vendor/github.com/google/cel-go/cel/env.go b/vendor/github.com/google/cel-go/cel/env.go index e2de2ff6..784790ba 100644 --- a/vendor/github.com/google/cel-go/cel/env.go +++ b/vendor/github.com/google/cel-go/cel/env.go @@ -48,6 +48,10 @@ type Source = common.Source type Ast struct { source Source impl *celast.AST + // loadErr captures an error detected while loading the AST (e.g. an over-deep AST ingested via + // ParsedExprToAst / CheckedExprToAst) so it can be surfaced when the Ast is checked or planned + // instead of recursing into the checker or planner on adversarially deep input. + loadErr error } // NativeRep converts the AST to a Go-native representation. @@ -395,6 +399,20 @@ func NewCustomEnv(opts ...EnvOption) (*Env, error) { // It is possible to have both non-nil Ast and Issues values returned from this call: however, // the mere presence of an Ast does not imply that it is valid for use. func (e *Env) Check(ast *Ast) (*Ast, *Issues) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) before recursing into the type checker on it. + if ast != nil && ast.loadErr != nil { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, ast.loadErr.Error()) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + if nodeLimit := e.configuredExpressionNodeLimit(); nodeLimit > 0 && ast != nil && ast.NativeRep() != nil { + if count := celast.NodeCount(ast.NativeRep()); count > nodeLimit { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, fmt.Sprintf("expression node count exceeds limit: count %d, limit %d", count, nodeLimit)) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + } // Construct the internal checker env, erroring if there is an issue adding the declarations. chk, err := e.initChecker() if err != nil { @@ -436,6 +454,24 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { return ast, nil } +// configuredExpressionSizeLimit returns the effective expression size code point limit. +// A zero value means "use the parser default". +func (e *Env) configuredExpressionSizeLimit() int { + if l := e.limits[limitCodePointSize]; l != 0 { + return l + } + return 100_000 +} + +// configuredExpressionNodeLimit returns the effective expression node limit. +// A zero value means "use default". +func (e *Env) configuredExpressionNodeLimit() int { + if l := e.limits[limitExpressionNodeCount]; l != 0 { + return l + } + return 100_000 +} + // Compile combines the Parse and Check phases CEL program compilation to produce an Ast and // associated issues. // @@ -445,7 +481,11 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { // // Note, for parse-only uses of CEL use Parse. func (e *Env) Compile(txt string) (*Ast, *Issues) { - return e.CompileSource(common.NewTextSource(txt)) + src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit()) + if err != nil { + return nil, ErrorAsIssues(err) + } + return e.CompileSource(src) } // CompileSource combines the Parse and Check phases CEL program compilation to produce an Ast and @@ -650,7 +690,10 @@ func (e *Env) Validators() []ASTValidator { // This form of Parse creates a Source value for the input `txt` and forwards to the // ParseSource method. func (e *Env) Parse(txt string) (*Ast, *Issues) { - src := common.NewTextSource(txt) + src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit()) + if err != nil { + return nil, ErrorAsIssues(err) + } return e.ParseSource(src) } @@ -671,6 +714,12 @@ func (e *Env) ParseSource(src Source) (*Ast, *Issues) { // Program generates an evaluable instance of the Ast within the environment (Env). func (e *Env) Program(ast *Ast, opts ...ProgramOption) (Program, error) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) rather than recursing into the planner on it. This is a + // cheap field read; the depth traversal itself runs once at conversion time, not here. + if ast != nil && ast.loadErr != nil { + return nil, ast.loadErr + } return e.PlanProgram(ast.NativeRep(), opts...) } @@ -843,6 +892,9 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { if l := e.limits[limitParseRecursionDepth]; l != 0 { prsrOpts = append(prsrOpts, parser.MaxRecursionDepth(l)) } + if l := e.limits[limitExpressionNodeCount]; l != 0 { + prsrOpts = append(prsrOpts, parser.MaxExpressionNodeCount(l)) + } e.prsr, err = parser.NewParser(prsrOpts...) if err != nil { return nil, err diff --git a/vendor/github.com/google/cel-go/cel/folding.go b/vendor/github.com/google/cel-go/cel/folding.go index d1ea6b19..5525f080 100644 --- a/vendor/github.com/google/cel-go/cel/folding.go +++ b/vendor/github.com/google/cel-go/cel/folding.go @@ -15,6 +15,8 @@ package cel import ( + "context" + "errors" "fmt" "github.com/google/cel-go/common/ast" @@ -93,18 +95,18 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) for _, fold := range foldableExprs { // If the expression could be folded because it's a non-strict call, and the // branches are pruned, continue to the next fold. - if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, fold) { + if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, a, fold) { continue } // Late-bound function calls cannot be folded. - if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, fold) { + if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, fold) { continue } // Otherwise, assume all context is needed to evaluate the expression. err := opt.tryFold(ctx, a, fold) - // Ignore errors for identifiers, since there is no guarantee that the environment + // Ignore errors for identifiers or subexpressions that cannot be folded, since there is no guarantee that the environment // has a value for them. - if err != nil && fold.Kind() != ast.IdentKind { + if err != nil && fold.Kind() != ast.IdentKind && !errors.Is(err, errCannotFold) { ctx.ReportErrorAtID(fold.ID(), "constant-folding evaluation failed: %v", err.Error()) return a } @@ -142,24 +144,19 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) return a } +var errCannotFold = errors.New("subexpression cannot be folded") + // tryFold attempts to evaluate a sub-expression to a literal. // // If the evaluation succeeds, the input expr value will be modified to become a literal, otherwise // the method will return an error. func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) error { - // Assume all context is needed to evaluate the expression. - subAST := &Ast{ - impl: ast.NewCheckedAST(ast.NewAST(expr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), - } - prg, err := ctx.Program(subAST) - if err != nil { - return err - } activation := opt.knownValues if activation == nil { activation = NoVars() } - out, _, err := prg.Eval(activation) + navExpr := expr.(ast.NavigableExpr) + out, err := evaluateExpr(ctx, a, navExpr, activation) if err != nil { return err } @@ -168,7 +165,31 @@ func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, return nil } -func isLateBoundFunctionCall(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) bool { +func evaluateExpr(ctx *OptimizerContext, a *ast.AST, navigableExpr ast.NavigableExpr, activation Activation) (ref.Val, error) { + partialActivation, err := ctx.PartialVars(activation) + if err != nil { + return nil, err + } + subAST := &Ast{ + impl: ast.NewCheckedAST(ast.NewAST(navigableExpr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), + } + prg, err := ctx.Program(subAST) + if err != nil { + return nil, err + } + // Folding will not attempt to call async functions which are all marked as late-bound, + // but the presence of such functions requires the use of `ConcurrentEval` in order to + // avoid an early return error which blocks async functions from running in `Eval` and + // `ContextEval` call paths. + resCh := prg.ConcurrentEval(context.Background(), partialActivation) + res := <-resCh + if res.Err != nil || types.IsUnknown(res.Val) { + return nil, errCannotFold + } + return res.Val, nil +} + +func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { call := expr.AsCall() function := ctx.Functions()[call.FunctionName()] if function == nil { @@ -181,12 +202,12 @@ func isLateBoundFunctionCall(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) b // a branch can be removed. Evaluation will naturally prune logical and / or calls, // but conditional will not be pruned cleanly, so this is one small area where the // constant folding step reimplements a portion of the evaluator. -func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { +func maybePruneBranches(ctx *OptimizerContext, a *ast.AST, expr ast.NavigableExpr) bool { call := expr.AsCall() args := call.Args() switch call.FunctionName() { case operators.LogicalAnd, operators.LogicalOr: - return maybeShortcircuitLogic(ctx, call.FunctionName(), args, expr) + return maybeShortcircuitLogic(ctx, a, call.FunctionName(), args, expr) case operators.Conditional: cond := args[0] truthy := args[1] @@ -207,11 +228,17 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { return true } needle := args[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() + if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind { + needleIsLit := needle.Kind() == ast.LiteralKind + needleLitVal := needle.AsLiteral() + needleIdentVal := needle.AsIdent() list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { + for _, elem := range list.Elements() { + if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True { + ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) + return true + } + if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal { ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) return true } @@ -221,7 +248,7 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { return false } -func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.Expr, expr ast.NavigableExpr) bool { +func maybeShortcircuitLogic(ctx *OptimizerContext, a *ast.AST, function string, args []ast.Expr, expr ast.NavigableExpr) bool { shortcircuit := types.False skip := types.True if function == operators.LogicalOr { @@ -244,10 +271,14 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E } if len(newArgs) == 0 { newArgs = append(newArgs, args[0]) - ctx.UpdateExpr(expr, newArgs[0]) - return true + } + if len(newArgs) == len(args) { + return false } if len(newArgs) == 1 { + if !isBoolType(a, newArgs[0]) { + return false + } ctx.UpdateExpr(expr, newArgs[0]) return true } @@ -255,6 +286,16 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E return true } +func isBoolType(a *ast.AST, e ast.Expr) bool { + if a != nil && a.GetType(e.ID()) == types.BoolType { + return true + } + if e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == types.BoolType { + return true + } + return false +} + // pruneOptionalElements works from the bottom up to resolve optional elements within // aggregate literals. // @@ -285,9 +326,9 @@ func pruneOptionalListElements(ctx *OptimizerContext, e ast.Expr) { updatedElems := []ast.Expr{} updatedIndices := []int32{} newOptIndex := -1 - for _, e := range elems { + for i, e := range elems { newOptIndex++ - if !l.IsOptional(int32(newOptIndex)) { + if !l.IsOptional(int32(i)) { updatedElems = append(updatedElems, e) continue } @@ -501,7 +542,7 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, sel := e.AsSelect() // guaranteed to be a navigable value return constantMatcher(sel.Operand().(ast.NavigableExpr)) case ast.IdentKind: - return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil + return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil && !hasComprehensionVar(e) case ast.ComprehensionKind: if isNestedComprehension(e) { return false @@ -513,12 +554,15 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, nested := e.AsComprehension() vars[nested.AccuVar()] = true vars[nested.IterVar()] = true + if nested.IterVar2() != "" { + vars[nested.IterVar2()] = true + } } if e.Kind() == ast.IdentKind && !vars[e.AsIdent()] { constantExprs = false } // Late-bound function calls cannot be folded. - if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, e) { + if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, e) { constantExprs = false } }) @@ -554,17 +598,33 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } } + if fnName == operators.Equals || fnName == operators.NotEquals { + if hasComprehensionVar(e) { + return false + } + if isExprConstantOfKind(children[0], types.BoolType) || isExprConstantOfKind(children[1], types.BoolType) { + return true + } + } if fnName == operators.In { + if hasComprehensionVar(e) { + return false + } haystack := children[1] if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 { return true } needle := children[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() + if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind { + needleIsLit := needle.Kind() == ast.LiteralKind + needleLitVal := needle.AsLiteral() + needleIdentVal := needle.AsIdent() list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { + for _, elem := range list.Elements() { + if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True { + return true + } + if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal { return true } } @@ -579,6 +639,74 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } +// isSelfEqualIdent indicates whether the expression is an identifier whose static type +// guarantees that its runtime value is equal to itself. +// +// Matching an identifier against a list element by name only proves list membership when the +// value the name resolves to is self-equal. A double may be NaN, which is not equal to itself, +// and dyn, abstract, and struct types may all hold a NaN at runtime, so the check is limited +// to the scalar types which cannot, and to the aggregate types whose type parameters are +// themselves self-equal. +func isSelfEqualIdent(e ast.Expr) bool { + if e.Kind() != ast.IdentKind { + return false + } + nav, ok := e.(ast.NavigableExpr) + if !ok { + return false + } + return isSelfEqualType(nav.Type()) +} + +// isSelfEqualType indicates whether all runtime values of the given type are equal to themselves. +func isSelfEqualType(t *types.Type) bool { + if t == nil { + return false + } + switch t.Kind() { + case types.BoolKind, types.BytesKind, types.DurationKind, types.IntKind, + types.NullTypeKind, types.StringKind, types.TimestampKind, types.TypeKind, + types.UintKind: + return true + case types.ListKind, types.MapKind: + // Aggregates compare element-wise, so they are self-equal exactly when their type + // parameters are. A list(dyn) or map(string, double) may still contain a NaN. + for _, p := range t.Parameters() { + if !isSelfEqualType(p) { + return false + } + } + return true + default: + return false + } +} + +func isExprConstantOfKind(e ast.Expr, t *types.Type) bool { + return e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == t +} + +func hasComprehensionVar(e ast.NavigableExpr) bool { + idents := ast.MatchDescendants(e, ast.KindMatcher(ast.IdentKind)) + for _, identNode := range idents { + identName := identNode.AsIdent() + curr := identNode + parent, found := curr.Parent() + for found { + if parent.Kind() == ast.ComprehensionKind { + compre := parent.AsComprehension() + if (compre.AccuVar() == identName || compre.IterVar() == identName || compre.IterVar2() == identName) && + curr.ID() != compre.IterRange().ID() && curr.ID() != compre.AccuInit().ID() { + return true + } + } + curr = parent + parent, found = parent.Parent() + } + } + return false +} + func isNestedComprehension(e ast.NavigableExpr) bool { parent, found := e.Parent() for found { diff --git a/vendor/github.com/google/cel-go/cel/io.go b/vendor/github.com/google/cel-go/cel/io.go index 2e611228..c991c95c 100644 --- a/vendor/github.com/google/cel-go/cel/io.go +++ b/vendor/github.com/google/cel-go/cel/io.go @@ -52,7 +52,12 @@ func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src Source) (*A if err != nil { return nil, err } - return &Ast{source: src, impl: checked}, nil + out := &Ast{source: src, impl: checked} + if err := checkLoadedASTDepth(checked); err != nil { + out.loadErr = err + return out, err + } + return out, nil } // AstToCheckedExpr converts an Ast to an protobuf CheckedExpr value. @@ -83,7 +88,26 @@ func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src Source) *Ast { src = common.NewInfoSource(parsedExpr.GetSourceInfo()) } e, _ := ast.ProtoToExpr(parsedExpr.GetExpr()) - return &Ast{source: src, impl: ast.NewAST(e, info)} + out := &Ast{source: src, impl: ast.NewAST(e, info)} + // ParsedExprToAstWithSource has no error return, so record an over-depth violation on the Ast + // to be surfaced when it is later checked or planned. + out.loadErr = checkLoadedASTDepth(out.impl) + return out +} + +// checkLoadedASTDepth guards ASTs that enter through the proto conversion helpers +// (ParsedExprToAst / CheckedExprToAst) against nesting deeper than the parser's recursion limit. +// Those entry points bypass the parser, so without this check a deeply nested loaded AST could +// exhaust the Go stack during later checking or planning. It returns a normal error rather than +// risking that overflow; the traversal itself is bounded so it stays safe on the same input. +// +// Embedders that fully control their AST inputs can skip this by building the AST through the +// common/ast package directly instead of these conversion helpers. +func checkLoadedASTDepth(a *ast.AST) error { + if ast.ExceedsDepth(a, defaultMaxASTDepth) { + return fmt.Errorf("input exceeds maximum expression nesting depth: %d", defaultMaxASTDepth) + } + return nil } // AstToParsedExpr converts an Ast to an protobuf ParsedExpr value. diff --git a/vendor/github.com/google/cel-go/cel/library.go b/vendor/github.com/google/cel-go/cel/library.go index 3c8b6ba3..332eb3f1 100644 --- a/vendor/github.com/google/cel-go/cel/library.go +++ b/vendor/github.com/google/cel-go/cel/library.go @@ -590,7 +590,7 @@ func (lib *optionalLib) CompileOptions() []EnvOption { // ProgramOptions implements the Library interface method. func (lib *optionalLib) ProgramOptions() []ProgramOption { return []ProgramOption{ - CustomDecorator(decorateOptionalOr), + CustomDecoratorV2(decorateOptionalOr), } } @@ -683,7 +683,7 @@ func EnableErrorOnBadPresenceTest(value bool) EnvOption { return features(featureEnableErrorOnBadPresenceTest, value) } -func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable, error) { +func decorateOptionalOr(i interpreter.InterpretableV2) (interpreter.InterpretableV2, error) { call, ok := i.(interpreter.InterpretableCall) if !ok { return i, nil @@ -720,8 +720,8 @@ func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable, // the second optional expression is evaluated and returned. type evalOptionalOr struct { id int64 - lhs interpreter.Interpretable - rhs interpreter.Interpretable + lhs interpreter.InterpretableV2 + rhs interpreter.InterpretableV2 } // ID implements the Interpretable interface method. @@ -729,11 +729,9 @@ func (opt *evalOptionalOr) ID() int64 { return opt.id } -// Eval evaluates the left-hand side optional to determine whether it contains a value, else -// proceeds with the right-hand side evaluation. -func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val { +func (opt *evalOptionalOr) Exec(frame *interpreter.ExecutionFrame) ref.Val { // short-circuit lhs. - optLHS := opt.lhs.Eval(ctx) + optLHS := opt.lhs.Exec(frame) switch val := optLHS.(type) { case *types.Err, *types.Unknown: return optLHS @@ -741,18 +739,24 @@ func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val { if val.HasValue() { return optLHS } - return opt.rhs.Eval(ctx) + return opt.rhs.Exec(frame) default: return types.NoSuchOverloadErr() } } +// Eval evaluates the left-hand side optional to determine whether it contains a value, else +// proceeds with the right-hand side evaluation. +func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val { + return opt.Exec(interpreter.AsFrame(ctx)) +} + // evalOptionalOrValue selects between an optional or a concrete value. If the optional has a value, // its value is returned, otherwise the alternative value expression is evaluated and returned. type evalOptionalOrValue struct { id int64 - lhs interpreter.Interpretable - rhs interpreter.Interpretable + lhs interpreter.InterpretableV2 + rhs interpreter.InterpretableV2 } // ID implements the Interpretable interface method. @@ -760,11 +764,9 @@ func (opt *evalOptionalOrValue) ID() int64 { return opt.id } -// Eval evaluates the left-hand side optional to determine whether it contains a value, else -// proceeds with the right-hand side evaluation. -func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val { +func (opt *evalOptionalOrValue) Exec(frame *interpreter.ExecutionFrame) ref.Val { // short-circuit lhs. - optLHS := opt.lhs.Eval(ctx) + optLHS := opt.lhs.Exec(frame) switch val := optLHS.(type) { case *types.Err, *types.Unknown: @@ -773,12 +775,18 @@ func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val { if val.HasValue() { return val.GetValue() } - return opt.rhs.Eval(ctx) + return opt.rhs.Exec(frame) default: return types.NoSuchOverloadErr() } } +// Eval evaluates the left-hand side optional to determine whether it contains a value, else +// proceeds with the right-hand side evaluation. +func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val { + return opt.Exec(interpreter.AsFrame(ctx)) +} + type timeLegacyLibrary struct{} func (timeLegacyLibrary) CompileOptions() []EnvOption { diff --git a/vendor/github.com/google/cel-go/cel/options.go b/vendor/github.com/google/cel-go/cel/options.go index d7d2ab03..540ad38b 100644 --- a/vendor/github.com/google/cel-go/cel/options.go +++ b/vendor/github.com/google/cel-go/cel/options.go @@ -24,6 +24,7 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" + "github.com/google/cel-go/cel/async" "github.com/google/cel-go/checker" "github.com/google/cel-go/common/containers" "github.com/google/cel-go/common/decls" @@ -109,12 +110,23 @@ const ( limitCodePointSize // The number of attempts to recover from a parse error. limitParseErrorRecovery + // The maximum nesting depth permitted for ASTs loaded outside the parser. + limitMaxASTDepth + // The maximum number of expression nodes permitted in parsing (including macro expansion). + limitExpressionNodeCount ) +// defaultMaxASTDepth mirrors the parser's default maxRecursionDepth (250) and +// is applied to ASTs that enter through non-parser ingestion paths (e.g. via +// ParsedExprToAst / CheckedExprToAst) when no explicit limit is configured. +const defaultMaxASTDepth = 250 + var limitIDsToNames = map[limitID]string{ limitCodePointSize: "cel.limit.expression_code_points", limitParseErrorRecovery: "cel.limit.parse_error_recovery", limitParseRecursionDepth: "cel.limit.parse_recursion_depth", + limitMaxASTDepth: "cel.limit.max_ast_depth", + limitExpressionNodeCount: "cel.limit.expression_node_count", } func limitNameByID(id limitID) (string, bool) { @@ -456,6 +468,14 @@ func CustomDecorator(dec interpreter.InterpretableDecorator) ProgramOption { } } +// CustomDecoratorV2 appends an InterpreterDecoratorV2 to the program. +func CustomDecoratorV2(dec interpreter.InterpretableDecoratorV2) ProgramOption { + return func(p *prog) (*prog, error) { + p.plannerOptions = append(p.plannerOptions, interpreter.CustomDecoratorV2(dec)) + return p, nil + } +} + // Functions adds function overloads that extend or override the set of CEL built-ins. // // Deprecated: use Function() instead to declare the function, its overload signatures, @@ -727,6 +747,47 @@ func InterruptCheckFrequency(checkFrequency uint) ProgramOption { } } +// AsyncCallObserver sets the observer for monitoring asynchronous function calls during ConcurrentEval. +func AsyncCallObserver(observer async.Observer) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncObserver = observer + return p, nil + } +} + +// AsyncCompletionBufferSize sets the size of the buffer for the async completion channel. +// By default, the channel is unbuffered. +func AsyncCompletionBufferSize(size int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncCompletionBufferSize = size + return p, nil + } +} + +// AsyncMaxConcurrency sets the maximum number of concurrently launched async calls during +// ConcurrentEval. This bounds the number of in-flight async goroutines, so a wide fan-out (such +// as an async call inside a comprehension over a large list) cannot exhaust memory. +// +// A value of 0 (unset) applies a built-in default bound. A positive value sets an explicit bound. +// A negative value disables the limiter (unbounded launches) and should only be used when +// concurrency is bounded by other means. +func AsyncMaxConcurrency(maxConcurrency int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncMaxConcurrency = maxConcurrency + return p, nil + } +} + +// ConcurrentDrainStrategy configures the strategy for when to re-evaluate the program +// during a ConcurrentEval call after receiving asynchronous completion signals. +// By default, the program re-evaluates immediately after every completion. +func ConcurrentDrainStrategy(strategy async.DrainStrategy) ProgramOption { + return func(p *prog) (*prog, error) { + p.drainStrategy = strategy + return p, nil + } +} + // CostEstimatorOptions configure type-check time options for estimating expression cost. func CostEstimatorOptions(costOpts ...checker.CostOption) EnvOption { return func(e *Env) (*Env, error) { @@ -942,6 +1003,25 @@ func ParserExpressionSizeLimit(limit int) EnvOption { return setLimit(limitCodePointSize, limit) } +// ExpressionNodeLimit adjusts the maximum number of expression nodes permitted during parsing +// and checking, including nodes created by macro expansion. Defaults are defined in the parser +// package (100,000). A negative value means unbounded. +func ExpressionNodeLimit(limit int) EnvOption { + return setLimit(limitExpressionNodeCount, limit) +} + +// ExpressionNestingDepthLimit records the maximum nesting depth permitted for ASTs in the +// environment configuration so that the value round-trips through env.Config export/import. +// +// ASTs loaded outside the parser (e.g. via ParsedExprToAst / CheckedExprToAst) bypass the +// parser's recursion limit, so those conversion paths validate nesting depth against the +// parser-matching default (250) to avoid a Go stack overflow during later checking or planning. +// Embedders that fully control their AST inputs and want to skip the check can construct the AST +// through the common/ast package directly rather than the cel conversion helpers. +func ExpressionNestingDepthLimit(limit int) EnvOption { + return setLimit(limitMaxASTDepth, limit) +} + // EnableHiddenAccumulatorName sets the parser to use the identifier '@result' for accumulators // which is not normally accessible from CEL source. func EnableHiddenAccumulatorName(enabled bool) EnvOption { diff --git a/vendor/github.com/google/cel-go/cel/program.go b/vendor/github.com/google/cel-go/cel/program.go index c46d694e..3a7589a7 100644 --- a/vendor/github.com/google/cel-go/cel/program.go +++ b/vendor/github.com/google/cel-go/cel/program.go @@ -18,8 +18,9 @@ import ( "context" "errors" "fmt" - "sync" + "time" + "github.com/google/cel-go/cel/async" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/functions" "github.com/google/cel-go/common/types" @@ -53,6 +54,21 @@ type Program interface { // // The output contract for `ContextEval` is otherwise identical to the `Eval` method. ContextEval(context.Context, any) (ref.Val, *EvalDetails, error) + + // ConcurrentEval evaluates the program concurrently, returning a channel that will receive + // the final EvalResult when all asynchronous operations complete, or the context expires. + // + // The vars value may either be an `Activation` or `map[string]any`. + // + // Liveness: ConcurrentEval relies on context cancellation to terminate. If an async function + // never returns and does not honor its context, and the supplied context has no deadline, the + // call will block indefinitely. Always pass a context with a deadline or cancellation. + // + // Error handling is fail-fast: as soon as a re-evaluation pass yields an error, that error is + // returned and any still in-flight async calls are cancelled (their contexts are done) and + // their results discarded. Async functions should therefore be free of unwanted side effects + // on partial evaluation, or guard them with idempotency/cancellation handling. + ConcurrentEval(context.Context, any) <-chan EvalResult } // Activation used to resolve identifiers by name and references by id. @@ -145,6 +161,13 @@ func (ed *EvalDetails) ActualCost() *uint64 { return &cost } +// EvalResult encapsulates the response from a ConcurrentEval call. +type EvalResult struct { + Val ref.Val + EvalDetails *EvalDetails + Err error +} + // prog is the internal implementation of the Program interface. type prog struct { *Env @@ -160,11 +183,21 @@ type prog struct { regexOptimizations []*interpreter.RegexOptimization // Interpretable configured from an Ast and aggregate decorator set based on program options. - interpretable interpreter.Interpretable + interpretable interpreter.InterpretableV2 observable *interpreter.ObservableInterpretable callCostEstimator interpreter.ActualCostEstimator costOptions []interpreter.CostTrackerOption costLimit *uint64 + + // hasAsync indicates the planned expression contains an asynchronous function call, which can + // only be resolved by ConcurrentEval. + hasAsync bool + + // Async evaluation configuration used by ConcurrentEval. + drainStrategy async.DrainStrategy + asyncObserver async.Observer + asyncCompletionBufferSize int + asyncMaxConcurrency int } // newProgram creates a program instance with an environment, an ast, and an optional list of @@ -182,6 +215,7 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { plannerOptions: []interpreter.PlannerOption{}, dispatcher: disp, costOptions: []interpreter.CostTrackerOption{}, + drainStrategy: async.DrainReady(100 * time.Microsecond), } // Configure the program via the ProgramOption values. @@ -214,6 +248,17 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { return nil, err } + // Determine whether the environment declares any asynchronous function. Async is a property of + // the binding, so its presence is known from the environment alone, without inspecting the + // program plan. The synchronous entry points (Eval, ContextEval) reject programs from an env + // with async functions; callers needing synchronous evaluation should use a non-async env. + for _, b := range e.functionBindings { + if b.Async != nil { + p.hasAsync = true + break + } + } + // Set the attribute factory after the options have been set. var attrFactory interpreter.AttributeFactory attrFactorOpts := []interpreter.AttrFactoryOption{ @@ -262,8 +307,16 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { if p.costLimit != nil { costOpts = append(costOpts, interpreter.CostTrackerLimit(*p.costLimit)) } + // Creating a new cost tracker for each evaluation causes significant work that + // needs to be repeated for each evaluation even though the cost tracker is + // mostly read-only once constructed. Therefore it gets constructed + // once now and later a cheap clone is used for each evaluation. + tracker, err := interpreter.NewCostTracker(p.callCostEstimator, costOpts...) + if err != nil { + return nil, fmt.Errorf("construct cost tracker: %w", err) + } trackerFactory := func() (*interpreter.CostTracker, error) { - return interpreter.NewCostTracker(p.callCostEstimator, costOpts...) + return tracker.Clone() } var observers []interpreter.PlannerOption if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 { @@ -312,23 +365,25 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { } } }() - // Build a hierarchical activation if there are default vars set. - var vars Activation - switch v := input.(type) { - case Activation: - vars = v - case map[string]any: - vars = activationPool.Setup(v) - defer activationPool.Put(vars) - default: - return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input) + // Asynchronous calls cannot be resolved by a single-pass evaluation. Reject before doing any + // work (this also covers ContextEval, which delegates here); ConcurrentEval does not call Eval. + if p.hasAsync { + return nil, nil, errAsyncRequiresConcurrentEval } - if p.defaultVars != nil { - vars = interpreter.NewHierarchicalActivation(p.defaultVars, vars) + // Build a hierarchical activation if there are default vars set. + var frame *interpreter.ExecutionFrame + if f, ok := input.(*interpreter.ExecutionFrame); ok { + frame = f + } else { + frame, err = p.newExecutionFrame(input) + if err != nil { + return nil, nil, err + } + defer frame.Close() } if p.observable != nil { det = &EvalDetails{} - out = p.observable.ObserveEval(vars, func(observed any) { + out = p.observable.ObserveExec(frame, func(observed any) { switch o := observed.(type) { case interpreter.EvalState: det.state = o @@ -337,7 +392,7 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { } }) } else { - out = p.interpretable.Eval(vars) + out = p.interpretable.Exec(frame) } // The output of an internal Eval may have a value (`v`) that is a types.Err. This step // translates the CEL value to a Go error response. This interface does not quite match the @@ -353,164 +408,220 @@ func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetail if ctx == nil { return nil, nil, fmt.Errorf("context can not be nil") } - // Configure the input, making sure to wrap Activation inputs in the special ctxActivation which - // exposes the #interrupted variable and manages rate-limited checks of the ctx.Done() state. - var vars Activation - switch v := input.(type) { - case Activation: - vars = ctxActivationPool.Setup(v, ctx.Done(), p.interruptCheckFrequency) - defer ctxActivationPool.Put(vars) - case map[string]any: - rawVars := activationPool.Setup(v) - defer activationPool.Put(rawVars) - vars = ctxActivationPool.Setup(rawVars, ctx.Done(), p.interruptCheckFrequency) - defer ctxActivationPool.Put(vars) - default: - return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input) + frame, err := p.newExecutionFrame(input) + if err != nil { + return nil, nil, err } - out, det, err := p.Eval(vars) - if err != nil && errors.Is(err, interpreter.InterruptError{}) { - return out, det, context.Cause(ctx) + defer frame.Close() + frame.SetContext(ctx, p.interruptCheckFrequency) + out, det, errEval := p.Eval(frame) + if errEval != nil && errors.Is(errEval, interpreter.InterruptError{}) { + return out, det, fmt.Errorf("%w: %w", errEval, context.Cause(ctx)) } - return out, det, err + return out, det, errEval } -type ctxEvalActivation struct { - parent Activation - interrupt <-chan struct{} - interruptCheckCount uint - interruptCheckFrequency uint -} - -// ResolveName implements the Activation interface method, but adds a special #interrupted variable -// which is capable of testing whether a 'done' signal is provided from a context.Context channel. -func (a *ctxEvalActivation) ResolveName(name string) (any, bool) { - if name == "#interrupted" { - a.interruptCheckCount++ - if a.interruptCheckCount%a.interruptCheckFrequency == 0 { - select { - case <-a.interrupt: - return true, true - default: - return nil, false - } - } - return nil, false +// newExecutionFrame creates an ExecutionFrame for the given input without a timeout context. +func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error) { + frame, err := interpreter.NewExecutionFrame(input) + if err != nil { + return nil, err + } + if p.defaultVars != nil { + // Update the frame's activation in place. + frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation) } - return a.parent.ResolveName(name) -} -func (a *ctxEvalActivation) Parent() Activation { - return a.parent + return frame, nil } -func (a *ctxEvalActivation) AsPartialActivation() (interpreter.PartialActivation, bool) { - pa, ok := a.parent.(interpreter.PartialActivation) - return pa, ok +// newAsyncFrame creates an ExecutionFrame configured for asynchronous evaluation under the +// given context, wiring the observer and concurrency limit from the program options. +func (p *prog) newAsyncFrame(ctx context.Context, input any) (*interpreter.ExecutionFrame, error) { + frame, err := p.newExecutionFrame(input) + if err != nil { + return nil, err + } + if err := frame.SetContext(ctx, p.interruptCheckFrequency); err != nil { + frame.Close() + return nil, err + } + frame.SetAsyncObserver(p.asyncObserver) + frame.SetAsyncMaxConcurrency(resolveAsyncMaxConcurrency(p.asyncMaxConcurrency)) + return frame, nil } -func newCtxEvalActivationPool() *ctxEvalActivationPool { - return &ctxEvalActivationPool{ - Pool: sync.Pool{ - New: func() any { - return &ctxEvalActivation{} - }, - }, +// defaultAsyncMaxConcurrency bounds the number of concurrently launched async calls when the +// program does not configure AsyncMaxConcurrency. It exists so that a wide fan-out (e.g. an async +// call inside a comprehension over a large list) cannot spawn an unbounded number of goroutines. +const defaultAsyncMaxConcurrency = 100 + +// resolveAsyncMaxConcurrency maps the configured concurrency to the effective launch limit: +// - 0 (unset): apply defaultAsyncMaxConcurrency. +// - >0: use the configured value. +// - <0: unlimited (no launch limiter); use only if the caller bounds concurrency another way. +func resolveAsyncMaxConcurrency(configured int) int { + if configured == 0 { + return defaultAsyncMaxConcurrency } + return configured } -type ctxEvalActivationPool struct { - sync.Pool +// resolveCompletionBufferSize returns the size of the async completion channel. When unset, it +// defaults to the effective launch concurrency so that all in-flight calls can report completion +// without blocking. An unbuffered channel would make a completed call hold its launch slot until +// the evaluator drained it, throttling effective concurrency to the drain rate. +func (p *prog) resolveCompletionBufferSize() int { + if p.asyncCompletionBufferSize > 0 { + return p.asyncCompletionBufferSize + } + limit := resolveAsyncMaxConcurrency(p.asyncMaxConcurrency) + if limit < 0 { + // Unlimited launches: fall back to the default bound for the buffer so it stays finite. + return defaultAsyncMaxConcurrency + } + return limit } -// Setup initializes a pooled Activation with the ability check for context.Context cancellation -func (p *ctxEvalActivationPool) Setup(vars Activation, done <-chan struct{}, interruptCheckRate uint) *ctxEvalActivation { - a := p.Pool.Get().(*ctxEvalActivation) - a.parent = vars - a.interrupt = done - a.interruptCheckCount = 0 - a.interruptCheckFrequency = interruptCheckRate - return a -} +// ConcurrentEval implements the Program interface. +func (p *prog) ConcurrentEval(ctx context.Context, input any) <-chan EvalResult { + resCh := make(chan EvalResult, 1) + if ctx == nil { + resCh <- EvalResult{Err: errors.New("context can not be nil")} + close(resCh) + return resCh + } -type evalActivation struct { - vars map[string]any - lazyVars map[string]any -} + go func() { + defer close(resCh) + // Ensure concurrent eval handles panic / recovery properly + defer func() { + if r := recover(); r != nil { + switch t := r.(type) { + case interpreter.EvalCancelledError: + resCh <- EvalResult{Err: t} + default: + resCh <- EvalResult{Err: fmt.Errorf("internal error: %v", r)} + } + } + }() -// ResolveName looks up the value of the input variable name, if found. -// -// Lazy bindings may be supplied within the map-based input in either of the following forms: -// - func() any -// - func() ref.Val -// -// The lazy binding will only be invoked once per evaluation. -// -// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using -// the types.Adapter configured in the environment. -func (a *evalActivation) ResolveName(name string) (any, bool) { - v, found := a.vars[name] - if !found { - return nil, false - } - switch obj := v.(type) { - case func() ref.Val: - if resolved, found := a.lazyVars[name]; found { - return resolved, true - } - lazy := obj() - a.lazyVars[name] = lazy - return lazy, true - case func() any: - if resolved, found := a.lazyVars[name]; found { - return resolved, true + frame, err := p.newAsyncFrame(ctx, input) + if err != nil { + resCh <- EvalResult{Err: err} + return } - lazy := obj() - a.lazyVars[name] = lazy - return lazy, true - default: - return obj, true - } -} + defer frame.Close() + + // Completions are signaled to this channel as async calls finish. The asyncCallState + // fan-in also selects on ctx.Done(), so the sender will not leak if this loop returns early. + completions := make(chan int64, p.resolveCompletionBufferSize()) + frame.SetCompletions(completions) + + for { + var out ref.Val + var det *EvalDetails + + if p.observable != nil { + det = &EvalDetails{} + out = p.observable.ObserveExec(frame, func(observed any) { + switch o := observed.(type) { + case interpreter.EvalState: + det.state = o + case *interpreter.CostTracker: + det.costTracker = o + } + }) + } else { + out = p.interpretable.Exec(frame) + } -// Parent implements the Activation interface -func (a *evalActivation) Parent() Activation { - return nil -} + // Communicate errors quickly. + if types.IsError(out) { + var err error = out.(*types.Err) + if errors.Is(err, interpreter.InterruptError{}) { + err = fmt.Errorf("%w: %w", err, context.Cause(ctx)) + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: err} + return + } -func newEvalActivationPool() *evalActivationPool { - return &evalActivationPool{ - Pool: sync.Pool{ - New: func() any { - return &evalActivation{lazyVars: make(map[string]any)} - }, - }, - } -} + // A concrete (non-unknown) result is final. + unk, isUnknown := out.(*types.Unknown) + if !isUnknown || !unk.HasUnknownFunction() { + resCh <- EvalResult{Val: out, EvalDetails: det, Err: nil} + return + } -type evalActivationPool struct { - sync.Pool -} + // Post-execution dispatch: launch only the async calls required by the unknown result. + frame.DispatchPendingAsyncCalls(unk.IDs()) -// Setup initializes a pooled Activation object with the map input. -func (p *evalActivationPool) Setup(vars map[string]any) *evalActivation { - a := p.Pool.Get().(*evalActivation) - a.vars = vars - return a -} + // The result depends on one or more unresolved async calls. Wait for completions and + // re-evaluate according to the configured drain strategy. + var batch []async.Call -func (p *evalActivationPool) Put(value any) { - a := value.(*evalActivation) - for k := range a.lazyVars { - delete(a.lazyVars, k) - } - p.Pool.Put(a) -} + // Wait for at least one completion (or cancellation). + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-ctx.Done(): + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + + // Accumulate completions and consult the strategy. + var timer *time.Timer + reevaluate := false + for !reevaluate { + active := frame.ActiveAsyncCalls() + action := p.drainStrategy.NextAction(batch, active) + if action.Reevaluate { + break + } + + var timeoutCh <-chan time.Time + if action.WaitDuration > 0 { + if timer == nil { + timer = time.NewTimer(action.WaitDuration) + } else { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(action.WaitDuration) + } + timeoutCh = timer.C + } + + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-timeoutCh: + reevaluate = true + case <-ctx.Done(): + if timer != nil { + timer.Stop() + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + } + if timer != nil { + timer.Stop() + } + } + }() -var ( - // activationPool is an internally managed pool of Activation values that wrap map[string]any inputs - activationPool = newEvalActivationPool() + return resCh +} - // ctxActivationPool is an internally managed pool of Activation values that expose a special #interrupted variable - ctxActivationPool = newCtxEvalActivationPool() -) +// errAsyncRequiresConcurrentEval is returned by the synchronous entry points (Eval, ContextEval) +// when the expression contains asynchronous function calls, which only ConcurrentEval can resolve. +var errAsyncRequiresConcurrentEval = errors.New( + "expression contains asynchronous function calls; use ConcurrentEval") diff --git a/vendor/github.com/google/cel-go/cel/prompt.go b/vendor/github.com/google/cel-go/cel/prompt.go index 1529680f..f5993482 100644 --- a/vendor/github.com/google/cel-go/cel/prompt.go +++ b/vendor/github.com/google/cel-go/cel/prompt.go @@ -108,7 +108,7 @@ type Prompt struct { // tmpl is the text template base-configuration for rendering text. tmpl *template.Template - // fieldPaths is a flag to enable including reachable field paths in the prompt. + // fieldPaths is a flag to include reachable field paths in the prompt. fieldPaths bool // env reference used to collect variables, functions, and macros available to the prompt. @@ -131,6 +131,12 @@ type promptInst struct { // Render renders the user prompt with the associated context from the prompt template // for use with LLM generators. +// +// User-supplied input is passed as template data via the UserPrompt field, which +// Go's text/template renders as a literal string value. Template action delimiters +// such as {{.Persona}} in the user prompt are never evaluated as template directives +// because text/template only executes directives present in the template definition +// itself, not in data values interpolated at render time. func (p *Prompt) Render(userPrompt string) string { var buffer strings.Builder vars := make([]*promptVariable, len(p.env.Variables())) @@ -178,7 +184,8 @@ func (p *Prompt) Render(userPrompt string) string { Variables: vars, Macros: macs, Functions: funcs, - UserPrompt: userPrompt} + UserPrompt: userPrompt, + } p.tmpl.Execute(&buffer, inst) return buffer.String() } diff --git a/vendor/github.com/google/cel-go/cel/validator.go b/vendor/github.com/google/cel-go/cel/validator.go index 952f88f4..cb7f4c29 100644 --- a/vendor/github.com/google/cel-go/cel/validator.go +++ b/vendor/github.com/google/cel-go/cel/validator.go @@ -15,6 +15,7 @@ package cel import ( + "context" "fmt" "reflect" "regexp" @@ -25,11 +26,12 @@ import ( ) const ( - durationValidatorName = "cel.validator.duration" - regexValidatorName = "cel.validator.matches" - timestampValidatorName = "cel.validator.timestamp" - homogeneousValidatorName = "cel.validator.homogeneous_literals" - nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + durationValidatorName = "cel.validator.duration" + regexValidatorName = "cel.validator.matches" + timestampValidatorName = "cel.validator.timestamp" + homogeneousValidatorName = "cel.validator.homogeneous_literals" + nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + bindNestingLimitValidatorName = "cel.validator.bind_nesting_limit" // HomogeneousAggregateLiteralExemptFunctions is the ValidatorConfig key used to configure // the set of function names which are exempt from homogeneous type checks. The expected type @@ -60,6 +62,23 @@ var ( } return nil, fmt.Errorf("invalid validator: %s missing limit", nestingLimitValidatorName) }, + bindNestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { + if limit, found := val.ConfigValue("limit"); found { + // In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double. + if val, isDouble := limit.(float64); isDouble { + if val != float64(int64(val)) { + return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", bindNestingLimitValidatorName, limit) + } + return ValidateBindNestingLimit(int(val)), nil + } + + if val, isInt := limit.(int); isInt { + return ValidateBindNestingLimit(val), nil + } + return nil, fmt.Errorf("invalid validator: %s unsupported limit type: %v", bindNestingLimitValidatorName, limit) + } + return nil, fmt.Errorf("invalid validator: %s missing limit", bindNestingLimitValidatorName) + }, durationValidatorName: func(*env.Validator) (ASTValidator, error) { return ValidateDurationLiterals(), nil }, @@ -80,12 +99,20 @@ type ASTValidatorFactory func(*env.Validator) (ASTValidator, error) // ASTValidators configures a set of ASTValidator instances into the target environment. // -// Validators are applied in the order in which the are specified and are treated as singletons. -// The same ASTValidator with a given name will not be applied more than once. +// Validators are applied in the order in which they are specified. +// If an ASTValidator with the same name is already configured, it will be replaced. func ASTValidators(validators ...ASTValidator) EnvOption { return func(e *Env) (*Env, error) { for _, v := range validators { - if !e.HasValidator(v.Name()) { + found := false + for i, existing := range e.validators { + if existing.Name() == v.Name() { + e.validators[i] = v + found = true + break + } + } + if !found { e.validators = append(e.validators, v) } } @@ -232,6 +259,13 @@ func ValidateComprehensionNestingLimit(limit int) ASTValidator { return nestingLimitValidator{limit: limit} } +// ValidateBindNestingLimit ensures that cel.bind() macro nesting does not exceed the specified limit. +// +// This validator can be useful for preventing arbitrarily nested cel.bind() macro calls. +func ValidateBindNestingLimit(limit int) ASTValidator { + return bindNestingLimitValidator{limit: limit} +} + type argChecker func(env *Env, call, arg ast.Expr) error func newFormatValidator(funcName string, argNum int, check argChecker) formatValidator { @@ -284,8 +318,9 @@ func evalCall(env *Env, call, arg ast.Expr) error { if err != nil { return err } - _, _, err = prg.Eval(NoVars()) - return err + resCh := prg.ConcurrentEval(context.Background(), NoVars()) + res := <-resCh + return res.Err } func compileRegex(_ *Env, _, arg ast.Expr) error { @@ -430,8 +465,7 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } // When the comprehension has an empty range, continue to the next ancestor // as this comprehension does not have any associated cost. - iterRange := e.AsComprehension().IterRange() - if iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 { + if isEmptyRangeComprehension(e) { e, hasParent = e.Parent() continue } @@ -445,3 +479,68 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } } } + +type bindNestingLimitValidator struct { + limit int +} + +// Name returns the name of the cel.bind nesting limit validator. +func (v bindNestingLimitValidator) Name() string { + return bindNestingLimitValidatorName +} + +// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the nesting limit +// as an integer value: {"limit": int} +func (v bindNestingLimitValidator) ToConfig() *env.Validator { + return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit}) +} + +// Validate implements the ASTValidator interface method. +func (v bindNestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) { + root := ast.NavigateAST(a) + comprehensions := ast.MatchDescendants(root, ast.KindMatcher(ast.ComprehensionKind)) + var celBinds []ast.NavigableExpr + for _, comp := range comprehensions { + if isCelBind(comp) { + celBinds = append(celBinds, comp) + } + } + if len(celBinds) <= v.limit { + return + } + for _, comp := range celBinds { + count := 0 + e := comp + hasParent := true + for hasParent { + if isCelBind(e) { + count++ + if count > v.limit { + iss.ReportErrorAtID(comp.ID(), "cel.bind exceeds nesting limit") + break + } + } + e, hasParent = e.Parent() + } + } +} + +func isEmptyRangeComprehension(e ast.NavigableExpr) bool { + if e.Kind() != ast.ComprehensionKind { + return false + } + iterRange := e.AsComprehension().IterRange() + return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 +} + +func isCelBind(e ast.NavigableExpr) bool { + if !isEmptyRangeComprehension(e) { + return false + } + compre := e.AsComprehension() + loopCond := compre.LoopCondition() + loopStep := compre.LoopStep() + return compre.IterVar() == unusedIterVar && + loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral().Value() == false && + loopStep.Kind() == ast.IdentKind && loopStep.AsIdent() == compre.AccuVar() +} diff --git a/vendor/github.com/google/cel-go/checker/cost.go b/vendor/github.com/google/cel-go/checker/cost.go index 5bc6318e..3d7dd7ec 100644 --- a/vendor/github.com/google/cel-go/checker/cost.go +++ b/vendor/github.com/google/cel-go/checker/cost.go @@ -159,6 +159,11 @@ func (se SizeEstimate) Union(size SizeEstimate) SizeEstimate { return result } +// AsCost converts a size estimates to an equivalent cost estimate. +func (se SizeEstimate) AsCost() CostEstimate { + return se.MultiplyByCostFactor(1) +} + // CostEstimate represents an estimated cost range and provides add and multiply operations // that do not overflow. type CostEstimate struct { @@ -786,18 +791,26 @@ func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *A return CallEstimate{CostEstimate: c.sizeOrUnknown(args[1]).MultiplyByCostFactor(1).Add(argCostSum())} } // O(nm) functions - case overloads.MatchesString: + case overloads.Matches, overloads.MatchesString: // https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL - if target != nil && len(args) == 1 { + var strNode, regexNode AstNode + if overloadID == overloads.MatchesString && target != nil && len(args) == 1 { + strNode = *target + regexNode = args[0] + } else if overloadID == overloads.Matches && target == nil && len(args) == 2 { + strNode = args[0] + regexNode = args[1] + } + if strNode != nil && regexNode != nil { // Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0 // in case where string is empty but regex is still expensive. - strCost := c.sizeOrUnknown(*target).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor) + strCost := c.sizeOrUnknown(strNode).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor) // We don't know how many expressions are in the regex, just the string length (a huge // improvement here would be to somehow get a count the number of expressions in the regex or // how many states are in the regex state machine and use that to measure regex cost). // For now, we're making a guess that each expression in a regex is typically at least 4 chars // in length. - regexCost := c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.RegexStringLengthCostFactor) + regexCost := c.sizeOrUnknown(regexNode).MultiplyByCostFactor(common.RegexStringLengthCostFactor) return CallEstimate{CostEstimate: strCost.Multiply(regexCost).Add(argCostSum())} } case overloads.ContainsString: diff --git a/vendor/github.com/google/cel-go/common/ast/ast.go b/vendor/github.com/google/cel-go/common/ast/ast.go index 3ae2e106..c8f8f8a0 100644 --- a/vendor/github.com/google/cel-go/common/ast/ast.go +++ b/vendor/github.com/google/cel-go/common/ast/ast.go @@ -172,6 +172,14 @@ func (a *AST) IDs() map[int64]bool { return visitor } +// NodeCount returns the total number of expression nodes in the AST, including macro calls. +func NodeCount(a *AST) int { + if a == nil { + return 0 + } + return len(a.IDs()) +} + // ClearUnusedIDs removes IDs not used in the AST or macro calls from SourceInfo. func (a *AST) ClearUnusedIDs() { ids := a.IDs() diff --git a/vendor/github.com/google/cel-go/common/ast/navigable.go b/vendor/github.com/google/cel-go/common/ast/navigable.go index 13e5777b..364edfa3 100644 --- a/vendor/github.com/google/cel-go/common/ast/navigable.go +++ b/vendor/github.com/google/cel-go/common/ast/navigable.go @@ -181,6 +181,29 @@ func PreOrderVisit(expr Expr, visitor Visitor) { visit(expr, visitor, preOrder, 0, 0) } +// ExceedsDepth determines whether the AST contains expressions nested deeper than the specified +// maxDepth. The root expression has depth 0, so a maxDepth of 250 permits expressions nested up +// to and including 250 levels deep. +// +// The traversal is bounded: it descends at most maxDepth+1 levels, so it remains safe to call on +// adversarially deep inputs that could otherwise exhaust the Go stack during later checking or +// planning. A non-positive maxDepth disables the check and returns false. +func ExceedsDepth(a *AST, maxDepth int) bool { + if a == nil || maxDepth <= 0 { + return false + } + exceedsDepth := false + visitor := NewExprVisitor(func(e Expr) { + if nav, ok := e.(NavigableExpr); ok && nav.Depth() >= maxDepth { + exceedsDepth = true + } + }) + // Bound the walk to maxDepth+1 levels so it never recurses past the first level that exceeds + // the limit, keeping the check itself safe on the deep inputs it guards against. + visit(NavigateAST(a), visitor, postOrder, 0, maxDepth+1) + return exceedsDepth +} + type visitOrder int const ( diff --git a/vendor/github.com/google/cel-go/common/containers/container.go b/vendor/github.com/google/cel-go/common/containers/container.go index fc146b6f..fcfcdfc3 100644 --- a/vendor/github.com/google/cel-go/common/containers/container.go +++ b/vendor/github.com/google/cel-go/common/containers/container.go @@ -227,7 +227,7 @@ func Abbrevs(qualifiedNames ...string) ContainerOption { } alias := qn[ind+1:] var err error - c, err = aliasAs("abbreviation", qn, alias)(c) + c, err = aliasAs("abbreviation", qn, alias, true)(c) if err != nil { return nil, err } @@ -236,31 +236,32 @@ func Abbrevs(qualifiedNames ...string) ContainerOption { } } -// Alias associates a fully-qualified name with a user-defined alias. +// Alias associates a name with a user-defined alias. // // In general, Abbrevs is preferred to Alias since the names generated from the Abbrevs option // are more easily traced back to source code. The Alias option is useful for propagating alias // configuration from one Container instance to another, and may also be useful for remapping // poorly chosen protobuf message / package names. -// -// Note: all of the rules that apply to Abbrevs also apply to Alias. func Alias(qualifiedName, alias string) ContainerOption { - return aliasAs("alias", qualifiedName, alias) + return aliasAs("alias", qualifiedName, alias, false) } -func aliasAs(kind, qualifiedName, alias string) ContainerOption { +func aliasAs(kind, qualifiedName, alias string, requireQualified bool) ContainerOption { return func(c *Container) (*Container, error) { if len(alias) == 0 || strings.Contains(alias, ".") { return nil, fmt.Errorf( "%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias) } + if len(qualifiedName) == 0 { + return nil, fmt.Errorf("%s must refer to a valid name: %s", kind, qualifiedName) + } if qualifiedName[0:1] == "." { return nil, fmt.Errorf("qualified name must not begin with a leading '.': %s", qualifiedName) } ind := strings.LastIndex(qualifiedName, ".") - if ind <= 0 || ind == len(qualifiedName)-1 { + if ind == len(qualifiedName)-1 || (requireQualified && ind <= 0) { return nil, fmt.Errorf("%s must refer to a valid qualified name: %s", kind, qualifiedName) } diff --git a/vendor/github.com/google/cel-go/common/decls/decls.go b/vendor/github.com/google/cel-go/common/decls/decls.go index cd4d3a56..51cb689e 100644 --- a/vendor/github.com/google/cel-go/common/decls/decls.go +++ b/vendor/github.com/google/cel-go/common/decls/decls.go @@ -16,6 +16,7 @@ package decls import ( + "context" "fmt" "strings" @@ -316,8 +317,12 @@ func (f *FunctionDecl) HasLateBinding() bool { if f == nil { return false } + if f.singleton != nil && f.singleton.Async != nil { + return true + } for _, oID := range f.overloadOrdinals { - if f.overloads[oID].HasLateBinding() { + o := f.overloads[oID] + if o.HasLateBinding() { return true } } @@ -342,6 +347,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: o.guardedUnaryOp(f.Name(), f.disableTypeGuards), Binary: o.guardedBinaryOp(f.Name(), f.disableTypeGuards), Function: o.guardedFunctionOp(f.Name(), f.disableTypeGuards), + Async: o.guardedAsyncOp(f.Name(), f.disableTypeGuards), OperandTrait: o.OperandTrait(), NonStrict: o.IsNonStrict(), } @@ -362,6 +368,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: f.singleton.Unary, Binary: f.singleton.Binary, Function: f.singleton.Function, + Async: f.singleton.Async, OperandTrait: f.singleton.OperandTrait, }, } @@ -380,6 +387,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: overloads[0].Unary, Binary: overloads[0].Binary, Function: overloads[0].Function, + Async: overloads[0].Async, NonStrict: overloads[0].NonStrict, OperandTrait: overloads[0].OperandTrait, }), nil @@ -538,6 +546,30 @@ func SingletonFunctionBinding(fn functions.FunctionOp, traits ...int) FunctionOp } } +// SingletonAsyncBinding creates a singleton async function definition to be used with all function overloads. +// The provided function is called in its own goroutine with the provided context. The function should +// block until the result is available, and the framework manages goroutine and channel lifecycle. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + trait := 0 + for _, t := range traits { + trait = trait | t + } + return func(f *FunctionDecl) (*FunctionDecl, error) { + if f.singleton != nil { + return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name()) + } + f.singleton = &functions.Overload{ + Operator: f.Name(), + Async: wrapAsyncOp(fn), + OperandTrait: trait, + } + return f, nil + } +} + // Overload defines a new global overload with an overload id, argument types, and result type. Through the // use of OverloadOpt options, the overload may also be configured with a binding, an operand trait, and to // be non-strict. @@ -622,6 +654,8 @@ type OverloadDecl struct { binaryOp functions.BinaryOp // functionOp is a catch-all for zero-arity and three-plus arity functions. functionOp functions.FunctionOp + // asyncOp is an asynchronous function binding that returns a channel. + asyncOp functions.AsyncOp } // Examples returns a list of string examples for the overload. @@ -677,7 +711,7 @@ func (o *OverloadDecl) HasLateBinding() bool { if o == nil { return false } - return o.hasLateBinding + return o.hasLateBinding || o.asyncOp != nil } // OperandTrait returns the trait mask of the first operand to the overload call, e.g. @@ -750,7 +784,7 @@ func (o *OverloadDecl) SignatureOverlaps(other *OverloadDecl) bool { // HasBinding indicates whether the overload already has a definition. func (o *OverloadDecl) HasBinding() bool { - return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil) + return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil || o.asyncOp != nil) } // guardedUnaryOp creates an invocation guard around the provided unary operator, if one is defined. @@ -792,6 +826,22 @@ func (o *OverloadDecl) guardedFunctionOp(funcName string, disableTypeGuards bool } } +// guardedAsyncOp creates an invocation guard around the provided async function binding, if one is provided. +func (o *OverloadDecl) guardedAsyncOp(funcName string, disableTypeGuards bool) functions.AsyncOp { + if o.asyncOp == nil { + return nil + } + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + if !o.matchesRuntimeSignature(disableTypeGuards, args...) { + ch := make(chan ref.Val, 1) + ch <- MaybeNoSuchOverload(funcName, args...) + close(ch) + return ch + } + return o.asyncOp(ctx, args...) + } +} + // matchesRuntimeUnarySignature indicates whether the argument type is runtime assiganble to the overload's expected argument. func (o *OverloadDecl) matchesRuntimeUnarySignature(disableTypeGuards bool, arg ref.Val) bool { return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg) && @@ -825,6 +875,8 @@ func matchRuntimeArgType(nonStrict, disableTypeGuards bool, argType *types.Type, if nonStrict && (disableTypeGuards || types.IsUnknownOrError(arg)) { return true } + // Note, early returns and unknown aggregation happen in the interpretable.go file; however, this check is here + // for defense in depth or for scenarios where someone manipulates bindings to offer their own dispatch logic. if types.IsUnknownOrError(arg) { return false } @@ -897,6 +949,40 @@ func FunctionBinding(binding functions.FunctionOp) OverloadOpt { } } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return func(o *OverloadDecl) (*OverloadDecl, error) { + if o.HasBinding() { + return nil, fmt.Errorf("overload already has a binding: %s", o.ID()) + } + if o.hasLateBinding { + return nil, fmt.Errorf("overload already has a late binding: %s", o.ID()) + } + o.asyncOp = wrapAsyncOp(fn) + return o, nil + } +} + +// wrapAsyncOp adapts a blocking function into the channel-based AsyncOp used internally. +// +// The blocking function is invoked synchronously and its result delivered on a buffered channel. +// The interpreter always invokes an AsyncOp from a dedicated goroutine, so running the blocking +// call inline here keeps the framework to a single goroutine per async call rather than spawning +// an additional one to bridge blocking-to-channel. +func wrapAsyncOp(fn functions.BlockingAsyncOp) functions.AsyncOp { + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + ch := make(chan ref.Val, 1) + ch <- fn(ctx, args...) + close(ch) + return ch + } +} + // LateFunctionBinding indicates that the function has a binding which is not known at compile time. // This is useful for functions which have side-effects or are not deterministically computable. func LateFunctionBinding() OverloadOpt { diff --git a/vendor/github.com/google/cel-go/common/env/BUILD.bazel b/vendor/github.com/google/cel-go/common/env/BUILD.bazel index b2e0c293..261da924 100644 --- a/vendor/github.com/google/cel-go/common/env/BUILD.bazel +++ b/vendor/github.com/google/cel-go/common/env/BUILD.bazel @@ -23,12 +23,14 @@ go_library( name = "go_default_library", srcs = [ "env.go", + "io.go", ], importpath = "github.com/google/cel-go/common/env", deps = [ "//common:go_default_library", "//common/decls:go_default_library", "//common/types:go_default_library", + "@in_yaml_go_yaml_v3//:go_default_library", ], ) @@ -37,6 +39,7 @@ go_test( size = "small", srcs = [ "env_test.go", + "io_test.go", ], data = glob(["testdata/**"]), embed = [":go_default_library"], @@ -45,6 +48,7 @@ go_test( "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/types:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", "@in_yaml_go_yaml_v3//:go_default_library", ], ) diff --git a/vendor/github.com/google/cel-go/common/env/env.go b/vendor/github.com/google/cel-go/common/env/env.go index 85ec85cd..936036ed 100644 --- a/vendor/github.com/google/cel-go/common/env/env.go +++ b/vendor/github.com/google/cel-go/common/env/env.go @@ -258,7 +258,9 @@ type Variable struct { // Type represents the type declaration for the variable. // - // Deprecated: use the embedded *TypeDesc fields directly. + // When serialized, 'type' is used for shorthand specifier string. + // + // Use GetType() for getting the effective type. Type *TypeDesc `yaml:"type,omitempty"` // TypeDesc is an embedded set of fields allowing for the specification of the Variable type. @@ -276,6 +278,9 @@ func (v *Variable) Validate() error { if err := v.GetType().Validate(); err != nil { return fmt.Errorf("invalid variable %q: %w", v.Name, err) } + if v.GetType().IsTypeParam { + return fmt.Errorf("invalid variable %q: variables cannot be type parameters", v.Name) + } return nil } @@ -844,6 +849,34 @@ func (td *TypeDesc) Validate() error { return nil } +func formatSpecifierImpl(td *TypeDesc, sb *strings.Builder) { + if td.IsTypeParam { + sb.WriteRune('~') + sb.WriteString(td.TypeName) + return + } + sb.WriteString(td.TypeName) + l := len(td.Params) + if l < 1 { + return + } + sb.WriteRune('<') + for i, p := range td.Params { + formatSpecifierImpl(p, sb) + if i < l-1 { + sb.WriteString(", ") + } + } + sb.WriteRune('>') +} + +// SpecifierFormat returns the short text representation of the type. e.g. "map" +func (td *TypeDesc) SpecifierFormat() string { + var sb strings.Builder + formatSpecifierImpl(td, &sb) + return sb.String() +} + // AsCELType converts the serializable object to a *types.Type value. func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) { err := td.Validate() @@ -853,6 +886,27 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) { switch td.TypeName { case "dyn": return types.DynType, nil + // short aliases for WKTs + case "duration": + return types.DurationType, nil + case "timestamp": + return types.TimestampType, nil + case "any": + return types.AnyType, nil + case "null", "null_type": + return types.NullType, nil + case "bool_wrapper": + return types.NewNullableType(types.BoolType), nil + case "bytes_wrapper": + return types.NewNullableType(types.BytesType), nil + case "double_wrapper": + return types.NewNullableType(types.DoubleType), nil + case "int_wrapper": + return types.NewNullableType(types.IntType), nil + case "uint_wrapper": + return types.NewNullableType(types.UintType), nil + case "string_wrapper": + return types.NewNullableType(types.StringType), nil case "map": kt, err := td.Params[0].AsCELType(tp) if err != nil { @@ -926,6 +980,15 @@ func SerializeTypeDesc(t *types.Type) *TypeDesc { for _, p := range t.Parameters() { params = append(params, SerializeTypeDesc(p)) } + // Special types, these aren't useful for describing environments. + switch t.Kind() { + case types.ErrorKind: + typeName = "*error*" + case types.UnknownKind: + typeName = "*unknown*" + case types.UnspecifiedKind: + typeName = "*unspecified type*" + } return NewTypeDesc(typeName, params...) } diff --git a/vendor/github.com/google/cel-go/common/env/io.go b/vendor/github.com/google/cel-go/common/env/io.go new file mode 100644 index 00000000..ec126f9c --- /dev/null +++ b/vendor/github.com/google/cel-go/common/env/io.go @@ -0,0 +1,271 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package env + +import ( + "errors" + "fmt" + + "go.yaml.in/yaml/v3" +) + +type internalTypeDesc struct { + TypeName string `yaml:"type_name"` + Params []*TypeDesc `yaml:"params,omitempty"` + IsTypeParam bool `yaml:"is_type_param,omitempty"` +} + +// Embedding TypeDesc in variable causes issues with customizing +// unmarshalling / marshalling. Work around with a parallel type. +type internalVariable struct { + Name string `yaml:"name"` + Description string `yaml:"description,omitempty"` + + // Type represents the type declaration for the variable. + Type *TypeDesc `yaml:"type,omitempty"` + + TypeName string `yaml:"type_name"` + Params []*TypeDesc `yaml:"params,omitempty"` + IsTypeParam bool `yaml:"is_type_param,omitempty"` +} + +// UnmarshalYAML implements yaml.Unmarshal +func (v *Variable) UnmarshalYAML(n *yaml.Node) error { + buf := internalVariable{} + err := n.Decode(&buf) + if err != nil { + return err + } + v.Name = buf.Name + v.Description = buf.Description + if buf.TypeName != "" { + v.TypeDesc = &TypeDesc{ + TypeName: buf.TypeName, + Params: buf.Params, + IsTypeParam: buf.IsTypeParam, + } + } else if buf.Type != nil { + v.TypeDesc = buf.Type + } + return nil +} + +// MarshalYAML implements yaml.Marshaler +func (v *Variable) MarshalYAML() (any, error) { + // The presence of an unmarshaller alters the default marshaller behavior so + // provide a simple marshal implementation. + buf := internalVariable{ + Name: v.Name, + Description: v.Description, + } + if t := v.GetType(); t != nil { + buf.TypeName = t.TypeName + buf.Params = t.Params + buf.IsTypeParam = t.IsTypeParam + } + return &buf, nil +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (td *TypeDesc) UnmarshalYAML(n *yaml.Node) error { + if td == nil { + return fmt.Errorf("unexpected Unmarshal for TypeDesc at: %d", n.Line) + } + if n.Kind == yaml.ScalarNode { + o, err := ParseTypeDesc(n.Value) + if err != nil { + return err + } + *td = *o + return nil + } + + if n.Kind != yaml.MappingNode { + return errors.New("unsupported yaml for TypeDesc") + } + + buf := internalTypeDesc{} + err := n.Decode(&buf) + if err != nil { + return err + } + td.TypeName = buf.TypeName + td.Params = buf.Params + td.IsTypeParam = buf.IsTypeParam + return nil +} + +type typeDescParser struct { + text string + pos int + length int +} + +// ParseTypeDesc parses a TypeDesc from the type specifier format: "map" +func ParseTypeDesc(text string) (*TypeDesc, error) { + p := &typeDescParser{text: text, length: len(text)} + res, err := p.parseTypeElem() + if err != nil { + return nil, fmt.Errorf("failed to parse type %q: %v", text, err) + } + p.skipWhitespace() + if p.pos < p.length { + return nil, fmt.Errorf("unexpected character %q at position %d in %q", p.text[p.pos], p.pos, text) + } + return res, nil +} + +func (p *typeDescParser) parseConcreteType() (*TypeDesc, error) { + id, err := p.parseNamespaceIdentifier() + if err != nil { + return nil, err + } + if p.pos < p.length && p.text[p.pos] == '<' { + p.pos++ // consume '<' + var params []*TypeDesc + for { + p.skipWhitespace() + param, err := p.parseTypeElem() + if err != nil { + return nil, err + } + params = append(params, param) + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] == ',' { + p.pos++ // consume ',' + continue + } + if p.pos < p.length && p.text[p.pos] == '>' { + p.pos++ // consume '>' + break + } + return nil, fmt.Errorf("expected ',' or '>' at position %d", p.pos) + } + return NewTypeDesc(id, params...), nil + } + return NewTypeDesc(id), nil +} + +func (p *typeDescParser) parseTypeElem() (*TypeDesc, error) { + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] == '~' { + p.pos++ // consume '~' + id, err := p.parseTypeParamIdent() + if err != nil { + return nil, err + } + return NewTypeParam(id), nil + } + return p.parseConcreteType() +} + +func (p *typeDescParser) parseNamespaceIdentifier() (string, error) { + p.skipWhitespace() + var id string + for p.pos < p.length && p.text[p.pos] != '<' { + c := p.text[p.pos] + if c == '.' { + id += "." + p.pos++ // consume '.' + } + ident, err := p.parseIdentifier() + if err != nil { + return "", err + } + id += ident + p.skipWhitespace() + if p.pos < p.length && p.text[p.pos] != '.' { + break + } + } + if id == "" { + return "", fmt.Errorf("missing identifier at position %d", p.pos) + } + return id, nil +} + +func (p *typeDescParser) parseIdentifier() (string, error) { + p.skipWhitespace() + if p.pos >= p.length { + return "", fmt.Errorf("unexpected end of input") + } + start := p.pos + c := p.text[p.pos] + if !isAlpha(c) && c != '_' { + return "", fmt.Errorf("identifier is expected, but %q was found at position %d", c, p.pos) + } + p.pos++ + for p.pos < p.length { + c := p.text[p.pos] + if !isAlphaNumeric(c) && c != '_' { + break + } + p.pos++ + } + return p.text[start:p.pos], nil +} + +func (p *typeDescParser) parseTypeParamIdent() (string, error) { + p.skipWhitespace() + if p.pos >= p.length { + return "", fmt.Errorf("unexpected end of input") + } + c := p.text[p.pos] + if !isAlpha(c) { + return "", fmt.Errorf("invalid type parameter identifier %q at position %d, must be a single character from A-Z", c, p.pos) + } + p.pos++ + if p.pos < p.length && isAlpha(p.text[p.pos]) { + return "", fmt.Errorf("invalid type param, must have a single alphabetic character at position %d", p.pos) + } + return string(c), nil +} + +func (p *typeDescParser) skipWhitespace() { + for p.pos < p.length && p.text[p.pos] == ' ' { + p.pos++ + } +} + +func isAlpha(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +func isAlphaNumeric(c byte) bool { + return isAlpha(c) || (c >= '0' && c <= '9') +} + +// ConfigFromYAML returns a config from YAML source. +// +// Adds custom parsing logic for normalizing shorthand for specifiying some fields +// in a YAML document (mainly the type-specifier shorthand). +// +// Using yaml.Unmarshal with any implementation should be sufficient for most +// cases. +func ConfigFromYAML(data []byte) (*Config, error) { + c := &Config{} + e := yaml.Unmarshal(data, c) + if e != nil { + return nil, e + } + return c, nil +} + +// ConfigToYAML returns the config serialized to YAML +// +// Provided as a convenience wrapper around a tested YAML Marshaler. +func ConfigToYAML(c *Config) ([]byte, error) { + return yaml.Marshal(c) +} diff --git a/vendor/github.com/google/cel-go/common/functions/functions.go b/vendor/github.com/google/cel-go/common/functions/functions.go index 67f4a594..0c00781d 100644 --- a/vendor/github.com/google/cel-go/common/functions/functions.go +++ b/vendor/github.com/google/cel-go/common/functions/functions.go @@ -15,7 +15,11 @@ // Package functions defines the standard builtin functions supported by the interpreter package functions -import "github.com/google/cel-go/common/types/ref" +import ( + "context" + + "github.com/google/cel-go/common/types/ref" +) // Overload defines a named overload of a function, indicating an operand trait // which must be present on the first argument to the overload as well as one @@ -41,21 +45,37 @@ type Overload struct { // Binary defines the overload with a BinaryOp implementation. May be nil. Binary BinaryOp - // Function defines the overload with a FunctionOp implementation. May be - // nil. + // Function defines the overload with a FunctionOp implementation. May be nil. Function FunctionOp + // Async defines the overload with an AsyncOp implementation. May be nil. + Async AsyncOp + // NonStrict specifies whether the Overload will tolerate arguments that // are types.Err or types.Unknown. NonStrict bool } // UnaryOp is a function that takes a single value and produces an output. -type UnaryOp func(value ref.Val) ref.Val +type UnaryOp func(ref.Val) ref.Val // BinaryOp is a function that takes two values and produces an output. -type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val +type BinaryOp func(ref.Val, ref.Val) ref.Val // FunctionOp is a function with accepts zero or more arguments and produces // a value or error as a result. -type FunctionOp func(values ...ref.Val) ref.Val +type FunctionOp func(...ref.Val) ref.Val + +// AsyncOp is a function that accepts zero or more arguments and produces +// a value or error asynchronously via a channel. +// +// AsyncOp is an internal interface intended for use by CEL to manage goroutines and +// channels associated with async calls. For public API usage, use BlockingAsyncOp. +// Implementers should listen for context cancellation on the provided context for +// resource cleanup. +type AsyncOp func(context.Context, ...ref.Val) <-chan ref.Val + +// BlockingAsyncOp is a function that accepts zero or more arguments and blocks until +// the result is available. When used with AsyncBinding, the framework runs the function +// in its own goroutine and manages channel lifecycle internally. +type BlockingAsyncOp func(context.Context, ...ref.Val) ref.Val diff --git a/vendor/github.com/google/cel-go/common/overloads/overloads.go b/vendor/github.com/google/cel-go/common/overloads/overloads.go index 9d50f436..0e3a7244 100644 --- a/vendor/github.com/google/cel-go/common/overloads/overloads.go +++ b/vendor/github.com/google/cel-go/common/overloads/overloads.go @@ -291,7 +291,6 @@ const ( const ( DurationToDuration = "duration_to_duration" StringToDuration = "string_to_duration" - IntToDuration = "int64_to_duration" ) // Convert to dyn diff --git a/vendor/github.com/google/cel-go/common/runes/buffer.go b/vendor/github.com/google/cel-go/common/runes/buffer.go index 02119822..58dd33e1 100644 --- a/vendor/github.com/google/cel-go/common/runes/buffer.go +++ b/vendor/github.com/google/cel-go/common/runes/buffer.go @@ -16,6 +16,7 @@ package runes import ( + "fmt" "strings" "unicode/utf8" ) @@ -113,45 +114,64 @@ var _ Buffer = &supplementalBuffer{} var nilBuffer = &emptyBuffer{} +// SizeLimitError indicates that the input exceeded the configured code point limit. +type SizeLimitError struct { + Size int + Limit int +} + +func (e *SizeLimitError) Error() string { + return fmt.Sprintf("expression code point size exceeds limit: size: %d, limit %d", e.Size, e.Limit) +} + // NewBuffer returns an efficient implementation of Buffer for the given text based on the ranges of // the encoded code points contained within. -// -// Code points are represented as an array of byte, uint16, or rune. This approach ensures that -// each index represents a code point by itself without needing to use an array of rune. At first -// we assume all code points are less than or equal to '\u007f'. If this holds true, the -// underlying storage is a byte array containing only ASCII characters. If we encountered a code -// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the -// elements of previous byte array to the uint16 array, and continue. If this holds true, the -// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual -// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous -// elements of the byte or uint16 array, and continue. The underlying storage is an rune array -// containing any Unicode character. func NewBuffer(data string) Buffer { - buf, _ := newBuffer(data, false) + buf, _, _ := newBufferWithLimit(data, false, -1) return buf } // NewBufferAndLineOffsets returns an efficient implementation of Buffer for the given text based on // the ranges of the encoded code points contained within, as well as returning the line offsets. -// -// Code points are represented as an array of byte, uint16, or rune. This approach ensures that -// each index represents a code point by itself without needing to use an array of rune. At first -// we assume all code points are less than or equal to '\u007f'. If this holds true, the -// underlying storage is a byte array containing only ASCII characters. If we encountered a code -// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the -// elements of previous byte array to the uint16 array, and continue. If this holds true, the -// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual -// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous -// elements of the byte or uint16 array, and continue. The underlying storage is an rune array -// containing any Unicode character. func NewBufferAndLineOffsets(data string) (Buffer, []int32) { - return newBuffer(data, true) + buf, offs, _ := newBufferWithLimit(data, true, -1) + return buf, offs +} + +// NewBufferAndLineOffsetsWithLimit returns an efficient implementation of Buffer for the given text +// and enforces a code point limit while constructing the buffer. +func NewBufferAndLineOffsetsWithLimit(data string, limit int) (Buffer, []int32, error) { + if limit < 0 || len(data) <= limit { + return newBufferWithLimit(data, true, -1) + } + return newBufferWithLimit(data, true, limit) +} + +func countRemainingCodePoints(data string, idx int, count int) int { + for idx < len(data) { + _, s := utf8.DecodeRuneInString(data[idx:]) + idx += s + count++ + } + return count } -func newBuffer(data string, lines bool) (Buffer, []int32) { +func newBufferWithLimit(data string, lines bool, limit int) (Buffer, []int32, error) { if len(data) == 0 { - return nilBuffer, []int32{0} + return nilBuffer, []int32{0}, nil + } + if limit >= 0 && len(data) > limit { + size := countRemainingCodePoints(data, 0, 0) + if size > limit { + return nil, nil, &SizeLimitError{ + Size: size, + Limit: limit, + } + } } + + // The resulting buffers store one element per code point, so the worst case + // element count never exceeds len(data). var ( idx = 0 off int32 = 0 @@ -195,7 +215,8 @@ func newBuffer(data string, lines bool) (Buffer, []int32) { } return &asciiBuffer{ arr: buf8, - }, offs + }, offs, nil + copy16: for idx < len(data) { r, s := utf8.DecodeRuneInString(data[idx:]) @@ -222,7 +243,8 @@ copy16: } return &basicBuffer{ arr: buf16, - }, offs + }, offs, nil + copy32: for idx < len(data) { r, s := utf8.DecodeRuneInString(data[idx:]) @@ -238,5 +260,5 @@ copy32: } return &supplementalBuffer{ arr: buf32, - }, offs + }, offs, nil } diff --git a/vendor/github.com/google/cel-go/common/source.go b/vendor/github.com/google/cel-go/common/source.go index ec79cb54..9187e9b5 100644 --- a/vendor/github.com/google/cel-go/common/source.go +++ b/vendor/github.com/google/cel-go/common/source.go @@ -74,6 +74,12 @@ func NewTextSource(text string) Source { return NewStringSource(text, "") } +// NewTextSourceWithLimit creates a new Source from the input text string while +// enforcing a maximum code point count when needed. +func NewTextSourceWithLimit(text string, limit int) (Source, error) { + return NewStringSourceWithLimit(text, "", limit) +} + // NewStringSource creates a new Source from the given contents and description. func NewStringSource(contents string, description string) Source { // Compute line offsets up front as they are referred to frequently. @@ -85,6 +91,23 @@ func NewStringSource(contents string, description string) Source { } } +// NewStringSourceWithLimit creates a new Source from the given contents and +// description while enforcing a maximum code point count when needed. +func NewStringSourceWithLimit(contents string, description string, limit int) (Source, error) { + if limit < 0 || len(contents) <= limit { + return NewStringSource(contents, description), nil + } + buf, offs, err := runes.NewBufferAndLineOffsetsWithLimit(contents, limit) + if err != nil { + return nil, err + } + return &sourceImpl{ + Buffer: buf, + description: description, + lineOffsets: offs, + }, nil +} + // NewInfoSource creates a new Source from a SourceInfo. func NewInfoSource(info *exprpb.SourceInfo) Source { return &sourceImpl{ diff --git a/vendor/github.com/google/cel-go/common/stdlib/standard.go b/vendor/github.com/google/cel-go/common/stdlib/standard.go index 4040a4f5..d2313bef 100644 --- a/vendor/github.com/google/cel-go/common/stdlib/standard.go +++ b/vendor/github.com/google/cel-go/common/stdlib/standard.go @@ -16,6 +16,7 @@ package stdlib import ( + "math" "strconv" "strings" "time" @@ -310,6 +311,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') < duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne { return types.True @@ -367,6 +371,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') <= duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne || cmp == types.IntZero { return types.True @@ -424,6 +431,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') > duration('1us') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne { return types.True @@ -481,6 +491,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('60s') >= duration('1m') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne || cmp == types.IntZero { return types.True @@ -605,8 +618,6 @@ func init() { decls.Overload(overloads.DurationToDuration, argTypes(types.DurationType), types.DurationType, decls.OverloadExamples(`duration(duration('1s')) // duration('1s')`), decls.UnaryBinding(identity)), - decls.Overload(overloads.IntToDuration, argTypes(types.IntType), types.DurationType, - decls.UnaryBinding(convertToType(types.DurationType))), decls.Overload(overloads.StringToDuration, argTypes(types.StringType), types.DurationType, decls.OverloadExamples(`duration('1h2m3s') // duration('3723s')`), decls.UnaryBinding(convertToType(types.DurationType)))), @@ -928,6 +939,11 @@ func noBinaryOverrides(rhs, lhs ref.Val) ref.Val { return types.NoSuchOverloadErr() } +func isNaN(val ref.Val) bool { + d, ok := val.(types.Double) + return ok && math.IsNaN(float64(d)) +} + func noFunctionOverrides(args ...ref.Val) ref.Val { return types.NoSuchOverloadErr() } diff --git a/vendor/github.com/google/cel-go/common/types/bytes.go b/vendor/github.com/google/cel-go/common/types/bytes.go index 88da0531..2eefb5d7 100644 --- a/vendor/github.com/google/cel-go/common/types/bytes.go +++ b/vendor/github.com/google/cel-go/common/types/bytes.go @@ -44,7 +44,10 @@ func (b Bytes) Add(other ref.Val) ref.Val { if !ok { return ValOrErr(other, "no such overload") } - return append(b, otherBytes...) + sum := make([]byte, 0, len(b)+len(otherBytes)) + sum = append(sum, b...) + sum = append(sum, otherBytes...) + return Bytes(sum) } // Compare implements traits.Comparer interface method by lexicographic ordering. diff --git a/vendor/github.com/google/cel-go/common/types/string.go b/vendor/github.com/google/cel-go/common/types/string.go index 5f5a4335..1335903a 100644 --- a/vendor/github.com/google/cel-go/common/types/string.go +++ b/vendor/github.com/google/cel-go/common/types/string.go @@ -122,7 +122,11 @@ func (s String) ConvertToType(typeVal ref.Type) ref.Val { return durationOf(d) } case TimestampType: - if t, err := time.Parse(time.RFC3339, s.Value().(string)); err == nil { + str := s.Value().(string) + if !isStrictRFC3339(str) { + return NewErr("invalid RFC 3339 timestamp %q", str) + } + if t, err := time.Parse(time.RFC3339, str); err == nil { if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { return celErrTimestampOverflow } diff --git a/vendor/github.com/google/cel-go/common/types/timestamp.go b/vendor/github.com/google/cel-go/common/types/timestamp.go index 060caf6b..62a020d9 100644 --- a/vendor/github.com/google/cel-go/common/types/timestamp.go +++ b/vendor/github.com/google/cel-go/common/types/timestamp.go @@ -17,9 +17,11 @@ package types import ( "fmt" "reflect" + "regexp" "strconv" "strings" "time" + "unicode" "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types/ref" @@ -52,6 +54,79 @@ const ( maxUnixTime int64 = 253402300799 ) +// strictRFC3339Pattern gates the strings accepted by the `timestamp()` overload. +// time.Parse accepts inputs that RFC 3339 forbids: a ',' fractional-second +// separator, single-digit time fields, and numeric offsets whose hours exceed +// 23 or minutes exceed 59. Those slip past unnoticed and shift the parsed +// instant, so they are rejected before time.Parse runs. Month and day are held +// to the grammar ranges 01-12 and 01-31; the remaining calendar validation +// (day-of-month vs. month, leap years) is left to time.Parse. +// +// isStrictRFC3339 is the implementation used on the conversion path; the pattern +// is retained as the reference the scan is conformance tested against. +var strictRFC3339Pattern = regexp.MustCompile( + `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$`) + +// isStrictRFC3339 reports whether s matches strictRFC3339Pattern, hand-rolled to +// keep the conversion path off the regexp engine and its per-call cost. +func isStrictRFC3339(s string) bool { + // Shortest accepted form is "2006-01-02T15:04:05Z" (20 bytes): a 19-byte + // fixed-width date-time followed by at least a 'Z'/'z' zone. + if len(s) < 20 { + return false + } + // full-date "T" partial-time + if !isYear(s[0:4]) || !isChar(s[4], '-') || !isMonth(s[5:7]) || !isChar(s[7], '-') || !isDay(s[8:10]) || + !isChar(s[10], 't') || + !isHour(s[11:13]) || !isChar(s[13], ':') || !isMinute(s[14:16]) || !isChar(s[16], ':') || !isSecond(s[17:19]) { + return false + } + rest := s[19:] + // optional fractional seconds: "." 1*DIGIT + if rest[0] == '.' { + rest = rest[1:] + n := 0 + for n < len(rest) && isDigit(rest[n]) { + n++ + } + if n == 0 { + return false + } + rest = rest[n:] + } + // time-offset: "Z" or ("+" / "-") time-hour ":" time-minute + if len(rest) == 1 { + return isChar(rest[0], 'z') + } + if len(rest) == 6 && (rest[0] == '+' || rest[0] == '-') { + return isHour(rest[1:3]) && isChar(rest[3], ':') && isMinute(rest[4:6]) + } + return false +} + +func isDigit(c byte) bool { return c >= '0' && c <= '9' } + +// isChar reports whether got is want, case-insensitively; want must be lower case. +func isChar(got, want byte) bool { + g, w := rune(got), rune(want) + return g == w || unicode.ToLower(g) == w +} + +// inRange reports whether s is all decimal digits and its value lies in [lo, hi]. +func inRange(s string, lo, hi uint64) bool { + u, err := strconv.ParseUint(s, 10, 64) + return err == nil && u >= lo && u <= hi +} + +func isYear(s string) bool { return inRange(s, 0, 9999) } +func isMonth(s string) bool { return inRange(s, 1, 12) } +func isDay(s string) bool { return inRange(s, 1, 31) } +func isHour(s string) bool { return inRange(s, 0, 23) } +func isMinute(s string) bool { return inRange(s, 0, 59) } + +// isSecond permits 60 for a leap second. +func isSecond(s string) bool { return inRange(s, 0, 60) } + // Add implements traits.Adder.Add. func (t Timestamp) Add(other ref.Val) ref.Val { switch other.Type() { @@ -302,6 +377,9 @@ func timeZone(tz ref.Val, visitor timestampVisitor) timestampVisitor { if err != nil { return WrapErr(err) } + if min < 0 || min > 59 { + return WrapErr(fmt.Errorf("timezone offset minutes out of range [0, 59]: %s", val)) + } var offset int if string(val[0]) == "-" { offset = hr*60 - min diff --git a/vendor/github.com/google/cel-go/common/types/unknown.go b/vendor/github.com/google/cel-go/common/types/unknown.go index 9dd2b257..f43aff18 100644 --- a/vendor/github.com/google/cel-go/common/types/unknown.go +++ b/vendor/github.com/google/cel-go/common/types/unknown.go @@ -16,6 +16,7 @@ package types import ( "fmt" + "maps" "math" "reflect" "sort" @@ -181,6 +182,20 @@ func (u *Unknown) GetAttributeTrails(id int64) ([]*AttributeTrail, bool) { return trails, found } +// HasUnknownFunction returns whether any of the attribute trails contained within the unknown +// are unspecified. Unspecified attributes typically indicate an unresolved function call +// or operation, rather than a missing variable. +func (u *Unknown) HasUnknownFunction() bool { + for _, trails := range u.attributeTrails { + for _, t := range trails { + if t.variable == "" { + return true + } + } + } + return false +} + // Contains returns true if the input unknown is a subset of the current unknown. func (u *Unknown) Contains(other *Unknown) bool { for id, otherTrails := range other.attributeTrails { @@ -283,9 +298,7 @@ func MergeUnknowns(unk1, unk2 *Unknown) *Unknown { out := &Unknown{ attributeTrails: make(map[int64][]*AttributeTrail, len(unk1.attributeTrails)+len(unk2.attributeTrails)), } - for id, ats := range unk1.attributeTrails { - out.attributeTrails[id] = ats - } + maps.Copy(out.attributeTrails, unk1.attributeTrails) for id, ats := range unk2.attributeTrails { existing, found := out.attributeTrails[id] if !found { diff --git a/vendor/github.com/google/cel-go/ext/BUILD.bazel b/vendor/github.com/google/cel-go/ext/BUILD.bazel index ef4f4ec3..f362fd97 100644 --- a/vendor/github.com/google/cel-go/ext/BUILD.bazel +++ b/vendor/github.com/google/cel-go/ext/BUILD.bazel @@ -9,6 +9,7 @@ go_library( srcs = [ "bindings.go", "comprehensions.go", + "costs.go", "encoders.go", "extension_option_factory.go", "formatting.go", @@ -17,6 +18,7 @@ go_library( "lists.go", "math.go", "native.go", + "network.go", "protos.go", "regex.go", "sets.go", @@ -39,6 +41,7 @@ go_library( "//common/types/traits:go_default_library", "//interpreter:go_default_library", "//parser:go_default_library", + "@org_golang_google_protobuf//encoding/protojson:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", "@org_golang_google_protobuf//types/known/structpb", @@ -60,6 +63,7 @@ go_test( "lists_test.go", "math_test.go", "native_test.go", + "network_test.go", "protos_test.go", "regex_test.go", "sets_test.go", diff --git a/vendor/github.com/google/cel-go/ext/README.md b/vendor/github.com/google/cel-go/ext/README.md index 6a7163de..6133b5cb 100644 --- a/vendor/github.com/google/cel-go/ext/README.md +++ b/vendor/github.com/google/cel-go/ext/README.md @@ -33,6 +33,8 @@ Encoding utilities for marshalling data into standardized representations. ### Base64.Decode +**Introduced in version 0 (cost support in version 1)** + Decodes base64-encoded string to bytes. This function will return an error if the string input is not @@ -47,6 +49,8 @@ Examples: ### Base64.Encode +**Introduced in version 0 (cost support in version 1)** + Encodes bytes to a base64-encoded string. base64.encode() -> @@ -55,6 +59,20 @@ Example: base64.encode(b'hello') // return 'aGVsbG8=' +### JSON.Encode + +Introduced at version: 1 + +Encodes a CEL value to a JSON string. + + json.encode() -> + +Examples: + + json.encode('hello') // return '"hello"' + json.encode([1, 'two', true]) // return '[1,"two",true]' + json.encode({'items': [1, 'two', false]}) // return '{"items":[1,"two",false]}' + ## Math Math helper macros and functions. @@ -66,6 +84,8 @@ intended; however, there is some chance for collision. ### Math.Greatest +**Introduced in version 0 (cost support in version 3)** + Returns the greatest valued number present in the arguments to the macro. Greatest is a variable argument count macro which must take at least one @@ -93,6 +113,8 @@ Examples: ### Math.Least +**Introduced in version 0 (cost support in version 3)** + Returns the least valued number present in the arguments to the macro. Least is a variable argument count macro which must take at least one diff --git a/vendor/github.com/google/cel-go/ext/bindings.go b/vendor/github.com/google/cel-go/ext/bindings.go index bef29ae2..89766d60 100644 --- a/vendor/github.com/google/cel-go/ext/bindings.go +++ b/vendor/github.com/google/cel-go/ext/bindings.go @@ -108,7 +108,7 @@ func (lib *celBindings) CompileOptions() []cel.EnvOption { func (lib *celBindings) ProgramOptions() []cel.ProgramOption { if lib.version >= 1 { - celBlockPlan := func(i interpreter.Interpretable) (interpreter.Interpretable, error) { + celBlockPlan := func(i interpreter.InterpretableV2) (interpreter.InterpretableV2, error) { call, ok := i.(interpreter.InterpretableCall) if !ok { return i, nil @@ -140,7 +140,7 @@ func (lib *celBindings) ProgramOptions() []cel.ProgramOption { return i, nil } } - return []cel.ProgramOption{cel.CustomDecorator(celBlockPlan)} + return []cel.ProgramOption{cel.CustomDecoratorV2(celBlockPlan)} } return []cel.ProgramOption{} } @@ -190,7 +190,7 @@ func celBind(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Ex ), nil } -func newDynamicBlock(slotExprs []interpreter.Interpretable, expr interpreter.Interpretable) interpreter.Interpretable { +func newDynamicBlock(slotExprs []interpreter.InterpretableV2, expr interpreter.InterpretableV2) interpreter.InterpretableV2 { bs := &dynamicBlock{ slotExprs: slotExprs, expr: expr, @@ -213,8 +213,8 @@ func newDynamicBlock(slotExprs []interpreter.Interpretable, expr interpreter.Int } type dynamicBlock struct { - slotExprs []interpreter.Interpretable - expr interpreter.Interpretable + slotExprs []interpreter.InterpretableV2 + expr interpreter.InterpretableV2 slotActivationPool *sync.Pool } @@ -223,12 +223,23 @@ func (b *dynamicBlock) ID() int64 { return b.expr.ID() } -// Eval implements the Interpretable interface method. -func (b *dynamicBlock) Eval(activation cel.Activation) ref.Val { +// Exec implements the Interpretable interface method and pushes a new frame onto the +// execution frame for the duration of the block execution. +func (b *dynamicBlock) Exec(frame *interpreter.ExecutionFrame) ref.Val { sa := b.slotActivationPool.Get().(*dynamicSlotActivation) - sa.Activation = activation + sa.frame = frame.Push(sa) + // Ensure the 'unwrapped' Activation points to the original one from the frame, + // and not the hierarchical activation which composes the original and the slot + // activation. + sa.Activation = frame.Activation + defer sa.frame.Pop() defer b.clearSlots(sa) - return b.expr.Eval(sa) + return b.expr.Exec(sa.frame) +} + +// Eval implements the Interpretable interface method. +func (b *dynamicBlock) Eval(activation cel.Activation) ref.Val { + return b.Exec(interpreter.AsFrame(activation)) } func (b *dynamicBlock) clearSlots(sa *dynamicSlotActivation) { @@ -243,7 +254,8 @@ type slotVal struct { type dynamicSlotActivation struct { cel.Activation - slotExprs []interpreter.Interpretable + frame *interpreter.ExecutionFrame + slotExprs []interpreter.InterpretableV2 slotCount int slotVals []*slotVal } @@ -267,7 +279,7 @@ func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) { return *v.value, true } v.visited = true - val := sa.slotExprs[idx].Eval(sa) + val := sa.slotExprs[idx].Exec(sa.frame) v.value = &val return val, true } @@ -276,13 +288,14 @@ func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) { func (sa *dynamicSlotActivation) reset() { sa.Activation = nil + sa.frame = nil for _, sv := range sa.slotVals { sv.visited = false sv.value = nil } } -func newConstantBlock(slots traits.Lister, expr interpreter.Interpretable) interpreter.Interpretable { +func newConstantBlock(slots traits.Lister, expr interpreter.InterpretableV2) interpreter.InterpretableV2 { count := slots.Size().(types.Int) return &constantBlock{slots: slots, slotCount: int(count), expr: expr} } @@ -290,7 +303,7 @@ func newConstantBlock(slots traits.Lister, expr interpreter.Interpretable) inter type constantBlock struct { slots traits.Lister slotCount int - expr interpreter.Interpretable + expr interpreter.InterpretableV2 } // ID implements the interpreter.Interpretable interface method. @@ -298,15 +311,24 @@ func (b *constantBlock) ID() int64 { return b.expr.ID() } +// Exec implements the Interpretable interface method and pushes a new frame onto the +// stack for the duration of the block execution. +func (b *constantBlock) Exec(frame *interpreter.ExecutionFrame) ref.Val { + sa := constantSlotActivation{Activation: frame.Activation, slots: b.slots, slotCount: b.slotCount} + sa.frame = frame.Push(sa) + defer sa.frame.Pop() + return b.expr.Exec(sa.frame) +} + // Eval implements the interpreter.Interpretable interface method, and will proxy @index prefixed variable // lookups into a set of constant slots determined from the plan step. func (b *constantBlock) Eval(activation cel.Activation) ref.Val { - vars := constantSlotActivation{Activation: activation, slots: b.slots, slotCount: b.slotCount} - return b.expr.Eval(vars) + return b.Exec(interpreter.AsFrame(activation)) } type constantSlotActivation struct { cel.Activation + frame *interpreter.ExecutionFrame slots traits.Lister slotCount int } diff --git a/vendor/github.com/google/cel-go/ext/costs.go b/vendor/github.com/google/cel-go/ext/costs.go new file mode 100644 index 00000000..d2cf7c75 --- /dev/null +++ b/vendor/github.com/google/cel-go/ext/costs.go @@ -0,0 +1,122 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ext + +import ( + "math" + + "github.com/google/cel-go/checker" + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +var ( + callCostEstimate = checker.FixedCostEstimate(1) + callCost = uint64(1) + listAllocCost = checker.FixedCostEstimate(common.ListCreateBaseCost) + stringCostFactor = common.StringTraversalCostFactor +) + +func estimateStringScan(sz checker.SizeEstimate) (checker.CostEstimate, *checker.SizeEstimate) { + return estimateTraversal(sz, stringCostFactor, nil) +} + +func estimateListAlloc(sz checker.SizeEstimate, costFactor float64) (checker.CostEstimate, *checker.SizeEstimate) { + return estimateTraversal(sz, costFactor, &listAllocCost) +} + +// estimateTraversal computes cost as a function of the size of the target object and whether the call allocates memory. +func estimateTraversal(nodeSize checker.SizeEstimate, costFactor float64, allocationCost *checker.CostEstimate) (checker.CostEstimate, *checker.SizeEstimate) { + cost := nodeSize.MultiplyByCostFactor(costFactor) + if allocationCost != nil { + cost = cost.Add(*allocationCost) + } + return cost, &nodeSize +} + +func estimateSize(estimator checker.CostEstimator, node checker.AstNode) checker.SizeEstimate { + if l := node.ComputedSize(); l != nil { + return *l + } + if l := estimator.EstimateSize(node); l != nil { + return *l + } + return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} +} + +func actualSize(value ref.Val) uint64 { + if sz, ok := value.(traits.Sizer); ok { + return uint64(sz.Size().(types.Int)) + } + return 1 +} + +func nodeAsUintValue(node checker.AstNode, defaultVal uint64) uint64 { + if node.Expr().Kind() != ast.LiteralKind { + return defaultVal + } + lit := node.Expr().AsLiteral() + if lit.Type() != types.IntType { + return defaultVal + } + val := lit.(types.Int) + if val < types.IntZero { + return 0 + } + return uint64(lit.(types.Int)) +} + +func callEstimate(cost checker.CostEstimate, sz *checker.SizeEstimate) *checker.CallEstimate { + return &checker.CallEstimate{CostEstimate: cost, ResultSize: sz} +} + +func rangedSizeEstimate(min, max uint64) checker.SizeEstimate { + return checker.SizeEstimate{Min: min, Max: max} +} + +func fixedSizeEstimate(val uint64) checker.SizeEstimate { + return checker.FixedSizeEstimate(val) +} + +func atLeastOne(size checker.SizeEstimate) checker.SizeEstimate { + if size.Min == 0 { + size.Min = 1 + } + if size.Max == 0 { + size.Max = 1 + } + return size +} + +func safeAdd(x, y uint64, rest ...uint64) uint64 { + if y > 0 && x > math.MaxUint64-y { + return math.MaxUint64 + } + next := x + y + if len(rest) == 0 { + return next + } + return safeAdd(next, rest[0], rest[1:]...) +} + +func safeMul(x, y uint64) uint64 { + if y != 0 && x > math.MaxUint64/y { + return math.MaxUint64 + } + return x * y +} diff --git a/vendor/github.com/google/cel-go/ext/encoders.go b/vendor/github.com/google/cel-go/ext/encoders.go index 731c3d09..97fc932a 100644 --- a/vendor/github.com/google/cel-go/ext/encoders.go +++ b/vendor/github.com/google/cel-go/ext/encoders.go @@ -16,11 +16,17 @@ package ext import ( "encoding/base64" + "encoding/json" + "fmt" "math" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" ) // Encoders returns a cel.EnvOption to configure extended functions for string, byte, and object @@ -48,6 +54,18 @@ import ( // Examples: // // base64.encode(b'hello') // return b'aGVsbG8=' +// +// # JSON.Encode +// +// Introduced at version: 1 +// +// Encodes a CEL value to a JSON string. +// +// json.encode() -> +// +// Examples: +// +// json.encode({'hello': 'world'}) // return '{"hello":"world"}' func Encoders(options ...EncodersOption) cel.EnvOption { l := &encoderLib{version: math.MaxUint32} for _, o := range options { @@ -75,8 +93,8 @@ func (*encoderLib) LibraryName() string { return "cel.lib.ext.encoders" } -func (*encoderLib) CompileOptions() []cel.EnvOption { - return []cel.EnvOption{ +func (lib *encoderLib) CompileOptions() []cel.EnvOption { + opts := []cel.EnvOption{ cel.Function("base64.decode", cel.Overload("base64_decode_string", []*cel.Type{cel.StringType}, cel.BytesType, cel.UnaryBinding(func(str ref.Val) ref.Val { @@ -90,10 +108,35 @@ func (*encoderLib) CompileOptions() []cel.EnvOption { return stringOrError(base64EncodeBytes([]byte(b))) }))), } + if lib.version >= 1 { + estimators := []checker.CostOption{ + checker.OverloadCostEstimate("base64_decode_string", estimateDecode), + checker.OverloadCostEstimate("base64_encode_bytes", estimateEncode), + checker.OverloadCostEstimate("json_encode_dyn", estimateJSONEncode), + } + opts = append(opts, cel.CostEstimatorOptions(estimators...)) + opts = append(opts, + cel.Function("json.encode", + cel.Overload("json_encode_dyn", []*cel.Type{cel.DynType}, cel.StringType, + cel.UnaryBinding(func(val ref.Val) ref.Val { + return stringOrError(jsonEncodeValue(val)) + }))), + ) + } + return opts } -func (*encoderLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} +func (lib *encoderLib) ProgramOptions() []cel.ProgramOption { + var opts []cel.ProgramOption + if lib.version >= 1 { + trackers := []interpreter.CostTrackerOption{ + interpreter.OverloadCostTracker("base64_decode_string", trackDecode), + interpreter.OverloadCostTracker("base64_encode_bytes", trackEncode), + interpreter.OverloadCostTracker("json_encode_dyn", trackJSONEncode), + } + opts = append(opts, cel.CostTrackerOptions(trackers...)) + } + return opts } func base64DecodeString(str string) ([]byte, error) { @@ -110,3 +153,93 @@ func base64DecodeString(str string) ([]byte, error) { func base64EncodeBytes(bytes []byte) (string, error) { return base64.StdEncoding.EncodeToString(bytes), nil } + +func estimateEncode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(stringCostFactor).Add(callCostEstimate) + resSize := estimateEncodeSize(sz) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resSize} +} + +func estimateJSONEncode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + size := estimateJSONEncodeSize() + return &checker.CallEstimate{CostEstimate: checker.UnknownCostEstimate(), ResultSize: &size} +} + +func estimateDecode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(stringCostFactor).Add(callCostEstimate) + resSize := estimateDecodeSize(sz) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resSize} +} + +func trackEncode(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := uint64(math.Ceil(float64(sz)*stringCostFactor)) + callCost + return &cost +} + +func trackJSONEncode(args []ref.Val, _ ref.Val) *uint64 { + maxCost := uint64(math.MaxUint64) + return &maxCost +} + +func trackDecode(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := uint64(math.Ceil(float64(sz)*stringCostFactor)) + callCost + return &cost +} + +func estimateEncodeSize(sz checker.SizeEstimate) checker.SizeEstimate { + minVal := (sz.Min*4 + 2) / 3 + maxVal := (sz.Max*4 + 2) / 3 + if sz.Max > math.MaxUint64/4 { + maxVal = math.MaxUint64 + } + return checker.SizeEstimate{Min: minVal, Max: maxVal} +} + +func estimateJSONEncodeSize() checker.SizeEstimate { + // TODO: provide a more sophisticated size estimate based on the CEL value's type. + return checker.UnknownSizeEstimate() +} + +func estimateDecodeSize(sz checker.SizeEstimate) checker.SizeEstimate { + minVal := sz.Min * 3 / 4 + maxVal := sz.Max * 3 / 4 + return checker.SizeEstimate{Min: minVal, Max: maxVal} +} + +func jsonEncodeValue(val ref.Val) (string, error) { + native, err := val.ConvertToNative(types.JSONValueType) + if err != nil { + return "", err + } + jsonValue, ok := native.(*structpb.Value) + if !ok { + return "", fmt.Errorf("cannot convert %T to JSON value", native) + } + jsonBytes, err := protojson.Marshal(jsonValue) + if err != nil { + return "", err + } + var obj interface{} + if err := json.Unmarshal(jsonBytes, &obj); err != nil { + return "", fmt.Errorf("unmarshaling protojson: %w", err) + } + // Re-marshal with standard json.Marshal for deterministic compact output + jsonBytes, err = json.Marshal(obj) + if err != nil { + return "", fmt.Errorf("re-marshaling value: %w", err) + } + return string(jsonBytes), nil +} diff --git a/vendor/github.com/google/cel-go/ext/lists.go b/vendor/github.com/google/cel-go/ext/lists.go index b27ddf22..3d0e6764 100644 --- a/vendor/github.com/google/cel-go/ext/lists.go +++ b/vendor/github.com/google/cel-go/ext/lists.go @@ -153,15 +153,18 @@ var comparableTypes = []*cel.Type{ // ].sortBy(e, e.score).map(e, e.name) // == ["bar", "foo", "baz"] func Lists(options ...ListsOption) cel.EnvOption { - l := &listsLib{version: math.MaxUint32} + l := &listsLib{version: math.MaxUint32, maxRangeSize: defaultMaxRangeSize} for _, o := range options { l = o(l) } return cel.Lib(l) } +const defaultMaxRangeSize = 1_000_000 + type listsLib struct { - version uint32 + version uint32 + maxRangeSize int64 } // LibraryName implements the SingletonLibrary interface method. @@ -188,6 +191,16 @@ func ListsVersion(version uint32) ListsOption { } } +// ListsMaxRangeSize sets the maximum number of elements lists.range() will +// allocate. If not set, the default is 10,000,000. Setting this to zero +// disables the limit (not recommended). +func ListsMaxRangeSize(size int64) ListsOption { + return func(lib *listsLib) *listsLib { + lib.maxRangeSize = size + return lib + } +} + // CompileOptions implements the Library interface method. func (lib listsLib) CompileOptions() []cel.EnvOption { listType := cel.ListType(cel.TypeParamType("T")) @@ -309,11 +322,12 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { )..., )) + maxRange := lib.maxRangeSize opts = append(opts, cel.Function("lists.range", cel.Overload("lists_range", []*cel.Type{cel.IntType}, cel.ListType(cel.IntType), cel.UnaryBinding(func(n ref.Val) ref.Val { - result, err := genRange(n.(types.Int)) + result, err := genRange(n.(types.Int), maxRange) if err != nil { return types.WrapErr(err) } @@ -349,23 +363,45 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { if lib.version >= 3 { estimators := []checker.CostOption{ checker.OverloadCostEstimate("list_slice", estimateListSlice), - checker.OverloadCostEstimate("list_flatten", estimateListFlatten), - checker.OverloadCostEstimate("list_flatten_int", estimateListFlatten), checker.OverloadCostEstimate("lists_range", estimateListsRange), checker.OverloadCostEstimate("list_reverse", estimateListReverse), - checker.OverloadCostEstimate("list_distinct", estimateListDistinct), } - for _, t := range comparableTypes { + if lib.version == 3 { estimators = append(estimators, - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sort", t.TypeName()), - estimateListSort(t), - ), - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), - estimateListSortBy(t), - ), + checker.OverloadCostEstimate("list_flatten", estimateListFlattenLegacy), + checker.OverloadCostEstimate("list_flatten_int", estimateListFlattenLegacy), + checker.OverloadCostEstimate("list_distinct", estimateListDistinctLegacy), ) + for _, t := range comparableTypes { + estimators = append(estimators, + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sort", t.TypeName()), + estimateListSortLegacy(t), + ), + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), + estimateListSortByLegacy(t), + ), + ) + } + } else { + estimators = append(estimators, + checker.OverloadCostEstimate("list_flatten", estimateListFlatten), + checker.OverloadCostEstimate("list_flatten_int", estimateListFlatten), + checker.OverloadCostEstimate("list_distinct", estimateListDistinct), + ) + for _, t := range comparableTypes { + estimators = append(estimators, + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sort", t.TypeName()), + estimateListSort(t), + ), + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), + estimateListSortBy(t), + ), + ) + } } opts = append(opts, cel.CostEstimatorOptions(estimators...)) } @@ -377,15 +413,23 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { func (lib *listsLib) ProgramOptions() []cel.ProgramOption { var opts []cel.ProgramOption if lib.version >= 3 { - // TODO: Add cost trackers for list operations trackers := []interpreter.CostTrackerOption{ interpreter.OverloadCostTracker("list_slice", trackListOutputSize), - interpreter.OverloadCostTracker("list_flatten", trackListFlatten), - interpreter.OverloadCostTracker("list_flatten_int", trackListFlatten), interpreter.OverloadCostTracker("lists_range", trackListOutputSize), interpreter.OverloadCostTracker("list_reverse", trackListOutputSize), interpreter.OverloadCostTracker("list_distinct", trackListDistinct), } + if lib.version == 3 { + trackers = append(trackers, + interpreter.OverloadCostTracker("list_flatten", trackListFlattenLegacy), + interpreter.OverloadCostTracker("list_flatten_int", trackListFlattenLegacy), + ) + } else { + trackers = append(trackers, + interpreter.OverloadCostTracker("list_flatten", trackListFlatten), + interpreter.OverloadCostTracker("list_flatten_int", trackListFlatten), + ) + } for _, t := range comparableTypes { trackers = append(trackers, interpreter.OverloadCostTracker( @@ -403,8 +447,14 @@ func (lib *listsLib) ProgramOptions() []cel.ProgramOption { return opts } -func genRange(n types.Int) (ref.Val, error) { - var newList []ref.Val +func genRange(n types.Int, maxSize int64) (ref.Val, error) { + if n < 0 { + return nil, fmt.Errorf("lists.range: size must be non-negative, got %d", n) + } + if maxSize > 0 && int64(n) > maxSize { + return nil, fmt.Errorf("lists.range: size %d exceeds maximum allowed (%d)", n, maxSize) + } + newList := make([]ref.Val, 0, n) for i := types.Int(0); i < n; i++ { newList = append(newList, i) } @@ -616,8 +666,8 @@ func estimateListSlice(estimator checker.CostEstimator, target *checker.AstNode, return nil } sz := estimateSize(estimator, *target) - start := nodeAsIntValue(args[0], 0) - end := nodeAsIntValue(args[1], sz.Max) + start := nodeAsUintValue(args[0], 0) + end := nodeAsUintValue(args[1], sz.Max) return estimateAllocatingListCall(1, checker.FixedSizeEstimate(end-start)) } @@ -626,7 +676,7 @@ func estimateListsRange(estimator checker.CostEstimator, target *checker.AstNode if target != nil || len(args) != 1 { return nil } - return estimateAllocatingListCall(1, checker.FixedSizeEstimate(nodeAsIntValue(args[0], math.MaxUint))) + return estimateAllocatingListCall(1, checker.FixedSizeEstimate(nodeAsUintValue(args[0], math.MaxUint))) } // estimateListReverse computes an O(n) reverse operation with a cost factor of 1. @@ -637,18 +687,73 @@ func estimateListReverse(estimator checker.CostEstimator, target *checker.AstNod return estimateAllocatingListCall(1, estimateSize(estimator, *target)) } -// estimateListFlatten computes an O(n) flatten operation with a cost factor proportional to the flatten depth. +// estimateListFlatten computes an O(n) flatten operation with a cost factor proportional to the total number of flattened items. func estimateListFlatten(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { if target == nil || len(args) > 1 { return nil } depth := uint64(1) if len(args) == 1 { - depth = nodeAsIntValue(args[0], math.MaxUint) + depth = nodeAsUintValue(args[0], math.MaxUint) + } + var resSize checker.SizeEstimate + if (*target).Expr() != nil && (*target).Expr().Kind() == ast.ListKind { + szVal := estimateLiteralFlattenSize((*target).Expr(), depth) + resSize = checker.FixedSizeEstimate(szVal) + } else { + resSize = estimateFlattenSize(estimator, *target, depth) + } + cost := resSize.AsCost() + return estimateListCallWithDirectCost(cost, resSize, true) +} + +func estimateListFlattenLegacy(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) > 1 { + return nil + } + depth := uint64(1) + if len(args) == 1 { + depth = nodeAsUintValue(args[0], math.MaxUint) } return estimateAllocatingListCall(float64(depth), estimateSize(estimator, *target)) } +func estimateFlattenSize(estimator checker.CostEstimator, node checker.AstNode, depth uint64) checker.SizeEstimate { + sz := estimateSize(estimator, node) + if depth == 0 { + return sz + } + tType := node.Type() + if tType.Kind() != types.ListKind || len(tType.Parameters()) == 0 { + return sz + } + elemType := tType.Parameters()[0] + elemNode := pathAstNode{ + path: append(append([]string(nil), node.Path()...), "@items"), + t: elemType, + } + flatElemSize := estimateFlattenSize(estimator, elemNode, depth-1) + return sz.Multiply(flatElemSize) +} + +func estimateLiteralFlattenSize(expr ast.Expr, depth uint64) uint64 { + if depth == 0 { + if expr.Kind() == ast.ListKind { + return uint64(expr.AsList().Size()) + } + return 1 + } + if expr.Kind() != ast.ListKind { + return 1 + } + listExpr := expr.AsList() + totalSize := uint64(0) + for _, el := range listExpr.Elements() { + totalSize += estimateLiteralFlattenSize(el, depth-1) + } + return totalSize +} + // Compute an O(n^2) with a cost factor of 2, equivalent to sets.contains with a result list // which can vary in size from 1 element to the original list size. func estimateListDistinct(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { @@ -656,8 +761,23 @@ func estimateListDistinct(estimator checker.CostEstimator, target *checker.AstNo return nil } sz := estimateSize(estimator, *target) - costFactor := 2.0 - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) + elemType := types.DynType + tType := (*target).Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType = tType.Parameters()[0] + } + itemSize := estimateItemSize(estimator, *target) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + + minSize := uint64(0) + if sz.Min > 0 { + minSize = 1 + } + resultSize := checker.SizeEstimate{Min: minSize, Max: sz.Max} + return estimateListCallWithDirectCost(cost, resultSize, true) } // estimateListSort computes an O(n^2) sort operation with a cost factor of 2 for the equality @@ -678,37 +798,53 @@ func estimateListSortBy(u *types.Type) checker.FunctionEstimator { if target == nil || len(args) != 1 { return nil } - // Estimate the size of the list used as the sort index - return estimateListSortCost(estimator, args[0], u) + // Estimate the size of the list used as the sort index, using target to resolve item size hints. + return estimateListSortByCost(estimator, *target, args[0], u) } } +func estimateListSortByCost(estimator checker.CostEstimator, target checker.AstNode, keysNode checker.AstNode, elemType *types.Type) *checker.CallEstimate { + sz := estimateSize(estimator, keysNode) + itemSize := estimateItemSize(estimator, target) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + return estimateListCallWithDirectCost(cost, sz, true) +} + // estimateListSortCost estimates an O(n^2) sort operation with a cost factor of 2 for the equality // operations which occur during the sort computation. func estimateListSortCost(estimator checker.CostEstimator, node checker.AstNode, elemType *types.Type) *checker.CallEstimate { sz := estimateSize(estimator, node) - costFactor := 2.0 - switch elemType { - case types.StringType, types.BytesType: - costFactor += common.StringTraversalCostFactor - } - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) + itemSize := estimateItemSize(estimator, node) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + return estimateListCallWithDirectCost(cost, sz, true) } // estimateAllocatingListCall computes cost as a function of the size of the result list with a // baseline cost for the call dispatch and the associated list allocation. func estimateAllocatingListCall(costFactor float64, listSize checker.SizeEstimate) *checker.CallEstimate { - return estimateListCall(costFactor, listSize, true) + return estimateListCallWithResultSize(costFactor, listSize, listSize, true) +} + +// estimateListCallWithResultSize computes cost as a function of the size of the target list and whether the +// call allocates memory, using a separate result size estimate for the output list. +func estimateListCallWithResultSize(costFactor float64, costSize checker.SizeEstimate, resultSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { + cost := costSize.MultiplyByCostFactor(costFactor) + return estimateListCallWithDirectCost(cost, resultSize, allocates) } -// estimateListCall computes cost as a function of the size of the target list and whether the -// call allocates memory. -func estimateListCall(costFactor float64, listSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { - cost := listSize.MultiplyByCostFactor(costFactor).Add(callCostEstimate) +// estimateListCallWithDirectCost computes cost using a pre-calculated CostEstimate and a separate result size estimate. +func estimateListCallWithDirectCost(cost checker.CostEstimate, resultSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { if allocates { cost = cost.Add(checker.FixedCostEstimate(common.ListCreateBaseCost)) } - return &checker.CallEstimate{CostEstimate: cost, ResultSize: &listSize} + cost = cost.Add(callCostEstimate) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resultSize} } // trackListOutputSize computes cost as a function of the size of the result list. @@ -716,9 +852,13 @@ func trackListOutputSize(_ []ref.Val, result ref.Val) *uint64 { return trackAllocatingListCall(1, actualSize(result)) } -// trackListFlatten computes cost as a function of the size of the result list and the depth of -// the flatten operation. -func trackListFlatten(args []ref.Val, _ ref.Val) *uint64 { +// trackListFlatten computes cost as a function of the size of the result list. +func trackListFlatten(args []ref.Val, result ref.Val) *uint64 { + resSize := actualSize(result) + return trackAllocatingListCall(1.0, resSize) +} + +func trackListFlattenLegacy(args []ref.Val, _ ref.Val) *uint64 { depth := 1.0 if len(args) == 2 { depth = float64(args[1].(types.Int)) @@ -753,27 +893,111 @@ func trackListSelfCompare(l traits.Lister) *uint64 { if elem.Type() == types.StringType || elem.Type() == types.BytesType { costFactor += common.StringTraversalCostFactor } - return trackAllocatingListCall(costFactor, sz*sz) + return trackAllocatingListCall(costFactor, safeMul(sz, sz)) } // trackAllocatingListCall computes costs as a function of the size of the result list with a baseline cost // for the call dispatch and the associated list allocation. func trackAllocatingListCall(costFactor float64, size uint64) *uint64 { - cost := uint64(float64(size)*costFactor) + callCost + common.ListCreateBaseCost + if costFactor < 0.0 { + costFactor = 1.0 + } + cost := safeAdd(uint64(float64(size)*costFactor), callCost, common.ListCreateBaseCost) return &cost } -func nodeAsIntValue(node checker.AstNode, defaultVal uint64) uint64 { - if node.Expr().Kind() != ast.LiteralKind { - return defaultVal +func estimateListDistinctLegacy(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + sz := estimateSize(estimator, *target) + costFactor := 2.0 + tType := (*target).Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType := tType.Parameters()[0] + if elemType.Kind() == types.StringKind || elemType.Kind() == types.BytesKind { + costFactor += common.StringTraversalCostFactor + } + } + return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) +} + +func estimateListSortLegacy(t *types.Type) checker.FunctionEstimator { + return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + return estimateListSortCostLegacy(estimator, *target, t) + } +} + +func estimateListSortByLegacy(u *types.Type) checker.FunctionEstimator { + return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 1 { + return nil + } + return estimateListSortCostLegacy(estimator, args[0], u) + } +} + +func estimateListSortCostLegacy(estimator checker.CostEstimator, node checker.AstNode, elemType *types.Type) *checker.CallEstimate { + sz := estimateSize(estimator, node) + costFactor := 2.0 + switch elemType { + case types.StringType, types.BytesType: + costFactor += common.StringTraversalCostFactor } - lit := node.Expr().AsLiteral() - if lit.Type() != types.IntType { - return defaultVal + return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) +} + +type pathAstNode struct { + path []string + t *types.Type +} + +func (p pathAstNode) Path() []string { + return p.path +} + +func (p pathAstNode) Type() *types.Type { + return p.t +} + +func (p pathAstNode) Expr() ast.Expr { + return nil +} + +func (p pathAstNode) ComputedSize() *checker.SizeEstimate { + return nil +} + +func estimateItemSize(estimator checker.CostEstimator, node checker.AstNode) checker.SizeEstimate { + path := node.Path() + if len(path) == 0 { + return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} + } + elemType := types.DynType + tType := node.Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType = tType.Parameters()[0] + } + itemNode := pathAstNode{ + path: append(append([]string(nil), path...), "@items"), + t: elemType, } - val := lit.(types.Int) - if val < types.IntZero { - return 0 + if l := estimator.EstimateSize(itemNode); l != nil { + return *l + } + return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} +} + +func estimateElementEqualityCost(estimator checker.CostEstimator, elemType *types.Type, itemSize checker.SizeEstimate) checker.CostEstimate { + switch elemType.Kind() { + case types.StringKind, types.BytesKind: + return itemSize.MultiplyByCostFactor(common.StringTraversalCostFactor) + case types.ListKind, types.MapKind, types.StructKind: + return checker.UnknownCostEstimate() + default: + return checker.FixedCostEstimate(1) } - return uint64(lit.(types.Int)) } diff --git a/vendor/github.com/google/cel-go/ext/math.go b/vendor/github.com/google/cel-go/ext/math.go index 6df8e377..e67b205d 100644 --- a/vendor/github.com/google/cel-go/ext/math.go +++ b/vendor/github.com/google/cel-go/ext/math.go @@ -20,10 +20,12 @@ import ( "strings" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" + "github.com/google/cel-go/interpreter" ) // Math returns a cel.EnvOption to configure namespaced math helper macros and @@ -339,9 +341,9 @@ import ( // // Examples: // -// math.sqrt(81) // returns 9.0 -// math.sqrt(985.25) // returns 31.388692231439016 -// math.sqrt(-15) // returns NaN +// math.sqrt(81) // returns 9.0 +// math.sqrt(985.25) // returns 31.388692231439016 +// math.sqrt(-15) // returns NaN func Math(options ...MathOption) cel.EnvOption { m := &mathLib{version: math.MaxUint32} for _, o := range options { @@ -580,12 +582,35 @@ func (lib *mathLib) CompileOptions() []cel.EnvOption { ), ) } + if lib.version >= 3 { + estimators := []checker.CostOption{ + checker.OverloadCostEstimate("math_@min_list_double", estimateMathListCost), + checker.OverloadCostEstimate("math_@min_list_int", estimateMathListCost), + checker.OverloadCostEstimate("math_@min_list_uint", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_double", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_int", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_uint", estimateMathListCost), + } + opts = append(opts, cel.CostEstimatorOptions(estimators...)) + } return opts } // ProgramOptions implements the Library interface method. -func (*mathLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} +func (lib *mathLib) ProgramOptions() []cel.ProgramOption { + var opts []cel.ProgramOption + if lib.version >= 3 { + trackers := []interpreter.CostTrackerOption{ + interpreter.OverloadCostTracker("math_@min_list_double", trackMathListCost), + interpreter.OverloadCostTracker("math_@min_list_int", trackMathListCost), + interpreter.OverloadCostTracker("math_@min_list_uint", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_double", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_int", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_uint", trackMathListCost), + } + opts = append(opts, cel.CostTrackerOptions(trackers...)) + } + return opts } func mathLeast(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) { @@ -723,21 +748,19 @@ func sign(val ref.Val) ref.Val { } } - func sqrt(val ref.Val) ref.Val { switch v := val.(type) { case types.Double: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Int: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Uint: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) default: - return types.NewErr("no such overload: sqrt") + return types.NewErr("no such overload: sqrt") } } - func bitAndPairInt(first, second ref.Val) ref.Val { l := first.(types.Int) r := second.(types.Int) @@ -946,3 +969,19 @@ func maybeSuffixError(val ref.Val, suffix string) ref.Val { } return val } + +func estimateMathListCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(1.0).Add(callCostEstimate) + resultSize := checker.FixedSizeEstimate(1) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resultSize} +} + +func trackMathListCost(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := safeAdd(sz, callCost) + return &cost +} diff --git a/vendor/github.com/google/cel-go/ext/native.go b/vendor/github.com/google/cel-go/ext/native.go index c30f26ad..d9f5fab0 100644 --- a/vendor/github.com/google/cel-go/ext/native.go +++ b/vendor/github.com/google/cel-go/ext/native.go @@ -164,6 +164,10 @@ func fieldNameByTag(structTagToParse string) func(field reflect.StructField) str } } +func isSkippedFieldName(name string) bool { + return name == "" || name == "-" +} + type nativeTypeOptions struct { // fieldNameHandler controls how CEL should perform struct field renames. // This is most commonly used for switching to parsing based off the struct field tag, @@ -286,9 +290,13 @@ func toFieldName(fieldNameHandler NativeTypesFieldNameHandler, f reflect.StructF func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) { if t, found := tp.nativeTypes[typeName]; found { fieldCount := t.refType.NumField() - fields := make([]string, fieldCount) + fields := make([]string, 0, fieldCount) for i := 0; i < fieldCount; i++ { - fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) + fieldName := toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) + if isSkippedFieldName(fieldName) { + continue + } + fields = append(fields, fieldName) } return fields, true } @@ -509,6 +517,9 @@ func (o *nativeObj) ConvertToNative(typeDesc reflect.Type) (any, error) { continue } fieldName := toFieldName(o.valType.fieldNameHandler, fieldType) + if isSkippedFieldName(fieldName) { + continue + } fieldCELVal := o.NativeToValue(fieldValue.Interface()) fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType) if err != nil { @@ -667,7 +678,9 @@ func newNativeType(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect for idx := 0; idx < refType.NumField(); idx++ { field := refType.Field(idx) fieldName := toFieldName(fieldNameHandler, field) - + if isSkippedFieldName(fieldName) { + continue + } if _, found := fieldNames[fieldName]; found { return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName) } else { @@ -737,6 +750,10 @@ func (t *nativeType) Value() any { // fieldByName returns the corresponding reflect.StructField for the give name either by matching // field tag or field name. func (t *nativeType) fieldByName(fieldName string) (reflect.StructField, bool) { + if isSkippedFieldName(fieldName) { + return reflect.StructField{}, false + } + if t.fieldNameHandler == nil { return t.refType.FieldByName(fieldName) } diff --git a/vendor/github.com/google/cel-go/ext/network.go b/vendor/github.com/google/cel-go/ext/network.go new file mode 100644 index 00000000..bca06570 --- /dev/null +++ b/vendor/github.com/google/cel-go/ext/network.go @@ -0,0 +1,810 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ext + +import ( + "fmt" + "math" + "net/netip" + "reflect" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" +) + +const ( + // Version1 is the initial version of the Network library, providing + // parity with Kubernetes v1.30+ CEL network functions. + Version1 uint32 = 1 +) + +// Network returns a cel.EnvOption to configure extended functions for network +// address parsing, inspection, and CIDR range manipulation. +// +// Note: This library defines global functions `ip`, `cidr`, `isIP`, `isCIDR` +// and `ip.isCanonical`. If you are currently using variables named `ip` or +// `cidr`, these functions will likely work as intended, however there is a +// chance for collision. +// +// The library closely mirrors the behavior of the Kubernetes CEL network +// libraries, treating IP addresses and CIDR ranges as opaque types. It parses +// IPs strictly: IPv4-mapped IPv6 addresses and IP zones are not allowed. +// +// This library includes a TypeAdapter that allows `netip.Addr` and +// `netip.Prefix` Go types to be passed directly into the CEL environment. +// +// # IP Addresses +// +// The `ip` function converts a string to an IP address (IPv4 or IPv6). If the +// string is not a valid IP, an error is returned. The `isIP` function checks +// if a string is a valid IP address without throwing an error. +// +// ip(string) -> ip +// isIP(string) -> bool +// +// Examples: +// +// ip('127.0.0.1') +// ip('::1') +// isIP('1.2.3.4') // true +// isIP('invalid') // false +// +// # CIDR Ranges +// +// The `cidr` function converts a string to a Classless Inter-Domain Routing +// (CIDR) range. If the string is not valid, an error is returned. +// +// The `isCIDR` function checks if a string is a valid CIDR notation. Note that +// `isCIDR` allows CIDR values with or without host bits (e.g., '10.0.0.1/8' +// or '10.0.0.0/8'). +// +// cidr(string) -> cidr +// isCIDR(string) -> bool +// +// Examples: +// +// cidr('192.168.0.0/24') +// cidr('::1/128') +// isCIDR('10.0.0.0/8') // true +// isCIDR('10.0.0.1/8') // true +// +// # IP Inspection and Canonicalization +// +// IP objects support various inspection methods. +// +// .family() -> int +// .isLoopback() -> bool +// .isGlobalUnicast() -> bool +// .isLinkLocalMulticast() -> bool +// .isLinkLocalUnicast() -> bool +// .isUnspecified() -> bool +// +// The `ip.isCanonical` function takes a string and returns true if it matches +// the RFC 5952 canonical string representation of that address. +// +// ip.isCanonical(string) -> bool +// +// Examples: +// +// ip('127.0.0.1').family() == 4 +// ip('::1').family() == 6 +// ip('127.0.0.1').isLoopback() == true +// ip.isCanonical('2001:db8::1') == true // RFC 5952 format +// ip.isCanonical('2001:DB8::1') == false // Uppercase is not canonical +// ip.isCanonical('2001:db8:0:0:0:0:0:1') == false // Expanded is not canonical +// +// # CIDR Member Functions +// +// CIDR objects support containment checks and property extraction. +// +// .containsIP(ip|string) -> bool +// .containsCIDR(cidr|string) -> bool +// .ip() -> ip +// .isMask() -> bool +// .masked() -> cidr +// .prefixLength() -> int +// +// Examples: +// +// cidr('10.0.0.0/8').containsIP(ip('10.0.0.1')) == true +// cidr('10.0.0.0/8').containsIP('10.0.0.1') == true +// cidr('10.0.0.0/8').containsCIDR('10.1.0.0/16') == true +// cidr('192.168.1.5/24').ip() == ip('192.168.1.5') +// cidr('192.168.1.0/24').isMask() == true +// cidr('192.168.1.5/24').isMask() == false +// cidr('192.168.1.5/24').masked() == cidr('192.168.1.0/24') +// cidr('192.168.1.0/24').prefixLength() == 24 +func Network(opts ...NetworkOption) cel.EnvOption { + lib := &networkLib{version: Version1} + for _, o := range opts { + lib = o(lib) + } + return func(e *cel.Env) (*cel.Env, error) { + // Install the library (Types and Functions) + e, err := cel.Lib(lib)(e) + if err != nil { + return nil, err + } + + // Install the Adapter (Wrapping the existing one) + adapter := &networkAdapter{Adapter: e.CELTypeAdapter()} + return cel.CustomTypeAdapter(adapter)(e) + } +} + +// NetworkOption declares a functional operator for configuring the Network library behavior. +type NetworkOption func(*networkLib) *networkLib + +// NetworkVersion sets the version of the network library to an explicit version. +func NetworkVersion(version uint32) NetworkOption { + return func(lib *networkLib) *networkLib { + lib.version = version + return lib + } +} + +const ( + // Function names matching the original Kubernetes implementation of this networking library. + // isStrictCIDR and isInterfaceAddress are added to enable strict isCIDR parsing without breaking + // functionality for existing users. Ctx: https://github.com/kubernetes/kubernetes/issues/134224 + cidrFunc = "cidr" + cidrToString = "string" + containsCIDRFunc = "containsCIDR" + containsIPFunc = "containsIP" + familyFunc = "family" + ipFunc = "ip" + ipToString = "string" + isCanonicalFunc = "ip.isCanonical" + isCIDRFunc = "isCIDR" + isGlobalUnicastFunc = "isGlobalUnicast" + isIPFunc = "isIP" + isLinkLocalMcastFunc = "isLinkLocalMulticast" + isLinkLocalUcastFunc = "isLinkLocalUnicast" + isLoopbackFunc = "isLoopback" + isMaskFunc = "isMask" + isUnspecifiedFunc = "isUnspecified" + maskedFunc = "masked" + prefixLengthFunc = "prefixLength" +) + +var ( + // Definitions for the Opaque Types + + // IPType represents a network IP address. + IPType = types.NewOpaqueType("net.IP") + + // CIDRType represents a CIDR-format network range. + CIDRType = types.NewOpaqueType("net.CIDR") +) + +type networkLib struct { + version uint32 +} + +func (*networkLib) LibraryName() string { + return "cel.lib.ext.network" +} + +func (*networkLib) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + cel.Types( + IPType, + CIDRType, + ), + + cel.Function(cidrFunc, + // K8s Parity: Following the pattern, this is "string_to_cidr" + cel.Overload("string_to_cidr", []*cel.Type{cel.StringType}, CIDRType, + cel.UnaryBinding(netCIDRString)), + ), + cel.Function(cidrToString, + cel.Overload("cidr_to_string", []*cel.Type{CIDRType}, cel.StringType, + cel.UnaryBinding(netCIDRToString)), + ), + cel.Function(containsCIDRFunc, + cel.MemberOverload("cidr_contains_cidr", []*cel.Type{CIDRType, CIDRType}, cel.BoolType, + cel.BinaryBinding(netCIDRContainsCIDR)), + cel.MemberOverload("cidr_contains_cidr_string", []*cel.Type{CIDRType, cel.StringType}, cel.BoolType, + cel.BinaryBinding(netCIDRContainsCIDRString)), + ), + cel.Function(containsIPFunc, + cel.MemberOverload("cidr_contains_ip_ip", []*cel.Type{CIDRType, IPType}, cel.BoolType, + cel.BinaryBinding(netCIDRContainsIP)), + cel.MemberOverload("cidr_contains_ip_string", []*cel.Type{CIDRType, cel.StringType}, cel.BoolType, + cel.BinaryBinding(netCIDRContainsIPString)), + ), + cel.Function(familyFunc, + cel.MemberOverload("ip_family", []*cel.Type{IPType}, cel.IntType, + cel.UnaryBinding(netIPFamily)), + ), + cel.Function(ipFunc, + // K8s Parity: The global overload is named "string_to_ip" + cel.Overload("string_to_ip", []*cel.Type{cel.StringType}, IPType, + cel.UnaryBinding(netIPString)), + // K8s Parity: The member overload is named "cidr_ip" + cel.MemberOverload("cidr_ip", []*cel.Type{CIDRType}, IPType, + cel.UnaryBinding(netCIDRIP)), + ), + cel.Function(ipToString, + cel.Overload("ip_to_string", []*cel.Type{IPType}, cel.StringType, + cel.UnaryBinding(netIPToString)), + ), + cel.Function(isCanonicalFunc, + cel.Overload("ip_is_canonical", []*cel.Type{cel.StringType}, cel.BoolType, + cel.UnaryBinding(netIPIsCanonical)), + ), + cel.Function(isCIDRFunc, + cel.Overload("is_cidr", []*cel.Type{cel.StringType}, cel.BoolType, + cel.UnaryBinding(netIsCIDR)), + ), + cel.Function(isGlobalUnicastFunc, + cel.MemberOverload("ip_is_global_unicast", []*cel.Type{IPType}, cel.BoolType, + cel.UnaryBinding(netIPIsGlobalUnicast)), + ), + cel.Function(isIPFunc, + cel.Overload("is_ip", []*cel.Type{cel.StringType}, cel.BoolType, + cel.UnaryBinding(netIsIP)), + ), + cel.Function(isLinkLocalMcastFunc, + cel.MemberOverload("ip_is_link_local_multicast", []*cel.Type{IPType}, cel.BoolType, + cel.UnaryBinding(netIPIsLinkLocalMulticast)), + ), + cel.Function(isLinkLocalUcastFunc, + cel.MemberOverload("ip_is_link_local_unicast", []*cel.Type{IPType}, cel.BoolType, + cel.UnaryBinding(netIPIsLinkLocalUnicast)), + ), + cel.Function(isLoopbackFunc, + cel.MemberOverload("ip_is_loopback", []*cel.Type{IPType}, cel.BoolType, + cel.UnaryBinding(netIPIsLoopback)), + ), + cel.Function(isMaskFunc, + cel.MemberOverload("cidr_is_mask", []*cel.Type{CIDRType}, cel.BoolType, + cel.UnaryBinding(netCIDRIsMask)), + ), + cel.Function(isUnspecifiedFunc, + cel.MemberOverload("ip_is_unspecified", []*cel.Type{IPType}, cel.BoolType, + cel.UnaryBinding(netIPIsUnspecified)), + ), + cel.Function(maskedFunc, + cel.MemberOverload("cidr_masked", []*cel.Type{CIDRType}, CIDRType, + cel.UnaryBinding(netCIDRMasked)), + ), + cel.Function(prefixLengthFunc, + cel.MemberOverload("cidr_prefix_length", []*cel.Type{CIDRType}, cel.IntType, + cel.UnaryBinding(netCIDRPrefixLength)), + ), + cel.ASTValidators( + networkFormatValidator{funcName: ipFunc, argNum: 0, check: checkIP}, + networkFormatValidator{funcName: cidrFunc, argNum: 0, check: checkCIDR}, + ), + cel.CostEstimatorOptions( + checker.OverloadCostEstimate("string_to_cidr", estimateNetworkParseCost), + checker.OverloadCostEstimate("cidr_to_string", estimateNetworkNominalStringCost), + checker.OverloadCostEstimate("cidr_contains_cidr", estimateNetworkContainsCIDRCIDRCost), + checker.OverloadCostEstimate("cidr_contains_cidr_string", estimateNetworkContainsCIDRStringCost), + checker.OverloadCostEstimate("cidr_contains_ip_ip", estimateNetworkContainsIPIPCost), + checker.OverloadCostEstimate("cidr_contains_ip_string", estimateNetworkContainsIPStringCost), + checker.OverloadCostEstimate("ip_family", estimateNetworkNominalCost), + checker.OverloadCostEstimate("string_to_ip", estimateNetworkParseCost), + checker.OverloadCostEstimate("cidr_ip", estimateNetworkNominalOpaqueCost), + checker.OverloadCostEstimate("ip_to_string", estimateNetworkNominalStringCost), + checker.OverloadCostEstimate("ip_is_canonical", estimateIPIsCanonicalCost), + checker.OverloadCostEstimate("is_cidr", estimateNetworkParseBoolCost), + checker.OverloadCostEstimate("ip_is_global_unicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("is_ip", estimateNetworkParseBoolCost), + checker.OverloadCostEstimate("ip_is_link_local_multicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_link_local_unicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_loopback", estimateNetworkNominalCost), + checker.OverloadCostEstimate("cidr_is_mask", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_unspecified", estimateNetworkNominalCost), + checker.OverloadCostEstimate("cidr_masked", estimateNetworkNominalOpaqueCost), + checker.OverloadCostEstimate("cidr_prefix_length", estimateNetworkNominalCost), + ), + } +} + +func (*networkLib) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{ + cel.CostTrackerOptions( + interpreter.OverloadCostTracker("string_to_cidr", trackNetworkParseCost), + interpreter.OverloadCostTracker("cidr_to_string", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_contains_cidr", trackNetworkContainsCIDRCIDRCost), + interpreter.OverloadCostTracker("cidr_contains_cidr_string", trackNetworkContainsCIDRStringCost), + interpreter.OverloadCostTracker("cidr_contains_ip_ip", trackNetworkContainsIPIPCost), + interpreter.OverloadCostTracker("cidr_contains_ip_string", trackNetworkContainsIPStringCost), + interpreter.OverloadCostTracker("ip_family", trackNetworkNominalCost), + interpreter.OverloadCostTracker("string_to_ip", trackNetworkParseCost), + interpreter.OverloadCostTracker("cidr_ip", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_to_string", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_canonical", trackIPIsCanonicalCost), + interpreter.OverloadCostTracker("is_cidr", trackNetworkParseCost), + interpreter.OverloadCostTracker("ip_is_global_unicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("is_ip", trackNetworkParseCost), + interpreter.OverloadCostTracker("ip_is_link_local_multicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_link_local_unicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_loopback", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_is_mask", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_unspecified", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_masked", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_prefix_length", trackNetworkNominalCost), + ), + } +} + +// networkAdapter adapts netip types while preserving existing adapters. +type networkAdapter struct { + types.Adapter +} + +func (a *networkAdapter) NativeToValue(value any) ref.Val { + switch v := value.(type) { + case netip.Addr: + return IP{Addr: v} + case netip.Prefix: + return CIDR{Prefix: v} + } + // Delegate to the wrapped adapter (e.g., Protobuf adapter) + return a.Adapter.NativeToValue(value) +} + +// --- Implementation Logic --- + +func netCIDRContainsCIDR(lhs, rhs ref.Val) ref.Val { + parent := lhs.(CIDR) + child := rhs.(CIDR) + return types.Bool(parent.Prefix.Overlaps(child.Prefix) && parent.Prefix.Bits() <= child.Prefix.Bits()) +} + +func netCIDRContainsCIDRString(lhs, rhs ref.Val) ref.Val { + parent := lhs.(CIDR) + s := rhs.(types.String) + childPrefix, err := parseCIDR(string(s)) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(parent.Prefix.Overlaps(childPrefix) && parent.Prefix.Bits() <= childPrefix.Bits()) +} + +func netCIDRContainsIP(lhs, rhs ref.Val) ref.Val { + cidr := lhs.(CIDR) + ip := rhs.(IP) + return types.Bool(cidr.Prefix.Contains(ip.Addr)) +} + +func netCIDRContainsIPString(lhs, rhs ref.Val) ref.Val { + cidr := lhs.(CIDR) + s := rhs.(types.String) + addr, err := parseIPAddr(string(s)) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(cidr.Prefix.Contains(addr)) +} + +func netCIDRIP(val ref.Val) ref.Val { + cidr := val.(CIDR) + return IP{Addr: cidr.Prefix.Addr()} +} + +func netCIDRMasked(val ref.Val) ref.Val { + cidr := val.(CIDR) + return CIDR{Prefix: cidr.Prefix.Masked()} +} + +func netCIDRPrefixLength(val ref.Val) ref.Val { + cidr := val.(CIDR) + return types.Int(cidr.Prefix.Bits()) +} + +func netCIDRString(val ref.Val) ref.Val { + s := val.(types.String) + str := string(s) + prefix, err := parseCIDR(str) + if err != nil { + return types.WrapErr(err) + } + return CIDR{Prefix: prefix} +} + +func netCIDRToString(val ref.Val) ref.Val { + cidr := val.(CIDR) + return types.String(cidr.Prefix.String()) +} + +func netIPFamily(val ref.Val) ref.Val { + ip := val.(IP) + if ip.Addr.Is4() { + return types.Int(4) + } + return types.Int(6) +} + +func netIPIsCanonical(val ref.Val) ref.Val { + s := val.(types.String) + str := string(s) + addr, err := parseIPAddr(str) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(addr.String() == str) +} + +func netIPIsGlobalUnicast(val ref.Val) ref.Val { + ip := val.(IP) + return types.Bool(ip.Addr.IsGlobalUnicast()) +} + +func netIPIsLinkLocalMulticast(val ref.Val) ref.Val { + ip := val.(IP) + return types.Bool(ip.Addr.IsLinkLocalMulticast()) +} + +func netIPIsLinkLocalUnicast(val ref.Val) ref.Val { + ip := val.(IP) + return types.Bool(ip.Addr.IsLinkLocalUnicast()) +} + +func netIPIsLoopback(val ref.Val) ref.Val { + ip := val.(IP) + return types.Bool(ip.Addr.IsLoopback()) +} + +func netIPIsUnspecified(val ref.Val) ref.Val { + ip := val.(IP) + return types.Bool(ip.Addr.IsUnspecified()) +} + +func netIPString(val ref.Val) ref.Val { + s := val.(types.String) + str := string(s) + addr, err := parseIPAddr(str) + if err != nil { + return types.WrapErr(err) + } + return IP{Addr: addr} +} + +func netIPToString(val ref.Val) ref.Val { + ip := val.(IP) + return types.String(ip.Addr.String()) +} + +func netIsCIDR(val ref.Val) ref.Val { + s := val.(types.String) + _, err := parseCIDR(string(s)) + return types.Bool(err == nil) +} + +func netIsIP(val ref.Val) ref.Val { + s := val.(types.String) + _, err := parseIPAddr(string(s)) + return types.Bool(err == nil) +} + +func netCIDRIsMask(val ref.Val) ref.Val { + cidr := val.(CIDR) + return types.Bool(cidr.Prefix.Addr() == cidr.Prefix.Masked().Addr()) +} + +func parseCIDR(raw string) (netip.Prefix, error) { + prefix, err := netip.ParsePrefix(raw) + if err != nil { + return netip.Prefix{}, fmt.Errorf("CIDR %q parse error during conversion from string: %v", raw, err) + } + if prefix.Addr().Zone() != "" { + return netip.Prefix{}, fmt.Errorf("CIDR %q with zone value is not allowed", raw) + } + if prefix.Addr().Is4In6() { + return netip.Prefix{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw) + } + return prefix, nil +} + +func parseIPAddr(raw string) (netip.Addr, error) { + addr, err := netip.ParseAddr(raw) + if err != nil { + return netip.Addr{}, fmt.Errorf("IP Address %q parse error during conversion from string: %v", raw, err) + } + if addr.Zone() != "" { + return netip.Addr{}, fmt.Errorf("IP address %q with zone value is not allowed", raw) + } + if addr.Is4In6() { + return netip.Addr{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw) + } + return addr, nil +} + +// IP represents an IP address type. +type IP struct { + netip.Addr +} + +// ConvertToNative converts the IP value to a native Go type. +func (i IP) ConvertToNative(typeDesc reflect.Type) (any, error) { + if typeDesc == reflect.TypeFor[netip.Addr]() { + return i.Addr, nil + } + if typeDesc.Kind() == reflect.String { + return i.Addr.String(), nil + } + return nil, fmt.Errorf("unsupported type conversion to '%v'", typeDesc) +} + +// ConvertToType converts the IP value to a CEL type. +func (i IP) ConvertToType(typeValue ref.Type) ref.Val { + switch typeValue { + case types.StringType: + return types.String(i.Addr.String()) + case IPType: + return i + case types.TypeType: + return IPType + } + return types.NewErr("type conversion error from '%s' to '%s'", IPType, typeValue) +} + +// Equal returns true if this IP is equal to the other ref.Val. +func (i IP) Equal(other ref.Val) ref.Val { + o, ok := other.(IP) + if !ok { + return types.False + } + return types.Bool(i.Addr == o.Addr) +} + +// Type returns the CEL type of the IP. +func (i IP) Type() ref.Type { + return IPType +} + +// Value returns the raw Go value (netip.Addr) of the IP. +func (i IP) Value() any { + return i.Addr +} + +// Size returns the size of the IP address in bytes. +// /Used in the size estimation of the runtime cost. +func (i IP) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(i.Addr.BitLen()) / 8))) +} + +// CIDR represents the CIDR network mask format. +type CIDR struct { + netip.Prefix +} + +// ConvertToNative converts the CIDR value to a native Go type. +func (c CIDR) ConvertToNative(typeDesc reflect.Type) (any, error) { + if typeDesc == reflect.TypeFor[netip.Prefix]() { + return c.Prefix, nil + } + if typeDesc.Kind() == reflect.String { + return c.Prefix.String(), nil + } + return nil, fmt.Errorf("unsupported type conversion to '%v'", typeDesc) +} + +// ConvertToType converts the CIDR value to a CEL type. +func (c CIDR) ConvertToType(typeValue ref.Type) ref.Val { + switch typeValue { + case types.StringType: + return types.String(c.Prefix.String()) + case CIDRType: + return c + case types.TypeType: + return CIDRType + } + return types.NewErr("type conversion error from '%s' to '%s'", CIDRType, typeValue) +} + +// Equal returns true if this CIDR is equal to the other ref.Val. +func (c CIDR) Equal(other ref.Val) ref.Val { + o, ok := other.(CIDR) + if !ok { + return types.False + } + return types.Bool(c.Prefix == o.Prefix) +} + +// Type returns the CEL type of the CIDR. +func (c CIDR) Type() ref.Type { + return CIDRType +} + +// Value returns the raw Go value (netip.Prefix) of the CIDR. +func (c CIDR) Value() any { + return c.Prefix +} + +// Size returns the size of the CIDR prefix address in bytes. +// Used in the size estimation of the runtime cost. +func (c CIDR) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(c.Prefix.Bits()) / 8))) +} + +// --- Static Validators --- + +type argChecker func(e *cel.Env, call, arg ast.Expr) error + +type networkFormatValidator struct { + funcName string + argNum int + check argChecker +} + +func (v networkFormatValidator) Name() string { + return fmt.Sprintf("cel.validator.network.%s", v.funcName) +} + +func (v networkFormatValidator) Validate(e *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) { + root := ast.NavigateAST(a) + funcCalls := ast.MatchDescendants(root, ast.FunctionMatcher(v.funcName)) + for _, call := range funcCalls { + callArgs := call.AsCall().Args() + if len(callArgs) <= v.argNum { + continue + } + litArg := callArgs[v.argNum] + if litArg.Kind() != ast.LiteralKind { + continue + } + if err := v.check(e, call, litArg); err != nil { + iss.ReportErrorAtID(litArg.ID(), "invalid %s argument: %v", v.funcName, err) + } + } +} + +func checkIP(e *cel.Env, call, arg ast.Expr) error { + pattern := arg.AsLiteral().Value().(string) + _, err := parseIPAddr(pattern) + return err +} + +func checkCIDR(e *cel.Env, call, arg ast.Expr) error { + pattern := arg.AsLiteral().Value().(string) + _, err := parseCIDR(pattern) + return err +} + +// Cost estimation functions for network extensions. + +func estimateNetworkParseCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + resultSize := rangedSizeEstimate(4, 16) + return callEstimate(sz.MultiplyByCostFactor(stringCostFactor), &resultSize) +} + +func estimateNetworkParseBoolCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + return callEstimate(sz.MultiplyByCostFactor(stringCostFactor), nil) +} + +func estimateIPIsCanonicalCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + return callEstimate(sz.MultiplyByCostFactor(2*stringCostFactor), nil) +} + +func estimateNetworkNominalCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + return callEstimate(callCostEstimate, nil) +} + +func estimateNetworkNominalOpaqueCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + resultSize := rangedSizeEstimate(4, 16) + return callEstimate(callCostEstimate, &resultSize) +} + +func estimateNetworkNominalStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + resultSize := rangedSizeEstimate(3, 45) + return callEstimate(callCostEstimate, &resultSize) +} + +func estimateNetworkContainsIPIPCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsIPStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + argSz := estimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(stringCostFactor)) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRCIDRCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(stringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(callCostEstimate) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(stringCostFactor)) + argSz := estimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(stringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(callCostEstimate) + return callEstimate(ipCompCost, nil) +} + +// Runtime cost tracking functions for network extensions. + +func trackNetworkParseCost(args []ref.Val, result ref.Val) *uint64 { + cost := uint64(math.Ceil(float64(actualSize(args[0])) * stringCostFactor)) + return &cost +} + +func trackIPIsCanonicalCost(args []ref.Val, result ref.Val) *uint64 { + cost := uint64(math.Ceil(float64(actualSize(args[0])) * 2 * stringCostFactor)) + return &cost +} + +func trackNetworkNominalCost(args []ref.Val, result ref.Val) *uint64 { + return &callCost +} + +func trackNetworkContainsIPIPCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + return &cost +} + +func trackNetworkContainsIPStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + otherSize := actualSize(args[1]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(otherSize)*stringCostFactor))) + return &cost +} + +func trackNetworkContainsCIDRCIDRCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(cidrSize)*stringCostFactor)), 1) + return &cost +} + +func trackNetworkContainsCIDRStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + otherSize := actualSize(args[1]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(cidrSize)*stringCostFactor)), 1) + cost = safeAdd(cost, uint64(math.Ceil(float64(otherSize)*stringCostFactor))) + return &cost +} diff --git a/vendor/github.com/google/cel-go/ext/regex.go b/vendor/github.com/google/cel-go/ext/regex.go index 55fd3885..bd222f17 100644 --- a/vendor/github.com/google/cel-go/ext/regex.go +++ b/vendor/github.com/google/cel-go/ext/regex.go @@ -341,16 +341,16 @@ func estimateExtractCost() checker.FunctionEstimator { targetSize := estimateSize(c, args[0]) // Fixed size estimate of +1 is added for safety from zero size args. // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) + targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) + regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) // The result is a single string. Worst Case: it's the size of the entire target. - resultSize := &checker.SizeEstimate{Min: 0, Max: targetSize.Max} + resultSize := rangedSizeEstimate(0, targetSize.Max) // The total cost is the search cost (target + regex) plus the allocation cost for the result string. - return &checker.CallEstimate{ - CostEstimate: regexCost.Multiply(targetCost).Add(checker.CostEstimate(*resultSize)), - ResultSize: resultSize, - } + return callEstimate( + regexCost.Multiply(targetCost).Add(checker.CostEstimate(resultSize)), + &resultSize, + ) } return nil } @@ -362,18 +362,18 @@ func estimateExtractAllCost() checker.FunctionEstimator { targetSize := estimateSize(c, args[0]) // Fixed size estimate of +1 is added for safety from zero size args. // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) + targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) + regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) // The result is a list of strings. Worst Case: it's contents are the size of the entire target. - resultSize := &checker.SizeEstimate{Min: 0, Max: targetSize.Max} + resultSize := rangedSizeEstimate(0, targetSize.Max) // The cost to allocate the result list is its base cost plus the size of its contents. - allocationSize := resultSize.Add(checker.FixedSizeEstimate(common.ListCreateBaseCost)) + allocationSize := resultSize.Add(fixedSizeEstimate(common.ListCreateBaseCost)) // The total cost is the search cost (target + regex) plus the allocation cost for the result list. - return &checker.CallEstimate{ - CostEstimate: targetCost.Multiply(regexCost).Add(checker.CostEstimate(allocationSize)), - ResultSize: resultSize, - } + return callEstimate( + targetCost.Multiply(regexCost).Add(checker.CostEstimate(allocationSize)), + &resultSize, + ) } return nil } @@ -382,28 +382,28 @@ func estimateExtractAllCost() checker.FunctionEstimator { func estimateReplaceCost() checker.FunctionEstimator { return func(c checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { l := len(args) - if l == 3 || l == 4 { + if target == nil && (l == 3 || l == 4) { targetSize := estimateSize(c, args[0]) replacementSize := estimateSize(c, args[2]) // Fixed size estimate of +1 is added for safety from zero size args. // The target cost is the size of the target string, scaled by a traversal factor. - targetCost := targetSize.Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) + targetCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.StringTraversalCostFactor) // The regex cost is the size of the regex pattern, scaled by a complexity factor. - regexCost := estimateSize(c, args[1]).Add(checker.FixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) + regexCost := estimateSize(c, args[1]).Add(fixedSizeEstimate(1)).MultiplyByCostFactor(common.RegexStringLengthCostFactor) // Estimate the potential size range of the output string. The final size could be smaller // (if the replacement size is 0) or larger than the original. allReplacedSize := targetSize.Max * replacementSize.Max noneReplacedSize := targetSize.Max // The allocation cost for the result is based on the estimated size of the output string. - resultSize := &checker.SizeEstimate{Min: noneReplacedSize, Max: allReplacedSize} + resultSize := rangedSizeEstimate(noneReplacedSize, allReplacedSize) if replacementSize.Max == 0 { - resultSize = &checker.SizeEstimate{Min: allReplacedSize, Max: noneReplacedSize} + resultSize = rangedSizeEstimate(allReplacedSize, noneReplacedSize) } // The final cost is result of search cost (target cost + regex cost) plus the allocation cost for the output string. - return &checker.CallEstimate{ - CostEstimate: targetCost.Multiply(regexCost).Add(checker.CostEstimate(*resultSize)), - ResultSize: resultSize, - } + return callEstimate( + targetCost.Multiply(regexCost).Add(resultSize.AsCost()), + &resultSize, + ) } return nil } @@ -411,8 +411,8 @@ func estimateReplaceCost() checker.FunctionEstimator { func extractCostTracker() interpreter.FunctionTracker { return func(args []ref.Val, result ref.Val) *uint64 { - targetCost := float64(actualSize(args[0])+1) * common.StringTraversalCostFactor - regexCost := float64(actualSize(args[1])+1) * common.RegexStringLengthCostFactor + targetCost := float64(safeAdd(actualSize(args[0]), 1)) * common.StringTraversalCostFactor + regexCost := float64(safeAdd(actualSize(args[1]), 1)) * common.RegexStringLengthCostFactor // Actual search cost calculation = targetCost + regexCost searchCost := targetCost * regexCost // The total cost is the base call cost + search cost + result string allocation. diff --git a/vendor/github.com/google/cel-go/ext/sets.go b/vendor/github.com/google/cel-go/ext/sets.go index ecac4bf9..63c019ad 100644 --- a/vendor/github.com/google/cel-go/ext/sets.go +++ b/vendor/github.com/google/cel-go/ext/sets.go @@ -15,8 +15,6 @@ package ext import ( - "math" - "github.com/google/cel-go/cel" "github.com/google/cel-go/checker" "github.com/google/cel-go/common/ast" @@ -242,37 +240,15 @@ func estimateSetsCost(costFactor float64) checker.FunctionEstimator { arg0Size := estimateSize(estimator, args[0]) arg1Size := estimateSize(estimator, args[1]) costEstimate := arg0Size.Multiply(arg1Size).MultiplyByCostFactor(costFactor).Add(callCostEstimate) - return &checker.CallEstimate{CostEstimate: costEstimate} - } -} - -func estimateSize(estimator checker.CostEstimator, node checker.AstNode) checker.SizeEstimate { - if l := node.ComputedSize(); l != nil { - return *l - } - if l := estimator.EstimateSize(node); l != nil { - return *l + return callEstimate(costEstimate, nil) } - return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} } func trackSetsCost(costFactor float64) interpreter.FunctionTracker { return func(args []ref.Val, _ ref.Val) *uint64 { lhsSize := actualSize(args[0]) rhsSize := actualSize(args[1]) - cost := callCost + uint64(float64(lhsSize*rhsSize)*costFactor) + cost := safeAdd(callCost, uint64(float64(lhsSize*rhsSize)*costFactor)) return &cost } } - -func actualSize(value ref.Val) uint64 { - if sz, ok := value.(traits.Sizer); ok { - return uint64(sz.Size().(types.Int)) - } - return 1 -} - -var ( - callCostEstimate = checker.FixedCostEstimate(1) - callCost = uint64(1) -) diff --git a/vendor/github.com/google/cel-go/ext/strings.go b/vendor/github.com/google/cel-go/ext/strings.go index 66b7806a..1f7732f2 100644 --- a/vendor/github.com/google/cel-go/ext/strings.go +++ b/vendor/github.com/google/cel-go/ext/strings.go @@ -28,9 +28,12 @@ import ( "golang.org/x/text/language" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" + "github.com/google/cel-go/common" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" + "github.com/google/cel-go/interpreter" ) const ( @@ -43,6 +46,8 @@ const ( // // # CharAt // +// Introduced at version: 0 (cost support in version 5) +// // Returns the character at the given position. If the position is negative, or greater than // the length of the string, the function will produce an error: // @@ -56,7 +61,7 @@ const ( // // # Format // -// Introduced at version: 1 +// Introduced at version: 1 (cost at version 5) // // Returns a new string with substitutions being performed, printf-style. // The valid formatting clauses are: @@ -103,6 +108,8 @@ const ( // // # IndexOf // +// Introduced at version: 0 (cost support in version 5) +// // Returns the integer index of the first occurrence of the search string. If the search string is // not found the function returns -1. // @@ -124,6 +131,8 @@ const ( // // # Join // +// Introduced at version: 0 (cost support in version 5) +// // Returns a new string where the elements of string list are concatenated. // // The function also accepts an optional separator which is placed between elements in the resulting string. @@ -140,6 +149,8 @@ const ( // // # LastIndexOf // +// Introduced at version: 0 (cost support in version 5) +// // Returns the integer index at the start of the last occurrence of the search string. If the // search string is not found the function returns -1. // @@ -161,6 +172,8 @@ const ( // // # LowerAscii // +// Introduced at version: 0 (cost support in version 5) +// // Returns a new string where all ASCII characters are lower-cased. // // This function does not perform Unicode case-mapping for characters outside the ASCII range. @@ -174,7 +187,7 @@ const ( // // # Strings.Quote // -// Introduced in version: 1 +// Introduced in version: 1 (cost support in version 5) // // Takes the given string and makes it safe to print (without any formatting due to escape sequences). // If any invalid UTF-8 characters are encountered, they are replaced with \uFFFD. @@ -188,6 +201,8 @@ const ( // // # Replace // +// Introduced at version: 0 (cost support in version 5) +// // Returns a new string based on the target, which replaces the occurrences of a search string // with a replacement string if present. The function accepts an optional limit on the number of // substring replacements to be made. @@ -209,6 +224,8 @@ const ( // // # Split // +// Introduced at version: 0 (cost support in version 5) +// // Returns a list of strings split from the input by the given separator. The function accepts // an optional argument specifying a limit on the number of substrings produced by the split. // @@ -229,6 +246,8 @@ const ( // // # Substring // +// Introduced at version: 0 (cost support in version 5) +// // Returns the substring given a numeric range corresponding to character positions. Optionally // may omit the trailing range for a substring from a given character position until the end of // a string. @@ -249,6 +268,8 @@ const ( // // # Trim // +// Introduced at version: 0 (cost support in version 5) +// // Returns a new string which removes the leading and trailing whitespace in the target string. // The trim function uses the Unicode definition of whitespace which does not include the // zero-width spaces. See: https://en.wikipedia.org/wiki/Whitespace_character#Unicode @@ -261,6 +282,8 @@ const ( // // # UpperAscii // +// Introduced at version: 0 (cost support in version 5) +// // Returns a new string where all ASCII characters are upper-cased. // // This function does not perform Unicode case-mapping for characters outside the ASCII range. @@ -274,7 +297,7 @@ const ( // // # Reverse // -// Introduced at version: 3 +// Introduced at version: 3 (cost support in version 5) // // Returns a new string whose characters are the same as the target string, only formatted in // reverse order. @@ -287,7 +310,7 @@ const ( // 'gums'.reverse() // returns 'smug' // 'John Smith'.reverse() // returns 'htimS nhoJ' // -// Introduced at version: 4 +// Introduced at version: 4 (cost support in version 5) // // Formatting updated to adhere to https://github.com/google/cel-spec/blob/master/doc/extensions/strings.md. // @@ -567,11 +590,59 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { opts = append(opts, cel.ASTValidators(stringFormatValidator{maxPrecision: maxPrecision})) } } + + if lib.version >= 5 { + // Cost estimators for string extension functions. + estimators := []checker.CostOption{ + // Format is captured in the core cost estimator logic and needs to be extracted out. + checker.OverloadCostEstimate("string_char_at_int", estimateStringCharAtCost), + checker.OverloadCostEstimate("string_index_of_string", estimateStringSearchCost), + checker.OverloadCostEstimate("string_index_of_string_int", estimateStringSearchCost), + checker.OverloadCostEstimate("string_last_index_of_string", estimateStringSearchCost), + checker.OverloadCostEstimate("string_last_index_of_string_int", estimateStringSearchCost), + checker.OverloadCostEstimate("string_lower_ascii", estimateStringFixedTransformCost), + checker.OverloadCostEstimate("string_upper_ascii", estimateStringFixedTransformCost), + checker.OverloadCostEstimate("string_replace_string_string", estimateStringReplaceCost), + checker.OverloadCostEstimate("string_replace_string_string_int", estimateStringReplaceCost), + checker.OverloadCostEstimate("string_split_string", estimateStringSplitCost), + checker.OverloadCostEstimate("string_split_string_int", estimateStringSplitCost), + checker.OverloadCostEstimate("string_substring_int", estimateSubstringCost), + checker.OverloadCostEstimate("string_substring_int_int", estimateSubstringCost), + checker.OverloadCostEstimate("string_trim", estimateStringVariableTransformCost), + checker.OverloadCostEstimate("string_reverse", estimateStringFixedTransformCost), + checker.OverloadCostEstimate("list_join", estimateStringJoinCost), + checker.OverloadCostEstimate("list_join_string", estimateStringJoinCost), + } + opts = append(opts, cel.CostEstimatorOptions(estimators...)) + } return opts } // ProgramOptions implements the Library interface method. -func (*stringLib) ProgramOptions() []cel.ProgramOption { +func (lib *stringLib) ProgramOptions() []cel.ProgramOption { + if lib.version >= 5 { + return []cel.ProgramOption{ + cel.CostTrackerOptions( + interpreter.OverloadCostTracker("string_char_at_int", trackStringCharAtCost), + interpreter.OverloadCostTracker("string_index_of_string", trackStringSearchCost), + interpreter.OverloadCostTracker("string_index_of_string_int", trackStringSearchCost), + interpreter.OverloadCostTracker("string_last_index_of_string", trackStringSearchCost), + interpreter.OverloadCostTracker("string_last_index_of_string_int", trackStringSearchCost), + interpreter.OverloadCostTracker("string_lower_ascii", trackStringTransformCost), + interpreter.OverloadCostTracker("string_upper_ascii", trackStringTransformCost), + interpreter.OverloadCostTracker("string_replace_string_string", trackStringReplaceCost), + interpreter.OverloadCostTracker("string_replace_string_string_int", trackStringReplaceCost), + interpreter.OverloadCostTracker("string_split_string", trackStringSplitCost), + interpreter.OverloadCostTracker("string_split_string_int", trackStringSplitCost), + interpreter.OverloadCostTracker("string_substring_int", trackStringTransformCost), + interpreter.OverloadCostTracker("string_substring_int_int", trackStringTransformCost), + interpreter.OverloadCostTracker("string_trim", trackStringTransformCost), + interpreter.OverloadCostTracker("string_reverse", trackStringTransformCost), + interpreter.OverloadCostTracker("list_join", trackStringJoinCost), + interpreter.OverloadCostTracker("list_join_string", trackStringJoinCost), + ), + } + } return []cel.ProgramOption{} } @@ -592,15 +663,19 @@ func indexOf(str, substr string) (int64, error) { } func indexOfOffset(str, substr string, offset int64) (int64, error) { - if substr == "" { - return offset, nil - } off := int(offset) - runes := []rune(str) - subrunes := []rune(substr) if off < 0 { return -1, fmt.Errorf("index out of range: %d", off) } + runes := []rune(str) + if substr == "" { + // The empty string matches at the search offset, clamped to the end of the string. + if off > len(runes) { + return int64(len(runes)), nil + } + return offset, nil + } + subrunes := []rune(substr) // If the offset exceeds the length, return -1 rather than error. if off >= len(runes) { return -1, nil @@ -633,15 +708,19 @@ func lastIndexOf(str, substr string) (int64, error) { } func lastIndexOfOffset(str, substr string, offset int64) (int64, error) { - if substr == "" { - return offset, nil - } off := int(offset) - runes := []rune(str) - subrunes := []rune(substr) if off < 0 { return -1, fmt.Errorf("index out of range: %d", off) } + runes := []rune(str) + if substr == "" { + // The empty string matches at the search offset, clamped to the end of the string. + if off > len(runes) { + return int64(len(runes)), nil + } + return offset, nil + } + subrunes := []rune(substr) // If the offset is far greater than the length return -1 if off >= len(runes) { return -1, nil @@ -810,5 +889,196 @@ func sanitize(s string) string { } var ( - stringListType = reflect.TypeOf([]string{}) + stringListType = reflect.TypeFor[[]string]() ) + +// Cost estimation functions for string extensions. +// +// These functions provide compile-time cost estimates proportional to the size of +// the input string(s), ensuring that the CEL cost system accurately reflects the +// computational work performed by string operations. + +// estimateStringFixedTransformCost estimates cost for O(n) string operations such as +// lowerAscii, upperAsciil, reverse and quote. +func estimateStringFixedTransformCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil { + return nil + } + cost, size := estimateStringScan(estimateSize(estimator, *target)) + return callEstimate(cost.Add(callCostEstimate).Add(size.AsCost()), size) +} + +// estimateStringVariableTransformCost estimates cost for O(n) string operations that result +// in a variable sized string which may be empty to the exact input string. +func estimateStringVariableTransformCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil { + return nil + } + cost, size := estimateStringScan(estimateSize(estimator, *target)) + transformSize := rangedSizeEstimate(0, size.Max) + return callEstimate(cost.Add(callCostEstimate).Add(transformSize.AsCost()), &transformSize) +} + +// estimateStringCharAtCost includes a cost of 1 for the allocation, plus the string traversal cost. +func estimateStringCharAtCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 1 { + return nil + } + cost, _ := estimateStringScan(estimateSize(estimator, *target)) + resultSize := rangedSizeEstimate(0, 1) + return callEstimate(cost.Add(callCostEstimate).Add(callCostEstimate), &resultSize) +} + +// estimateSubstringCost estimates the cost for an O(n) traversal and allocation. +func estimateSubstringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) < 1 || len(args) > 2 { + return nil + } + targetSize := estimateSize(estimator, *target) + cost, _ := estimateStringScan(targetSize) + + start := nodeAsUintValue(args[0], 0) + end := targetSize.Max + if len(args) == 2 { + end = nodeAsUintValue(args[1], end) + } + resultSize := fixedSizeEstimate(end - start) + return callEstimate(cost.Add(callCostEstimate).Add(resultSize.AsCost()), &resultSize) +} + +// estimateStringSearchCost estimates cost for O(n*m) string search operations +// such as indexOf and lastIndexOf. +func estimateStringSearchCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) < 1 { + return nil + } + targetSize := estimateSize(estimator, *target) + needleSize := estimateSize(estimator, args[0]) + searchSize := targetSize.Multiply(needleSize) + searchCost, _ := estimateStringScan(searchSize) + // Search cost is proportional to target size * substring size. + return callEstimate(searchCost.Add(callCostEstimate), nil) +} + +// estimateStringReplaceCost estimates cost for string replace operations. +// The cost accounts for search (O(n*m)) and potential output size growth. +func estimateStringReplaceCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) < 2 { + return nil + } + // Compute the search for the replacement string, by 'm' times + targetSize := estimateSize(estimator, *target) + needleSize := atLeastOne(estimateSize(estimator, args[0])) + searchCost := atLeastOne(targetSize).Multiply(needleSize).MultiplyByCostFactor(stringCostFactor) + + replacementSize := estimateSize(estimator, args[1]).Add(fixedSizeEstimate(1)) + allReplacedSize := safeMul(safeAdd(targetSize.Max, 1), replacementSize.Max) + resultMinSize := targetSize.Min + if resultMinSize > replacementSize.Min { + resultMinSize = replacementSize.Min + } + resultSize := rangedSizeEstimate(resultMinSize, allReplacedSize) + return callEstimate( + searchCost.Add(resultSize.AsCost()).Add(callCostEstimate), &resultSize, + ) +} + +// estimateStringSplitCost estimates cost for string split operations. +// Split creates a list of substrings, so cost includes both traversal and +// list allocation proportional to the input size. +func estimateStringSplitCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) < 1 { + return nil + } + targetSize := estimateSize(estimator, *target) + // Traversal cost proportional to input size. + traversalCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(stringCostFactor) + // Worst case: split("") produces N elements for a string of size N. + resultSize := rangedSizeEstimate(0, targetSize.Max) + // Include list creation base cost plus allocation for each element. + allocationCost := resultSize.MultiplyByCostFactor(1).Add(checker.FixedCostEstimate(common.ListCreateBaseCost)) + cost := traversalCost.Add(allocationCost).Add(callCostEstimate) + return callEstimate(cost, &resultSize) +} + +// estimateStringJoinCost estimates cost for string join operations. +// Join iterates over all list elements and concatenates them, so cost is +// proportional to the total size of all elements plus separator overhead. +func estimateStringJoinCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil { + return nil + } + targetSize := estimateSize(estimator, *target) + sepSize := fixedSizeEstimate(0) + if len(args) >= 1 { + sepSize = estimateSize(estimator, args[0]) + } + // Traversal cost proportional to the number of list elements. + traversalCost := targetSize.Add(fixedSizeEstimate(1)).MultiplyByCostFactor(stringCostFactor) + // Result size: sum of element sizes + (n-1) * separator size. + // Worst case estimate: use list size * max element size + list size * separator size. + maxResultSize := safeAdd(safeMul(targetSize.Max, (safeAdd(1, sepSize.Max))), sepSize.Max) + resultSize := rangedSizeEstimate(0, maxResultSize) + cost := traversalCost.Add(resultSize.MultiplyByCostFactor(1)).Add(callCostEstimate) + return callEstimate(cost, &resultSize) +} + +// Runtime cost tracking functions for string extensions. +// +// These functions compute the actual cost of string operations after evaluation, +// using the real sizes of the inputs and outputs. + +// trackStringCharAtCost tracks runtime cost for O(n) string operations. +func trackStringCharAtCost(args []ref.Val, result ref.Val) *uint64 { + size := float64(actualSize(args[0])) * stringCostFactor + cost := safeAdd(callCost, uint64(math.Ceil(size)), 1) + return &cost +} + +// trackStringTransformCost tracks runtime cost for O(n) string operations. +func trackStringTransformCost(args []ref.Val, result ref.Val) *uint64 { + transformCost := math.Ceil(float64(actualSize(args[0])) * stringCostFactor) + resultSize := actualSize(result) + cost := safeAdd(callCost, uint64(transformCost), resultSize) + return &cost +} + +// trackStringSearchCost tracks runtime cost for O(n*m) string search operations. +func trackStringSearchCost(args []ref.Val, _ ref.Val) *uint64 { + searchCost := float64(actualSize(args[0])*actualSize(args[1])) * stringCostFactor + cost := safeAdd(uint64(math.Ceil(searchCost)), callCost) + return &cost +} + +// trackStringReplaceCost tracks runtime cost for string replace operations, +// accounting for search cost and the size of the result. +func trackStringReplaceCost(args []ref.Val, result ref.Val) *uint64 { + targetSize := actualSize(args[0]) + if targetSize == 0 { + targetSize = 1 + } + needleSize := actualSize(args[1]) + if needleSize == 0 { + needleSize = 1 + } + searchCost := uint64(math.Ceil(float64(targetSize*needleSize) * stringCostFactor)) + cost := safeAdd(callCost, searchCost, actualSize(result)) + return &cost +} + +// trackStringSplitCost tracks runtime cost for string split operations, +// accounting for traversal and list allocation. +func trackStringSplitCost(args []ref.Val, result ref.Val) *uint64 { + traversalCost := float64(safeAdd(actualSize(args[0]), 1)) * stringCostFactor + resultSize := actualSize(result) + cost := safeAdd(callCost, uint64(math.Ceil(traversalCost)), resultSize, common.ListCreateBaseCost) + return &cost +} + +// trackStringJoinCost tracks runtime cost for string join operations, +// accounting for traversal and the size of the result. +func trackStringJoinCost(args []ref.Val, result ref.Val) *uint64 { + traversalCost := float64(safeAdd(actualSize(args[0]), 1)) * stringCostFactor + cost := safeAdd(callCost, uint64(math.Ceil(traversalCost)), actualSize(result)) + return &cost +} diff --git a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel b/vendor/github.com/google/cel-go/interpreter/BUILD.bazel index 220e23d4..40ac2ba6 100644 --- a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel +++ b/vendor/github.com/google/cel-go/interpreter/BUILD.bazel @@ -9,11 +9,13 @@ go_library( name = "go_default_library", srcs = [ "activation.go", + "async.go", "attribute_patterns.go", "attributes.go", "decorators.go", "dispatcher.go", "evalstate.go", + "frame.go", "interpretable.go", "interpreter.go", "optimizations.go", @@ -45,8 +47,10 @@ go_test( name = "go_default_test", srcs = [ "activation_test.go", + "async_test.go", "attribute_patterns_test.go", "attributes_test.go", + "frame_test.go", "interpreter_test.go", "prune_test.go", "runtimecost_test.go", @@ -63,6 +67,7 @@ go_test( "//common/operators:go_default_library", "//common/stdlib:go_default_library", "//common/types:go_default_library", + "//common/types/ref:go_default_library", "//parser:go_default_library", "//test:go_default_library", "//test/proto2pb:go_default_library", diff --git a/vendor/github.com/google/cel-go/interpreter/activation.go b/vendor/github.com/google/cel-go/interpreter/activation.go index dd40619e..bc9296ed 100644 --- a/vendor/github.com/google/cel-go/interpreter/activation.go +++ b/vendor/github.com/google/cel-go/interpreter/activation.go @@ -110,8 +110,9 @@ func (a *mapActivation) ResolveName(name string) (any, bool) { // hierarchicalActivation which implements Activation and contains a parent and // child activation. type hierarchicalActivation struct { - parent Activation - child Activation + parent Activation + child Activation + poolAllocated bool } // Parent implements the Activation interface method. @@ -127,10 +128,41 @@ func (a *hierarchicalActivation) ResolveName(name string) (any, bool) { return a.parent.ResolveName(name) } +// Unwrap returns the parent activation, stripping the local child scope. +// This allows global disambiguation to skip past locally introduced variables. +func (a *hierarchicalActivation) Unwrap() Activation { + return a.parent +} + +// IsLocalVariable reports whether the variable name is locally bound in the hierarchical activation. +func (a *hierarchicalActivation) IsLocalVariable(name string) bool { + if holder, ok := a.child.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + if holder, ok := a.parent.(localVariableHolder); ok { + return holder.IsLocalVariable(name) + } + return false +} + +// AsPartialActivation checks the child first via direct type assertion (to +// avoid recursion through the folder → frame → hierarchicalActivation cycle), +// then walks the parent hierarchy via the free function. +func (a *hierarchicalActivation) AsPartialActivation() (PartialActivation, bool) { + if pv, ok := a.child.(partialActivationConverter); ok { + if p, ok := pv.AsPartialActivation(); ok { + return p, true + } + } + return AsPartialActivation(a.parent) +} + // NewHierarchicalActivation takes two activations and produces a new one which prioritizes // resolution in the child first and parent(s) second. func NewHierarchicalActivation(parent Activation, child Activation) Activation { - return &hierarchicalActivation{parent, child} + return &hierarchicalActivation{parent: parent, child: child, poolAllocated: false} } // NewPartialActivation returns an Activation which contains a list of AttributePattern values diff --git a/vendor/github.com/google/cel-go/interpreter/async.go b/vendor/github.com/google/cel-go/interpreter/async.go new file mode 100644 index 00000000..4e391196 --- /dev/null +++ b/vendor/github.com/google/cel-go/interpreter/async.go @@ -0,0 +1,530 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interpreter + +import ( + "context" + "encoding/binary" + "fmt" + "hash/fnv" + "math" + "sync" + "sync/atomic" + + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// Async extension function support. +// +// CEL supports `types.Unknown` as a first-class value, and concurrent (async) function execution +// in CEL invokes a stub function which checks for the presence of an existing result which matches +// the function call and call arguments, or which records the 'unexecuted' function and call arguments +// for concurrent execution in a later phase if the result is `types.Unknown` and indicates the +// expression ids of the functions necessary to advance the execution. +// +// This call pattern is repeated iteratively until there are either no more functions to call or no +// progress is made toward resolving the unknowns. + +// AsyncObserver provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: OnCallStarted is invoked from the evaluator +// goroutine when a call is launched, while OnCallFinished is invoked from the call's own goroutine +// when it completes. The two callbacks therefore run on different goroutines, and OnCallFinished +// callbacks for distinct calls may run concurrently with each other. +type AsyncObserver interface { + // OnCallStarted is called when an asynchronous function is first launched. + OnCallStarted(callID int64, function, overload string, args []ref.Val) + // OnCallFinished is called when an asynchronous function completes. + OnCallFinished(callID int64, function, overload string, res ref.Val) +} + +// AsyncCall describes a pending or completed asynchronous function call. +type AsyncCall interface { + // CallID returns the unique identifier for this async call invocation. + CallID() int64 + // Function returns the name of the function being called. + Function() string + // Overload returns the specific overload ID being invoked. + Overload() string +} + +// evalAsyncFunc is the planned Interpretable for an asynchronous function call. +type evalAsyncFunc struct { + id int64 + function string + overload string + args []InterpretableV2 + impl functions.AsyncOp +} + +// ID implements the Interpretable interface method. +func (fn *evalAsyncFunc) ID() int64 { + return fn.id +} + +// Function returns the name of the function being invoked. +func (fn *evalAsyncFunc) Function() string { + return fn.function +} + +// OverloadID returns the overload id of the function being invoked. +func (fn *evalAsyncFunc) OverloadID() string { + return fn.overload +} + +// Args returns the argument Interpretables for the function call. +func (fn *evalAsyncFunc) Args() []InterpretableV2 { + return fn.args +} + +// Eval implements the Interpretable interface method. +func (fn *evalAsyncFunc) Eval(vars Activation) ref.Val { + return fn.Exec(AsFrame(vars)) +} + +// Exec implements the InterpretableV2 interface method. +func (fn *evalAsyncFunc) Exec(frame *ExecutionFrame) ref.Val { + argVals := make([]ref.Val, len(fn.args)) + var unk *types.Unknown + for i, arg := range fn.args { + argVals[i] = arg.Exec(frame) + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) + } + if unk != nil { + return unk + } + result := frame.ComputeResult(fn.ID(), fn.Function(), fn.OverloadID(), fn.impl, argVals) + return types.LabelErrNode(fn.id, result) +} + +// asyncCallStateTracker manages async call states across re-evaluations of a single program. +type asyncCallStateTracker struct { + mu sync.RWMutex + // calls buckets call states by a composite hash of (node id, overload, string/int/double/uint/bool args). + // A single AST node id may host many concurrently-live calls when it is evaluated inside a + // comprehension (once per element with different arguments), so each bucket may hold more + // than one state. The exact match within a bucket is resolved via asyncCallState.matches, + // which applies CEL's full equality semantics to the arguments. + calls map[uint64][]*asyncCallState + callsByID map[int64]*asyncCallState + nextCallID atomic.Int64 +} + +func newAsyncCallStateTracker() *asyncCallStateTracker { + return &asyncCallStateTracker{ + calls: make(map[uint64][]*asyncCallState), + callsByID: make(map[int64]*asyncCallState), + } +} + +var ( + hashZeroMarker = []byte{0} + hashStringMarker = []byte{'s'} + hashBoolTrueMarker = []byte{'b', 1} + hashBoolFalseMarker = []byte{'b', 0} + hashNumberMarker = []byte{'n'} + hashDefaultMarker = []byte{'x'} +) + +// hashCall computes the composite bucket key for an async call. +// +// Only string, int, double, uint, and bool argument values contribute to the hash. More complex types +// rely on a richer notion of equivalence (e.g. unordered maps, proto equality, custom types) +// that a byte-level hash cannot capture safely, so they are intentionally excluded from the key +// and are instead disambiguated within the bucket by asyncCallState.matches. +func hashCall(id int64, overload string, args []ref.Val) uint64 { + h := fnv.New64a() + var idBuf [8]byte + binary.LittleEndian.PutUint64(idBuf[:], uint64(id)) + h.Write(idBuf[:]) + h.Write([]byte(overload)) + h.Write(hashZeroMarker) + for _, arg := range args { + switch v := arg.(type) { + case types.String: + h.Write(hashStringMarker) + h.Write([]byte(string(v))) + case types.Bool: + if bool(v) { + h.Write(hashBoolTrueMarker) + } else { + h.Write(hashBoolFalseMarker) + } + case types.Int: + h.Write(hashNumberMarker) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + case types.Uint: + h.Write(hashNumberMarker) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + case types.Double: + h.Write(hashNumberMarker) + if math.IsNaN(float64(v)) { + h.Write([]byte("NaN")) + h.Write(hashZeroMarker) + continue + } + // Normalize -0.0 to 0.0. Go will treat -0.0 as 0.0 at compile time, + // but the function math.Copysign(0.0, -1.0) can be used to test the -0.0 case. + if v == types.Double(0.0) && math.Signbit(float64(v)) { + v = types.Double(0.0) + } + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + default: + // Value intentionally omitted; bucket membership falls back to matches. + h.Write(hashDefaultMarker) + } + // Separator to avoid cross-argument collisions, e.g. ("a", "bc") vs ("ab", "c"). + h.Write(hashZeroMarker) + } + return h.Sum64() +} + +// findInBucket returns the call state in the bucket matching the same node id and call identity, +// or nil if no match is present. +func findInBucket(bucket []*asyncCallState, id int64, function, overload string, args []ref.Val) *asyncCallState { + for _, acs := range bucket { + if acs.matches(id, function, overload, args) { + return acs + } + } + return nil +} + +// getOrCreate returns the existing call state for the (node id, args) tuple, or registers and +// returns a new one. A newly registered call is assigned a unique callID and counted as pending. +func (t *asyncCallStateTracker) getOrCreate(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp, gate *asyncGate) *asyncCallState { + key := hashCall(id, overload, argVals) + + t.mu.RLock() + acs := findInBucket(t.calls[key], id, function, overload, argVals) + t.mu.RUnlock() + if acs != nil { + return acs + } + + t.mu.Lock() + defer t.mu.Unlock() + // Check again in case it was created while waiting for the lock. + if acs := findInBucket(t.calls[key], id, function, overload, argVals); acs != nil { + return acs + } + + // Assign a new unique call ID for this async call. + acs = newAsyncCallState(id, function, overload, argVals, impl) + callID := t.nextCallID.Add(1) + acs.callID = callID + acs.gate = gate + t.calls[key] = append(t.calls[key], acs) + t.callsByID[callID] = acs + return acs +} + +func (t *asyncCallStateTracker) getByID(callID int64) *asyncCallState { + t.mu.RLock() + defer t.mu.RUnlock() + return t.callsByID[callID] +} + +func newAsyncCallState(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp) *asyncCallState { + return &asyncCallState{ + id: id, + function: function, + overload: overload, + argVals: argVals, + impl: impl, + } +} + +// asyncCallState tracks the result of a single async function call across multiple re-evaluations. +type asyncCallState struct { + id int64 // AST expression node ID where the call is defined. + callID int64 // Unique incremental tracking ID assigned to this call. + function string + overload string + argVals []ref.Val + impl functions.AsyncOp + + mu sync.RWMutex + started bool + result ref.Val + + gate *asyncGate +} + +// CallID returns the unique identifier for this async call invocation. +func (acs *asyncCallState) CallID() int64 { + return acs.callID +} + +// Function returns the name of the function being called. +func (acs *asyncCallState) Function() string { + return acs.function +} + +// Overload returns the specific overload ID being invoked. +func (acs *asyncCallState) Overload() string { + return acs.overload +} + +// ResultOrUnknown returns the cached result if the call has completed, an Unknown +// with the call ID if pending, or nil if the call has not been started. +func (acs *asyncCallState) ResultOrUnknown() ref.Val { + if acs == nil { + return nil + } + acs.mu.RLock() + defer acs.mu.RUnlock() + if acs.result == nil && acs.started { + return types.NewUnknown(acs.callID, nil) + } + return acs.result +} + +// SetResult sets the completed result for an asynchronous function call. +func (acs *asyncCallState) SetResult(res ref.Val) { + if acs == nil { + return + } + acs.mu.Lock() + defer acs.mu.Unlock() + acs.result = res +} + +// launch returns a call's cached result, or starts the call (subject to the launch limiter) and +// returns an Unknown referencing its callID while the result is pending. +// +// Admission control: when a concurrency semaphore is configured, a launch slot is reserved with a +// non-blocking send. If no slot is free the call is left unstarted and an Unknown is returned; the +// call is retried on a later re-evaluation pass once an in-flight call completes and frees a slot. +// The reservation is non-blocking on purpose — the evaluator runs on a single goroutine, and +// blocking it here while completing calls block on an undrained completion channel would deadlock. +// The slot is held by the launched goroutine and released when it exits, so the number of live +// async goroutines is bounded by the semaphore capacity. +func (t *asyncCallStateTracker) launch(ctx context.Context, acs *asyncCallState, observer AsyncObserver) ref.Val { + if res := acs.ResultOrUnknown(); res != nil { + return res + } + gate := acs.gate + if !gate.TryAcquire() { + return types.NewUnknown(acs.callID, nil) + } + acs.mu.Lock() + if acs.started || acs.result != nil { + // Defensive: the evaluator is single-threaded so this should not happen, but if it does, + // return the reserved slot rather than leak it. + acs.mu.Unlock() + gate.Release() + return types.NewUnknown(acs.callID, nil) + } + acs.started = true + acs.mu.Unlock() + + if observer != nil { + observer.OnCallStarted(acs.callID, acs.function, acs.overload, acs.argVals) + } + go func() { + defer func() { + if observer != nil { + observer.OnCallFinished(acs.callID, acs.function, acs.overload, acs.ResultOrUnknown()) + } + gate.Complete(ctx, acs.callID) + }() + + ch := acs.impl(ctx, acs.argVals...) + // Early terminate with a CEL error when an implementation returns an empty channel. + if ch == nil { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + // Wait for the async computation to finish or for the context to be cancelled. + select { + case r, ok := <-ch: + if !ok { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + acs.SetResult(r) + case <-ctx.Done(): + // Evaluation context cancelled before the async operation completed. + acs.SetResult(types.WrapErr(context.Cause(ctx))) + } + }() + return types.NewUnknown(acs.callID, nil) +} + +// matches reports whether two call states refer to the same function, overload, and arguments. +func (acs *asyncCallState) matches(id int64, function, overload string, args []ref.Val) bool { + if acs == nil { + return false + } + if acs.id != id || acs.function != function || acs.overload != overload { + return false + } + if len(acs.argVals) != len(args) { + return false + } + for i, v := range acs.argVals { + otherV := args[i] + if types.Equal(v, otherV) == types.True { + continue + } + if n, ok := v.(types.Double); ok { + // Treat NaN as equivalent for the sake of function dispatch equality. + if otherN, ok := otherV.(types.Double); ok && math.IsNaN(float64(n)) && math.IsNaN(float64(otherN)) { + continue + } + } + return false + } + return true +} + +// trackerShrinkThreshold is the entry count above which a released tracker's maps are reallocated +// rather than cleared in place, so the pool does not retain a large backing array indefinitely. +const trackerShrinkThreshold = 1024 + +// asyncCallStateTrackerPool provides a synchronized pool of asyncCallStateTrackers. +type asyncCallTrackerPool struct { + sync.Pool +} + +func (pool *asyncCallTrackerPool) create() *asyncCallStateTracker { + return pool.Get().(*asyncCallStateTracker) +} + +func (pool *asyncCallTrackerPool) release(tracker *asyncCallStateTracker) { + if tracker == nil { + return + } + tracker.mu.Lock() + // Clearing with delete reuses the backing arrays, which is ideal for the common case but pins + // a large allocation in the pool after a wide fan-out (e.g. an async call over a big list). + // Past a threshold, reallocate so the high-water-mark memory is released to the GC instead of + // being retained by the pooled tracker. + if len(tracker.calls) > trackerShrinkThreshold || len(tracker.callsByID) > trackerShrinkThreshold { + tracker.calls = make(map[uint64][]*asyncCallState) + tracker.callsByID = make(map[int64]*asyncCallState) + } else { + for k := range tracker.calls { + delete(tracker.calls, k) + } + for k := range tracker.callsByID { + delete(tracker.callsByID, k) + } + } + tracker.nextCallID.Store(0) + tracker.mu.Unlock() + pool.Pool.Put(tracker) +} + +func newAsyncCallTrackerPool() *asyncCallTrackerPool { + return &asyncCallTrackerPool{ + Pool: sync.Pool{ + New: func() any { + return newAsyncCallStateTracker() + }, + }, + } +} + +var asyncCallStateTrackerPool = newAsyncCallTrackerPool() + +// asyncGate coordinates async call admission control and completion signaling. +type asyncGate struct { + semaphore chan struct{} + completions chan<- int64 + activeCalls atomic.Int32 +} + +func newAsyncGate(maxConcurrency int, completions chan<- int64) *asyncGate { + var sem chan struct{} + if maxConcurrency > 0 { + sem = make(chan struct{}, maxConcurrency) + } + return &asyncGate{ + semaphore: sem, + completions: completions, + } +} + +// TryAcquire attempts to acquire a concurrency slot and increments the active calls count. +func (g *asyncGate) TryAcquire() bool { + if g == nil { + return true + } + if g.semaphore != nil { + select { + case g.semaphore <- struct{}{}: + default: + return false + } + } + g.activeCalls.Add(1) + return true +} + +// Release releases a concurrency slot and decrements the active calls count (used for defensive recovery). +func (g *asyncGate) Release() { + if g == nil { + return + } + if g.semaphore != nil { + select { + case <-g.semaphore: + default: + } + } + g.activeCalls.Add(-1) +} + +// Complete releases a concurrency slot and notifies completions. +func (g *asyncGate) Complete(ctx context.Context, callID int64) { + if g == nil { + return + } + g.Release() + + if g.completions != nil { + // Prioritize context cancellation to prevent racy completion signals. + if ctx.Err() != nil { + return + } + select { + case g.completions <- callID: + case <-ctx.Done(): + } + } +} + +// ActiveCalls returns the number of active asynchronous calls. +func (g *asyncGate) ActiveCalls() int { + if g == nil { + return 0 + } + return int(g.activeCalls.Load()) +} diff --git a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go b/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go index 41ca5cd2..bbaca522 100644 --- a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go +++ b/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go @@ -255,6 +255,9 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( patterns := vars.UnknownAttributePatterns() candidateIndices := map[int]struct{}{} for _, variable := range variableNames { + if holder, ok := vars.(localVariableHolder); ok && holder.IsLocalVariable(variable) { + continue + } for i, pat := range patterns { if pat.VariableMatches(variable) { if len(qualifiers) == 0 { diff --git a/vendor/github.com/google/cel-go/interpreter/attributes.go b/vendor/github.com/google/cel-go/interpreter/attributes.go index 6b8b5c1b..26d8eb0f 100644 --- a/vendor/github.com/google/cel-go/interpreter/attributes.go +++ b/vendor/github.com/google/cel-go/interpreter/attributes.go @@ -190,7 +190,7 @@ func (r *attrFactory) AbsoluteAttribute(id int64, names ...string) NamespacedAtt func (r *attrFactory) ConditionalAttribute(id int64, expr Interpretable, t, f Attribute) Attribute { return &conditionalAttribute{ id: id, - expr: expr, + expr: adaptToV2(expr), truthy: t, falsy: f, adapter: r.adapter, @@ -225,7 +225,7 @@ func (r *attrFactory) MaybeAttribute(id int64, name string) Attribute { func (r *attrFactory) RelativeAttribute(id int64, operand Interpretable) Attribute { return &relativeAttribute{ id: id, - operand: operand, + operand: adaptToV2(operand), qualifiers: []Qualifier{}, adapter: r.adapter, fac: r, @@ -384,7 +384,7 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) { type conditionalAttribute struct { id int64 - expr Interpretable + expr InterpretableV2 truthy Attribute falsy Attribute adapter types.Adapter @@ -571,7 +571,7 @@ func (a *maybeAttribute) String() string { type relativeAttribute struct { id int64 - operand Interpretable + operand InterpretableV2 qualifiers []Qualifier adapter types.Adapter fac AttributeFactory @@ -964,9 +964,11 @@ func (q *intQualifier) qualifyInternal(vars Activation, obj any, presenceTest, p } case map[int32]any: isMap = true - obj, isKey := o[int32(i)] - if isKey { - return obj, true, nil + if i32 := int32(i); int64(i32) == i { + obj, isKey := o[i32] + if isKey { + return obj, true, nil + } } case map[int64]any: isMap = true @@ -1089,9 +1091,11 @@ func (q *uintQualifier) qualifyInternal(vars Activation, obj any, presenceTest, return obj, true, nil } case map[uint32]any: - obj, isKey := o[uint32(u)] - if isKey { - return obj, true, nil + if u32 := uint32(u); uint64(u32) == u { + obj, isKey := o[u32] + if isKey { + return obj, true, nil + } } case map[uint64]any: obj, isKey := o[u] @@ -1301,7 +1305,7 @@ func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, boo if !optObj.HasValue() { return optObj, false, nil } - obj = optObj.GetValue().Value() + obj = optObj.GetValue() } var err error diff --git a/vendor/github.com/google/cel-go/interpreter/decorators.go b/vendor/github.com/google/cel-go/interpreter/decorators.go index 502db35f..9c973664 100644 --- a/vendor/github.com/google/cel-go/interpreter/decorators.go +++ b/vendor/github.com/google/cel-go/interpreter/decorators.go @@ -25,9 +25,13 @@ import ( // Interpretable expression nodes at construction time. type InterpretableDecorator func(Interpretable) (Interpretable, error) +// InterpretableDecoratorV2 is a functional interface for decorating or replacing +// InterpretableV2 expression nodes at construction time. +type InterpretableDecoratorV2 func(InterpretableV2) (InterpretableV2, error) + // decObserveEval records evaluation state into an EvalState object. -func decObserveEval(observer EvalObserver) InterpretableDecorator { - return func(i Interpretable) (Interpretable, error) { +func decObserveEval(observer EvalObserver) InterpretableDecoratorV2 { + return func(i InterpretableV2) (InterpretableV2, error) { switch inst := i.(type) { case *evalWatch, *evalWatchAttr, *evalWatchConst, *evalWatchConstructor: // these instruction are already watching, return straight-away. @@ -49,8 +53,8 @@ func decObserveEval(observer EvalObserver) InterpretableDecorator { }, nil default: return &evalWatch{ - Interpretable: i, - observer: observer, + InterpretableV2: i, + observer: observer, }, nil } } @@ -58,8 +62,8 @@ func decObserveEval(observer EvalObserver) InterpretableDecorator { // decInterruptFolds creates an intepretable decorator which marks comprehensions as interruptable // where the interrupt state is communicated via a hidden variable on the Activation. -func decInterruptFolds() InterpretableDecorator { - return func(i Interpretable) (Interpretable, error) { +func decInterruptFolds() InterpretableDecoratorV2 { + return func(i InterpretableV2) (InterpretableV2, error) { fold, ok := i.(*evalFold) if !ok { return i, nil @@ -70,8 +74,8 @@ func decInterruptFolds() InterpretableDecorator { } // decDisableShortcircuits ensures that all branches of an expression will be evaluated, no short-circuiting. -func decDisableShortcircuits() InterpretableDecorator { - return func(i Interpretable) (Interpretable, error) { +func decDisableShortcircuits() InterpretableDecoratorV2 { + return func(i InterpretableV2) (InterpretableV2, error) { switch expr := i.(type) { case *evalOr: return &evalExhaustiveOr{ @@ -104,8 +108,8 @@ func decDisableShortcircuits() InterpretableDecorator { // conditionally precomputing the result. // - build list and map values with constant elements. // - convert 'in' operations to set membership tests if possible. -func decOptimize() InterpretableDecorator { - return func(i Interpretable) (Interpretable, error) { +func decOptimize() InterpretableDecoratorV2 { + return func(i InterpretableV2) (InterpretableV2, error) { switch inst := i.(type) { case *evalList: return maybeBuildListLiteral(i, inst) @@ -124,7 +128,7 @@ func decOptimize() InterpretableDecorator { } // decRegexOptimizer compiles regex pattern string constants. -func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDecorator { +func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDecoratorV2 { functionMatchMap := make(map[string]*RegexOptimization) overloadMatchMap := make(map[string]*RegexOptimization) for _, m := range regexOptimizations { @@ -134,7 +138,7 @@ func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDe } } - return func(i Interpretable) (Interpretable, error) { + return func(i InterpretableV2) (InterpretableV2, error) { call, ok := i.(InterpretableCall) if !ok { return i, nil @@ -165,7 +169,7 @@ func decRegexOptimizer(regexOptimizations ...*RegexOptimization) InterpretableDe } } -func maybeOptimizeConstUnary(i Interpretable, call InterpretableCall) (Interpretable, error) { +func maybeOptimizeConstUnary(i InterpretableV2, call InterpretableCall) (InterpretableV2, error) { args := call.Args() if len(args) != 1 { return i, nil @@ -181,7 +185,7 @@ func maybeOptimizeConstUnary(i Interpretable, call InterpretableCall) (Interpret return NewConstValue(call.ID(), val), nil } -func maybeBuildListLiteral(i Interpretable, l *evalList) (Interpretable, error) { +func maybeBuildListLiteral(i InterpretableV2, l *evalList) (InterpretableV2, error) { for _, elem := range l.elems { _, isConst := elem.(InterpretableConst) if !isConst { @@ -191,7 +195,7 @@ func maybeBuildListLiteral(i Interpretable, l *evalList) (Interpretable, error) return NewConstValue(l.ID(), l.Eval(EmptyActivation())), nil } -func maybeBuildMapLiteral(i Interpretable, mp *evalMap) (Interpretable, error) { +func maybeBuildMapLiteral(i InterpretableV2, mp *evalMap) (InterpretableV2, error) { for idx, key := range mp.keys { _, isConst := key.(InterpretableConst) if !isConst { @@ -209,7 +213,7 @@ func maybeBuildMapLiteral(i Interpretable, mp *evalMap) (Interpretable, error) { // test if the following conditions are true: // - the list is a constant with homogeneous element types. // - the elements are all of primitive type. -func maybeOptimizeSetMembership(i Interpretable, inlist InterpretableCall) (Interpretable, error) { +func maybeOptimizeSetMembership(i InterpretableV2, inlist InterpretableCall) (InterpretableV2, error) { args := inlist.Args() lhs := args[0] rhs := args[1] diff --git a/vendor/github.com/google/cel-go/interpreter/frame.go b/vendor/github.com/google/cel-go/interpreter/frame.go new file mode 100644 index 00000000..20ab313c --- /dev/null +++ b/vendor/github.com/google/cel-go/interpreter/frame.go @@ -0,0 +1,445 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interpreter + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// evalContext contains the stateful information needed for a single evaluation. +// +// This state is shared across all frames within a single evaluation, including +// child frames created for comprehension blocks. +type evalContext struct { + // interrupt exposes a callback channel for cancellation. + interrupt <-chan struct{} + + // interruptCheckCount is the number of times the interrupt channel has been checked. + interruptCheckCount atomic.Uint64 + + // interruptCheckFrequency is the frequency at which the interrupt channel is checked. + interruptCheckFrequency uint + + // interrupted indicates whether the evaluation has been interrupted. + interrupted atomic.Bool + + // state provides the context for tracking the evaluation state. + state EvalState + + // costs provides the context for tracking the evaluation costs. + costs *CostTracker + + // ctx is the context for async call implementations to use. + ctx context.Context + + // cancel cancels the context when the evaluation is finished. + cancel context.CancelFunc + + // asyncCalls tracks the state of async call invocations across re-evaluations. + asyncCalls *asyncCallStateTracker + + // gate coordinates async call admission control and completion signaling. + gate *asyncGate + + // observer for monitoring async calls. + observer AsyncObserver +} + +// ExecutionFrame provides the context for a single evaluation of an expression. +// +// The execution frame must not be stored in any fashion as its lifecycle is completely +// controlled by the CEL evaluation process. +type ExecutionFrame struct { + // Activation provides the context for resolving variables by name. + Activation + + // parent provides the context for parent scopes (used for comprehension iterators). + parent *ExecutionFrame + + // ctx provides the shared evaluation state across frames. + ctx *evalContext +} + +// NewExecutionFrame creates a new execution frame from the pool. +func NewExecutionFrame(input any) (*ExecutionFrame, error) { + f := frameStack.Get().(*ExecutionFrame) + switch v := input.(type) { + case Activation: + f.Activation = v + case map[string]any: + f.Activation = activationInput.create(v) + default: + return nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input) + } + return f, nil +} + +// SetContext sets the context for the execution frame. +func (f *ExecutionFrame) SetContext(ctx context.Context, interruptCheckFrequency uint) error { + if f.parent != nil { + return errors.New("SetContext() called on child frame") + } + if f.ctx != nil { + return errors.New("SetContext() called more than once") + } + f.ctx = evalContextPool.Get().(*evalContext) + f.ctx.ctx, f.ctx.cancel = context.WithCancel(ctx) + f.ctx.asyncCalls = asyncCallStateTrackerPool.create() + f.ctx.gate = &asyncGate{} + f.ctx.interrupt = ctx.Done() + f.ctx.interruptCheckFrequency = interruptCheckFrequency + f.ctx.interruptCheckCount.Store(0) + f.ctx.interrupted.Store(false) + return nil +} + +// Close releases the resources held by the execution frame and returns it to the pool. +func (f *ExecutionFrame) Close() { + if f.parent == nil && f.ctx != nil { + if f.ctx.cancel != nil { + f.ctx.cancel() + f.ctx.cancel = nil + } + f.ctx.ctx = nil + f.ctx.gate = nil + asyncCallStateTrackerPool.release(f.ctx.asyncCalls) + f.ctx.asyncCalls = nil + f.ctx.observer = nil + f.ctx.interrupt = nil + f.ctx.state = nil + f.ctx.costs = nil + f.ctx.interrupted.Store(false) + f.ctx.interruptCheckCount.Store(0) + f.ctx.interruptCheckFrequency = 0 + evalContextPool.Put(f.ctx) + } + f.ctx = nil + f.parent = nil + if f.Activation != nil { + switch a := f.Activation.(type) { + case *hierarchicalActivation: + if child, ok := a.child.(*inputActivation); ok { + activationInput.release(child) + } + activationStack.release(a) + case *inputActivation: + activationInput.release(a) + } + f.Activation = nil + frameStack.Put(f) + } +} + +// Push pushes the given activation onto the activation stack and returns the new frame. +// +// This operation is internal to the interpreter and is used to handle comprehension +// scoping. The child frame inherits the shared evalContext from the parent. +func (f *ExecutionFrame) Push(activation Activation) *ExecutionFrame { + child := frameStack.Get().(*ExecutionFrame) + child.parent = f + child.ctx = f.ctx + child.Activation = activationStack.create(f.Activation, activation) + return child +} + +// Pop returns the parent frame, releasing the current frame back to the pool. +func (f *ExecutionFrame) Pop() *ExecutionFrame { + if f.parent == nil { + return f + } + parent := f.parent + activationStack.release(f.Activation) + f.Activation = nil + f.parent = nil + f.ctx = nil + frameStack.Put(f) + return parent +} + +// ResolveName implements the Activation interface by proxying to the internal activation. +func (f *ExecutionFrame) ResolveName(name string) (any, bool) { + return f.Activation.ResolveName(name) +} + +// Parent implements the Activation interface by proxying to the internal activation. +func (f *ExecutionFrame) Parent() Activation { + return f.Activation.Parent() +} + +// AsPartialActivation implements the PartialActivation interface by proxying to the internal activation. +func (f *ExecutionFrame) AsPartialActivation() (PartialActivation, bool) { + return AsPartialActivation(f.Activation) +} + +// Unwrap returns the internal activation. +func (f *ExecutionFrame) Unwrap() Activation { + return f.Activation +} + +// IsLocalVariable reports whether the variable name is locally bound in the frame. +func (f *ExecutionFrame) IsLocalVariable(name string) bool { + if holder, ok := f.Activation.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + // Search parent scopes + if f.parent != nil { + return f.parent.IsLocalVariable(name) + } + return false +} + +// CheckInterrupt returns whether the evaluation has been interrupted. +func (f *ExecutionFrame) CheckInterrupt() bool { + if f.ctx == nil { + return false + } + if f.ctx.interrupted.Load() { + return true + } + count := f.ctx.interruptCheckCount.Add(1) + if f.ctx.interruptCheckFrequency > 0 && count%uint64(f.ctx.interruptCheckFrequency) == 0 { + select { + case <-f.ctx.interrupt: + f.ctx.interrupted.Store(true) + return true + default: + return false + } + } + return false +} + +// ComputeResult tracks and computes the result of the given asynchronous function. +// +// The first invocation for a given (node id, args) tuple registers the call state and returns an +// Unknown which references the call's unique callID. Subsequent invocations return the cached +// result once the call has completed. Launching background execution is deferred to post-execution +// dispatch via DispatchPendingAsyncCalls. +func (f *ExecutionFrame) ComputeResult(id int64, function, overload string, impl functions.AsyncOp, argVals []ref.Val) ref.Val { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return types.NewErrWithNodeID(id, "asynchronous function calls require concurrent evaluation and cannot be resolved by a synchronous Eval") + } + t := f.ctx.asyncCalls + acs := t.getOrCreate(id, function, overload, argVals, impl, f.ctx.gate) + if res := acs.ResultOrUnknown(); res != nil { + return res + } + return types.NewUnknown(acs.callID, nil) +} + +// DispatchPendingAsyncCalls launches pending asynchronous calls for the specified required call IDs. +func (f *ExecutionFrame) DispatchPendingAsyncCalls(callIDs []int64) { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return + } + t := f.ctx.asyncCalls + for _, callID := range callIDs { + if acs := t.getByID(callID); acs != nil { + t.launch(f.ctx.ctx, acs, f.ctx.observer) + } + } +} + +// ActiveAsyncCalls returns the number of async function calls that have been launched +// but whose completions have not yet been drained. +func (f *ExecutionFrame) ActiveAsyncCalls() int { + if f.ctx == nil || f.ctx.gate == nil { + return 0 + } + return f.ctx.gate.ActiveCalls() +} + +// AsyncCall returns the state of an async call by its callID, or nil if not found. +func (f *ExecutionFrame) AsyncCall(callID int64) AsyncCall { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return nil + } + acs := f.ctx.asyncCalls.getByID(callID) + if acs == nil { + return nil + } + return acs +} + +// SetCompletions configures a channel to receive callIDs when asynchronous evaluations finish. +func (f *ExecutionFrame) SetCompletions(ch chan<- int64) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.gate.completions = ch + return nil +} + +// SetAsyncObserver sets the observer for monitoring asynchronous function calls. +func (f *ExecutionFrame) SetAsyncObserver(observer AsyncObserver) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.observer = observer + return nil +} + +// SetAsyncMaxConcurrency sets the maximum concurrency for asynchronous function calls. +// +// A non-positive value indicates that concurrency is unbounded. +func (f *ExecutionFrame) SetAsyncMaxConcurrency(n int) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + if n > 0 { + f.ctx.gate.semaphore = make(chan struct{}, n) + } else { + f.ctx.gate.semaphore = nil + } + return nil +} + +// frameStack provides a synchronized pool of ExecutionFrames. +var frameStack = &sync.Pool{ + New: func() any { + return &ExecutionFrame{} + }, +} + +// evalContextPool provides a synchronized pool of evalContexts. +var evalContextPool = &sync.Pool{ + New: func() any { + return &evalContext{} + }, +} + +type activationStackPool struct { + sync.Pool +} + +func (pool *activationStackPool) create(parent, child Activation) Activation { + h := pool.Get().(*hierarchicalActivation) + h.child = child + h.parent = parent + h.poolAllocated = true + return h +} + +func (pool *activationStackPool) release(activation Activation) { + h, ok := activation.(*hierarchicalActivation) + if !ok || !h.poolAllocated { + return + } + h.parent = nil + h.child = nil + pool.Pool.Put(h) +} + +func newActivationStackPool() *activationStackPool { + return &activationStackPool{ + Pool: sync.Pool{ + New: func() any { + return &hierarchicalActivation{} + }, + }, + } +} + +type inputActivation struct { + vars map[string]any + lazyVars map[string]any +} + +// ResolveName looks up the value of the input variable name, if found. +// +// Lazy bindings may be supplied within the map-based input in either of the following forms: +// - func() any +// - func() ref.Val +// +// The lazy binding will only be invoked once per evaluation. +// +// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using +// the types.Adapter configured in the environment. +func (a *inputActivation) ResolveName(name string) (any, bool) { + v, found := a.vars[name] + if !found { + return nil, false + } + switch obj := v.(type) { + case func() ref.Val: + if resolved, found := a.lazyVars[name]; found { + return resolved, true + } + lazy := obj() + a.lazyVars[name] = lazy + return lazy, true + case func() any: + if resolved, found := a.lazyVars[name]; found { + return resolved, true + } + lazy := obj() + a.lazyVars[name] = lazy + return lazy, true + default: + return obj, true + } +} + +// Parent implements the Activation interface +func (a *inputActivation) Parent() Activation { + return nil +} + +func newActivationInputPool() *activationInputPool { + return &activationInputPool{ + Pool: sync.Pool{ + New: func() any { + return &inputActivation{ + lazyVars: make(map[string]any), + } + }, + }, + } +} + +type activationInputPool struct { + sync.Pool +} + +// create initializes a pooled Activation object with the map input. +func (p *activationInputPool) create(vars map[string]any) *inputActivation { + a := p.Pool.Get().(*inputActivation) + a.vars = vars + return a +} + +func (p *activationInputPool) release(value any) { + a := value.(*inputActivation) + for k := range a.lazyVars { + delete(a.lazyVars, k) + } + a.vars = nil + p.Pool.Put(a) +} + +var ( + activationStack = newActivationStackPool() + activationInput = newActivationInputPool() +) diff --git a/vendor/github.com/google/cel-go/interpreter/interpretable.go b/vendor/github.com/google/cel-go/interpreter/interpretable.go index 50e66d63..906c4f80 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpretable.go +++ b/vendor/github.com/google/cel-go/interpreter/interpretable.go @@ -26,20 +26,53 @@ import ( "github.com/google/cel-go/common/types/traits" ) -// Interpretable can accept a given Activation and produce a value along with -// an accompanying EvalState which can be used to inspect whether additional -// data might be necessary to complete the evaluation. +// Interpretable evaluates an Activation and produces a value. type Interpretable interface { // ID value corresponding to the expression node. ID() int64 - // Eval an Activation to produce an output. + // Eval evaluates an Activation and produces an output. Eval(activation Activation) ref.Val } +// InterpretableV2 evaluates an ExecutionFrame and produces a value. +// +// The ExecutionFrame should not be stored and should always be passed as the first +// argument to any function as it behaves like Golang's context.Context. +type InterpretableV2 interface { + Interpretable + + // Exec evaluates the expression within the given ExecutionFrame. + Exec(frame *ExecutionFrame) ref.Val +} + +// adaptToV2 adapts a V1 Interpretable implementation to the V2 interface. +// +// This adapter is used to bridge the legacy Interpretable interface to the +// modern InterpretableV2 interface, providing a shim that allows the use of +// both interfaces in the same system. +func adaptToV2(i Interpretable) InterpretableV2 { + switch v := i.(type) { + case InterpretableV2: + return v + default: + return &v1Adapter{Interpretable: v} + } +} + +// v1Adapter handles bridging a V1 Interpretable implementation to the V2 interface. +type v1Adapter struct { + Interpretable +} + +// Exec implements the InterpretableV2 interface method. +func (a *v1Adapter) Exec(f *ExecutionFrame) ref.Val { + return a.Eval(f) +} + // InterpretableConst interface for tracking whether the Interpretable is a constant value. type InterpretableConst interface { - Interpretable + InterpretableV2 // Value returns the constant value of the instruction. Value() ref.Val @@ -47,7 +80,7 @@ type InterpretableConst interface { // InterpretableAttribute interface for tracking whether the Interpretable is an attribute. type InterpretableAttribute interface { - Interpretable + InterpretableV2 // Attr returns the Attribute value. Attr() Attribute @@ -81,7 +114,7 @@ type InterpretableAttribute interface { // InterpretableCall interface for inspecting Interpretable instructions related to function calls. type InterpretableCall interface { - Interpretable + InterpretableV2 // Function returns the function name as it appears in text or mangled operator name as it // appears in the operators.go file. @@ -94,16 +127,16 @@ type InterpretableCall interface { // Args returns the normalized arguments to the function overload. // For receiver-style functions, the receiver target is arg 0. - Args() []Interpretable + Args() []InterpretableV2 } // InterpretableConstructor interface for inspecting Interpretable instructions that initialize a list, map // or struct. type InterpretableConstructor interface { - Interpretable + InterpretableV2 // InitVals returns all the list elements, map key and values or struct field values. - InitVals() []Interpretable + InitVals() []InterpretableV2 // Type returns the type constructed. Type() ref.Type @@ -112,18 +145,23 @@ type InterpretableConstructor interface { // ObservableInterpretable is an Interpretable which supports stateful observation, such as tracing // or cost-tracking. type ObservableInterpretable struct { - Interpretable + InterpretableV2 observers []StatefulObserver } // ID implements the Interpretable method to get the expression id associated with the step. func (oi *ObservableInterpretable) ID() int64 { - return oi.Interpretable.ID() + return oi.InterpretableV2.ID() +} + +// Exec implements the InterpretableV2 interface method. +func (oi *ObservableInterpretable) Exec(frame *ExecutionFrame) ref.Val { + return oi.ObserveExec(frame, func(any) {}) } // Eval proxies to the ObserveEval method while invoking a no-op callback to report the observations. func (oi *ObservableInterpretable) Eval(vars Activation) ref.Val { - return oi.ObserveEval(vars, func(any) {}) + return oi.ObserveExec(AsFrame(vars), func(any) {}) } // ObserveEval evaluates an interpretable and performs per-evaluation state-tracking. @@ -131,25 +169,65 @@ func (oi *ObservableInterpretable) Eval(vars Activation) ref.Val { // This method is concurrency safe and the expectation is that the observer function will use // a switch statement to determine the type of the state which has been reported back from the call. func (oi *ObservableInterpretable) ObserveEval(vars Activation, observer func(any)) ref.Val { - var err error + return oi.ObserveExec(AsFrame(vars), observer) +} + +// ObserveExec evaluates an interpretable and performs per-evaluation state-tracking. +// +// This method is concurrency safe and the expectation is that the observer function will use +// a switch statement to determine the type of the state which has been reported back from the call. +func (oi *ObservableInterpretable) ObserveExec(frame *ExecutionFrame, observer func(any)) ref.Val { // Initialize the state needed for the observers to function. for _, obs := range oi.observers { - vars, err = obs.InitState(vars) + state, err := obs.InitState(frame) if err != nil { return types.WrapErr(err) } // Provide an initial reference to the state to ensure state is available // even in cases of interrupting errors generated during evaluation. - observer(obs.GetState(vars)) + observer(state) } - result := oi.Interpretable.Eval(vars) + result := oi.InterpretableV2.Exec(frame) // Get the state which needs to be reported back as having been observed. for _, obs := range oi.observers { - observer(obs.GetState(vars)) + observer(obs.GetState(frame)) } return result } +// AsFrame promotes an Activation to an ExecutionFrame. +func AsFrame(a Activation) *ExecutionFrame { + if f, ok := a.(*ExecutionFrame); ok { + return f + } + frame := &ExecutionFrame{Activation: a} + // Walk the activation hierarchy to find a parent ExecutionFrame and inherit + // its shared context. + if parent := findFrame(a); parent != nil { + frame.ctx = parent.ctx + } + return frame +} + +// findFrame walks the activation hierarchy via Unwrap and Parent to locate an +// existing ExecutionFrame, if one exists. +func findFrame(a Activation) *ExecutionFrame { + if wrapper, ok := a.(activationWrapper); ok { + unwrapped := wrapper.Unwrap() + if f, ok := unwrapped.(*ExecutionFrame); ok { + return f + } + return findFrame(unwrapped) + } + if p := a.Parent(); p != nil { + if f, ok := p.(*ExecutionFrame); ok { + return f + } + return findFrame(p) + } + return nil +} + // Core Interpretable implementations used during the program planning phase. type evalTestOnly struct { @@ -162,9 +240,9 @@ func (test *evalTestOnly) ID() int64 { return test.id } -// Eval implements the Interpretable interface method. -func (test *evalTestOnly) Eval(ctx Activation) ref.Val { - val, err := test.Resolve(ctx) +// Exec implements the InterpretableV2 interface method. +func (test *evalTestOnly) Exec(frame *ExecutionFrame) ref.Val { + val, err := test.Resolve(frame) // Return an error if the resolve step fails if err != nil { return types.LabelErrNode(test.id, types.WrapErr(err)) @@ -175,6 +253,11 @@ func (test *evalTestOnly) Eval(ctx Activation) ref.Val { return test.Adapter().NativeToValue(val) } +// Eval implements the Interpretable interface method. +func (test *evalTestOnly) Eval(ctx Activation) ref.Val { + return test.Exec(AsFrame(ctx)) +} + // AddQualifier appends a qualifier that will always and only perform a presence test. func (test *evalTestOnly) AddQualifier(q Qualifier) (Attribute, error) { cq, ok := q.(ConstantQualifier) @@ -194,7 +277,7 @@ func (q *testOnlyQualifier) Qualify(vars Activation, obj any) (any, error) { if err != nil { return nil, err } - if unk, isUnk := out.(types.Unknown); isUnk { + if unk, isUnk := out.(*types.Unknown); isUnk { return unk, nil } return present, nil @@ -230,6 +313,11 @@ func (cons *evalConst) ID() int64 { return cons.id } +// Exec implements the InterpretableV2 interface method. +func (cons *evalConst) Exec(frame *ExecutionFrame) ref.Val { + return cons.val +} + // Eval implements the Interpretable interface method. func (cons *evalConst) Eval(ctx Activation) ref.Val { return cons.val @@ -242,7 +330,7 @@ func (cons *evalConst) Value() ref.Val { type evalOr struct { id int64 - terms []Interpretable + terms []InterpretableV2 } // ID implements the Interpretable interface method. @@ -250,12 +338,12 @@ func (or *evalOr) ID() int64 { return or.id } -// Eval implements the Interpretable interface method. -func (or *evalOr) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (or *evalOr) Exec(frame *ExecutionFrame) ref.Val { var err ref.Val = nil var unk *types.Unknown for _, term := range or.terms { - val := term.Eval(ctx) + val := term.Exec(frame) boolVal, ok := val.(types.Bool) // short-circuit on true. if ok && boolVal == types.True { @@ -283,9 +371,14 @@ func (or *evalOr) Eval(ctx Activation) ref.Val { return types.False } +// Eval implements the Interpretable interface method. +func (or *evalOr) Eval(ctx Activation) ref.Val { + return or.Exec(AsFrame(ctx)) +} + type evalAnd struct { id int64 - terms []Interpretable + terms []InterpretableV2 } // ID implements the Interpretable interface method. @@ -293,12 +386,12 @@ func (and *evalAnd) ID() int64 { return and.id } -// Eval implements the Interpretable interface method. -func (and *evalAnd) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (and *evalAnd) Exec(frame *ExecutionFrame) ref.Val { var err ref.Val = nil var unk *types.Unknown for _, term := range and.terms { - val := term.Eval(ctx) + val := term.Exec(frame) boolVal, ok := val.(types.Bool) // short-circuit on false. if ok && boolVal == types.False { @@ -326,10 +419,15 @@ func (and *evalAnd) Eval(ctx Activation) ref.Val { return types.True } +// Eval implements the Interpretable interface method. +func (and *evalAnd) Eval(ctx Activation) ref.Val { + return and.Exec(AsFrame(ctx)) +} + type evalEq struct { id int64 - lhs Interpretable - rhs Interpretable + lhs InterpretableV2 + rhs InterpretableV2 } // ID implements the Interpretable interface method. @@ -337,19 +435,30 @@ func (eq *evalEq) ID() int64 { return eq.id } -// Eval implements the Interpretable interface method. -func (eq *evalEq) Eval(ctx Activation) ref.Val { - lVal := eq.lhs.Eval(ctx) - rVal := eq.rhs.Eval(ctx) - if types.IsUnknownOrError(lVal) { +// Exec implements the InterpretableV2 interface method. +func (eq *evalEq) Exec(frame *ExecutionFrame) ref.Val { + lVal := eq.lhs.Exec(frame) + if types.IsError(lVal) { return lVal } - if types.IsUnknownOrError(rVal) { + rVal := eq.rhs.Exec(frame) + if types.IsError(rVal) { return rVal } + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk + } return types.Equal(lVal, rVal) } +// Eval implements the Interpretable interface method. +func (eq *evalEq) Eval(ctx Activation) ref.Val { + return eq.Exec(AsFrame(ctx)) +} + // Function implements the InterpretableCall interface method. func (*evalEq) Function() string { return operators.Equals @@ -361,14 +470,14 @@ func (*evalEq) OverloadID() string { } // Args implements the InterpretableCall interface method. -func (eq *evalEq) Args() []Interpretable { - return []Interpretable{eq.lhs, eq.rhs} +func (eq *evalEq) Args() []InterpretableV2 { + return []InterpretableV2{eq.lhs, eq.rhs} } type evalNe struct { id int64 - lhs Interpretable - rhs Interpretable + lhs InterpretableV2 + rhs InterpretableV2 } // ID implements the Interpretable interface method. @@ -376,19 +485,30 @@ func (ne *evalNe) ID() int64 { return ne.id } -// Eval implements the Interpretable interface method. -func (ne *evalNe) Eval(ctx Activation) ref.Val { - lVal := ne.lhs.Eval(ctx) - rVal := ne.rhs.Eval(ctx) - if types.IsUnknownOrError(lVal) { +// Exec implements the InterpretableV2 interface method. +func (ne *evalNe) Exec(frame *ExecutionFrame) ref.Val { + lVal := ne.lhs.Exec(frame) + if types.IsError(lVal) { return lVal } - if types.IsUnknownOrError(rVal) { + rVal := ne.rhs.Exec(frame) + if types.IsError(rVal) { return rVal } + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk + } return types.Bool(types.Equal(lVal, rVal) != types.True) } +// Eval implements the Interpretable interface method. +func (ne *evalNe) Eval(ctx Activation) ref.Val { + return ne.Exec(AsFrame(ctx)) +} + // Function implements the InterpretableCall interface method. func (*evalNe) Function() string { return operators.NotEquals @@ -400,8 +520,8 @@ func (*evalNe) OverloadID() string { } // Args implements the InterpretableCall interface method. -func (ne *evalNe) Args() []Interpretable { - return []Interpretable{ne.lhs, ne.rhs} +func (ne *evalNe) Args() []InterpretableV2 { + return []InterpretableV2{ne.lhs, ne.rhs} } type evalZeroArity struct { @@ -416,9 +536,14 @@ func (zero *evalZeroArity) ID() int64 { return zero.id } +// Exec implements the InterpretableV2 interface method. +func (zero *evalZeroArity) Exec(frame *ExecutionFrame) ref.Val { + return types.LabelErrNode(zero.id, zero.impl()) +} + // Eval implements the Interpretable interface method. func (zero *evalZeroArity) Eval(ctx Activation) ref.Val { - return types.LabelErrNode(zero.id, zero.impl()) + return zero.Exec(AsFrame(ctx)) } // Function implements the InterpretableCall interface method. @@ -432,15 +557,15 @@ func (zero *evalZeroArity) OverloadID() string { } // Args returns the argument to the unary function. -func (zero *evalZeroArity) Args() []Interpretable { - return []Interpretable{} +func (zero *evalZeroArity) Args() []InterpretableV2 { + return []InterpretableV2{} } type evalUnary struct { id int64 function string overload string - arg Interpretable + arg InterpretableV2 trait int impl functions.UnaryOp nonStrict bool @@ -451,9 +576,9 @@ func (un *evalUnary) ID() int64 { return un.id } -// Eval implements the Interpretable interface method. -func (un *evalUnary) Eval(ctx Activation) ref.Val { - argVal := un.arg.Eval(ctx) +// Exec implements the InterpretableV2 interface method. +func (un *evalUnary) Exec(frame *ExecutionFrame) ref.Val { + argVal := un.arg.Exec(frame) // Early return if the argument to the function is unknown or error. strict := !un.nonStrict if strict && types.IsUnknownOrError(argVal) { @@ -472,6 +597,11 @@ func (un *evalUnary) Eval(ctx Activation) ref.Val { return types.NewErrWithNodeID(un.id, "no such overload: %s", un.function) } +// Eval implements the Interpretable interface method. +func (un *evalUnary) Eval(ctx Activation) ref.Val { + return un.Exec(AsFrame(ctx)) +} + // Function implements the InterpretableCall interface method. func (un *evalUnary) Function() string { return un.function @@ -483,16 +613,16 @@ func (un *evalUnary) OverloadID() string { } // Args returns the argument to the unary function. -func (un *evalUnary) Args() []Interpretable { - return []Interpretable{un.arg} +func (un *evalUnary) Args() []InterpretableV2 { + return []InterpretableV2{un.arg} } type evalBinary struct { id int64 function string overload string - lhs Interpretable - rhs Interpretable + lhs InterpretableV2 + rhs InterpretableV2 trait int impl functions.BinaryOp nonStrict bool @@ -503,18 +633,23 @@ func (bin *evalBinary) ID() int64 { return bin.id } -// Eval implements the Interpretable interface method. -func (bin *evalBinary) Eval(ctx Activation) ref.Val { - lVal := bin.lhs.Eval(ctx) - rVal := bin.rhs.Eval(ctx) - // Early return if any argument to the function is unknown or error. +// Exec implements the InterpretableV2 interface method. +func (bin *evalBinary) Exec(frame *ExecutionFrame) ref.Val { + lVal := bin.lhs.Exec(frame) strict := !bin.nonStrict + if strict && types.IsError(lVal) { + return lVal + } + rVal := bin.rhs.Exec(frame) + if strict && types.IsError(rVal) { + return rVal + } if strict { - if types.IsUnknownOrError(lVal) { - return lVal - } - if types.IsUnknownOrError(rVal) { - return rVal + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk } } // If the implementation is bound and the argument value has the right traits required to @@ -530,6 +665,11 @@ func (bin *evalBinary) Eval(ctx Activation) ref.Val { return types.NewErrWithNodeID(bin.id, "no such overload: %s", bin.function) } +// Eval implements the Interpretable interface method. +func (bin *evalBinary) Eval(ctx Activation) ref.Val { + return bin.Exec(AsFrame(ctx)) +} + // Function implements the InterpretableCall interface method. func (bin *evalBinary) Function() string { return bin.function @@ -541,22 +681,22 @@ func (bin *evalBinary) OverloadID() string { } // Args returns the argument to the unary function. -func (bin *evalBinary) Args() []Interpretable { - return []Interpretable{bin.lhs, bin.rhs} +func (bin *evalBinary) Args() []InterpretableV2 { + return []InterpretableV2{bin.lhs, bin.rhs} } type evalVarArgs struct { id int64 function string overload string - args []Interpretable + args []InterpretableV2 trait int impl functions.FunctionOp nonStrict bool } // NewCall creates a new call Interpretable. -func NewCall(id int64, function, overload string, args []Interpretable, impl functions.FunctionOp) InterpretableCall { +func NewCall(id int64, function, overload string, args []InterpretableV2, impl functions.FunctionOp) InterpretableCall { return &evalVarArgs{ id: id, function: function, @@ -571,17 +711,23 @@ func (fn *evalVarArgs) ID() int64 { return fn.id } -// Eval implements the Interpretable interface method. -func (fn *evalVarArgs) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (fn *evalVarArgs) Exec(frame *ExecutionFrame) ref.Val { argVals := make([]ref.Val, len(fn.args)) - // Early return if any argument to the function is unknown or error. strict := !fn.nonStrict + var unk *types.Unknown for i, arg := range fn.args { - argVals[i] = arg.Eval(ctx) - if strict && types.IsUnknownOrError(argVals[i]) { - return argVals[i] + argVals[i] = arg.Exec(frame) + if strict { + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) } } + if strict && unk != nil { + return unk + } // If the implementation is bound and the argument value has the right traits required to // invoke it, then call the implementation. arg0 := argVals[0] @@ -596,6 +742,11 @@ func (fn *evalVarArgs) Eval(ctx Activation) ref.Val { return types.NewErrWithNodeID(fn.id, "no such overload: %s %d", fn.function, fn.id) } +// Eval implements the Interpretable interface method. +func (fn *evalVarArgs) Eval(ctx Activation) ref.Val { + return fn.Exec(AsFrame(ctx)) +} + // Function implements the InterpretableCall interface method. func (fn *evalVarArgs) Function() string { return fn.function @@ -607,13 +758,13 @@ func (fn *evalVarArgs) OverloadID() string { } // Args returns the argument to the unary function. -func (fn *evalVarArgs) Args() []Interpretable { +func (fn *evalVarArgs) Args() []InterpretableV2 { return fn.args } type evalList struct { id int64 - elems []Interpretable + elems []InterpretableV2 optionals []bool hasOptionals bool adapter types.Adapter @@ -624,31 +775,44 @@ func (l *evalList) ID() int64 { return l.id } -// Eval implements the Interpretable interface method. -func (l *evalList) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (l *evalList) Exec(frame *ExecutionFrame) ref.Val { elemVals := make([]ref.Val, 0, len(l.elems)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown for i, elem := range l.elems { - elemVal := elem.Eval(ctx) - if types.IsUnknownOrError(elemVal) { + elemVal := elem.Exec(frame) + if types.IsError(elemVal) { return elemVal } + unk, _ = types.MaybeMergeUnknowns(elemVal, unk) if l.hasOptionals && l.optionals[i] { - optVal, ok := elemVal.(*types.Optional) - if !ok { - return types.LabelErrNode(l.id, invalidOptionalElementInit(elemVal)) - } - if !optVal.HasValue() { - continue + if types.IsUnknown(elemVal) { + // skip optional checks for unknown values as they aren't fully resolved yet. + } else { + optVal, ok := elemVal.(*types.Optional) + if !ok { + return types.LabelErrNode(l.id, invalidOptionalElementInit(elemVal)) + } + if !optVal.HasValue() { + continue + } + elemVal = optVal.GetValue() } - elemVal = optVal.GetValue() } elemVals = append(elemVals, elemVal) } - return l.adapter.NativeToValue(elemVals) + if unk != nil { + return unk + } + return types.NewRefValList(l.adapter, elemVals) } -func (l *evalList) InitVals() []Interpretable { +// Eval implements the Interpretable interface method. +func (l *evalList) Eval(ctx Activation) ref.Val { + return l.Exec(AsFrame(ctx)) +} + +func (l *evalList) InitVals() []InterpretableV2 { return l.elems } @@ -658,8 +822,8 @@ func (l *evalList) Type() ref.Type { type evalMap struct { id int64 - keys []Interpretable - vals []Interpretable + keys []InterpretableV2 + vals []InterpretableV2 optionals []bool hasOptionals bool adapter types.Adapter @@ -670,20 +834,24 @@ func (m *evalMap) ID() int64 { return m.id } -// Eval implements the Interpretable interface method. -func (m *evalMap) Eval(ctx Activation) ref.Val { - entries := make(map[ref.Val]ref.Val) - // If any argument is unknown or error early terminate. +// Exec implements the InterpretableV2 interface method. +func (m *evalMap) Exec(frame *ExecutionFrame) ref.Val { + entries := make(map[ref.Val]ref.Val, len(m.keys)) + var unk *types.Unknown for i, key := range m.keys { - keyVal := key.Eval(ctx) - if types.IsUnknownOrError(keyVal) { + keyVal := key.Exec(frame) + if types.IsError(keyVal) { return keyVal } - valVal := m.vals[i].Eval(ctx) - if types.IsUnknownOrError(valVal) { + unk, _ = types.MaybeMergeUnknowns(keyVal, unk) + + valVal := m.vals[i].Exec(frame) + if types.IsError(valVal) { return valVal } - if m.hasOptionals && m.optionals[i] { + unk, _ = types.MaybeMergeUnknowns(valVal, unk) + + if m.hasOptionals && m.optionals[i] && !types.IsUnknown(valVal) { optVal, ok := valVal.(*types.Optional) if !ok { return types.LabelErrNode(m.id, invalidOptionalEntryInit(keyVal, valVal)) @@ -696,14 +864,22 @@ func (m *evalMap) Eval(ctx Activation) ref.Val { } entries[keyVal] = valVal } - return m.adapter.NativeToValue(entries) + if unk != nil { + return unk + } + return types.NewRefValMap(m.adapter, entries) } -func (m *evalMap) InitVals() []Interpretable { +// Eval implements the Interpretable interface method. +func (m *evalMap) Eval(ctx Activation) ref.Val { + return m.Exec(AsFrame(ctx)) +} + +func (m *evalMap) InitVals() []InterpretableV2 { if len(m.keys) != len(m.vals) { return nil } - result := make([]Interpretable, len(m.keys)+len(m.vals)) + result := make([]InterpretableV2, len(m.keys)+len(m.vals)) idx := 0 for i, k := range m.keys { v := m.vals[i] @@ -723,7 +899,7 @@ type evalObj struct { id int64 typeName string fields []string - vals []Interpretable + vals []InterpretableV2 optionals []bool hasOptionals bool provider types.Provider @@ -734,16 +910,17 @@ func (o *evalObj) ID() int64 { return o.id } -// Eval implements the Interpretable interface method. -func (o *evalObj) Eval(ctx Activation) ref.Val { - fieldVals := make(map[string]ref.Val) - // If any argument is unknown or error early terminate. +// Exec implements the InterpretableV2 interface method. +func (o *evalObj) Exec(frame *ExecutionFrame) ref.Val { + fieldVals := make(map[string]ref.Val, len(o.fields)) + var unk *types.Unknown for i, field := range o.fields { - val := o.vals[i].Eval(ctx) - if types.IsUnknownOrError(val) { + val := o.vals[i].Exec(frame) + if types.IsError(val) { return val } - if o.hasOptionals && o.optionals[i] { + unk, _ = types.MaybeMergeUnknowns(val, unk) + if o.hasOptionals && o.optionals[i] && !types.IsUnknown(val) { optVal, ok := val.(*types.Optional) if !ok { return types.LabelErrNode(o.id, invalidOptionalEntryInit(field, val)) @@ -756,11 +933,19 @@ func (o *evalObj) Eval(ctx Activation) ref.Val { } fieldVals[field] = val } + if unk != nil { + return unk + } return types.LabelErrNode(o.id, o.provider.NewValue(o.typeName, fieldVals)) } +// Eval implements the Interpretable interface method. +func (o *evalObj) Eval(ctx Activation) ref.Val { + return o.Exec(AsFrame(ctx)) +} + // InitVals implements the InterpretableConstructor interface method. -func (o *evalObj) InitVals() []Interpretable { +func (o *evalObj) InitVals() []InterpretableV2 { return o.vals } @@ -774,11 +959,11 @@ type evalFold struct { accuVar string iterVar string iterVar2 string - iterRange Interpretable - accu Interpretable - cond Interpretable - step Interpretable - result Interpretable + iterRange InterpretableV2 + accu InterpretableV2 + cond InterpretableV2 + step InterpretableV2 + result InterpretableV2 adapter types.Adapter // note an exhaustive fold will ensure that all branches are evaluated @@ -793,13 +978,13 @@ func (fold *evalFold) ID() int64 { return fold.id } -// Eval implements the Interpretable interface method. -func (fold *evalFold) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (fold *evalFold) Exec(frame *ExecutionFrame) ref.Val { // Initialize the folder interface - f := newFolder(fold, ctx) + f := newFolder(fold, frame) defer releaseFolder(f) - foldRange := fold.iterRange.Eval(ctx) + foldRange := fold.iterRange.Exec(frame) if types.IsUnknownOrError(foldRange) { return foldRange } @@ -824,14 +1009,19 @@ func (fold *evalFold) Eval(ctx Activation) ref.Val { return f.foldIterable(iterable) } +// Eval implements the Interpretable interface method. +func (fold *evalFold) Eval(ctx Activation) ref.Val { + return fold.Exec(AsFrame(ctx)) +} + // Optional Interpretable implementations that specialize, subsume, or extend the core evaluation // plan via decorators. // evalSetMembership is an Interpretable implementation which tests whether an input value // exists within the set of map keys used to model a set. type evalSetMembership struct { - inst Interpretable - arg Interpretable + inst InterpretableV2 + arg InterpretableV2 valueSet map[ref.Val]ref.Val } @@ -840,9 +1030,9 @@ func (e *evalSetMembership) ID() int64 { return e.inst.ID() } -// Eval implements the Interpretable interface method. -func (e *evalSetMembership) Eval(ctx Activation) ref.Val { - val := e.arg.Eval(ctx) +// Exec implements the InterpretableV2 interface method. +func (e *evalSetMembership) Exec(frame *ExecutionFrame) ref.Val { + val := e.arg.Exec(frame) if types.IsUnknownOrError(val) { return val } @@ -852,18 +1042,28 @@ func (e *evalSetMembership) Eval(ctx Activation) ref.Val { return types.False } +// Eval implements the Interpretable interface method. +func (e *evalSetMembership) Eval(ctx Activation) ref.Val { + return e.Exec(AsFrame(ctx)) +} + // evalWatch is an Interpretable implementation that wraps the execution of a given // expression so that it may observe the computed value and send it to an observer. type evalWatch struct { - Interpretable + InterpretableV2 observer EvalObserver } +// Exec implements the InterpretableV2 interface method. +func (e *evalWatch) Exec(frame *ExecutionFrame) ref.Val { + val := e.InterpretableV2.Exec(frame) + e.observer(frame, e.ID(), e.InterpretableV2, val) + return val +} + // Eval implements the Interpretable interface method. func (e *evalWatch) Eval(vars Activation) ref.Val { - val := e.Interpretable.Eval(vars) - e.observer(vars, e.ID(), e.Interpretable, val) - return val + return e.Exec(AsFrame(vars)) } // evalWatchAttr describes a watcher of an InterpretableAttribute Interpretable. @@ -918,11 +1118,16 @@ func (e *evalWatchAttr) AddQualifier(q Qualifier) (Attribute, error) { return e, err } +// Exec implements the InterpretableV2 interface method. +func (e *evalWatchAttr) Exec(frame *ExecutionFrame) ref.Val { + val := e.InterpretableAttribute.Exec(frame) + e.observer(frame, e.ID(), e.InterpretableAttribute, val) + return val +} + // Eval implements the Interpretable interface method. func (e *evalWatchAttr) Eval(vars Activation) ref.Val { - val := e.InterpretableAttribute.Eval(vars) - e.observer(vars, e.ID(), e.InterpretableAttribute, val) - return val + return e.Exec(AsFrame(vars)) } // evalWatchConstQual observes the qualification of an object using a constant boolean, int, @@ -1049,17 +1254,22 @@ type evalWatchConst struct { observer EvalObserver } -// Eval implements the Interpretable interface method. -func (e *evalWatchConst) Eval(vars Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (e *evalWatchConst) Exec(frame *ExecutionFrame) ref.Val { val := e.Value() - e.observer(vars, e.ID(), e.InterpretableConst, val) + e.observer(frame, e.ID(), e.InterpretableConst, val) return val } +// Eval implements the Interpretable interface method. +func (e *evalWatchConst) Eval(vars Activation) ref.Val { + return e.Exec(AsFrame(vars)) +} + // evalExhaustiveOr is just like evalOr, but does not short-circuit argument evaluation. type evalExhaustiveOr struct { id int64 - terms []Interpretable + terms []InterpretableV2 } // ID implements the Interpretable interface method. @@ -1067,13 +1277,13 @@ func (or *evalExhaustiveOr) ID() int64 { return or.id } -// Eval implements the Interpretable interface method. -func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (or *evalExhaustiveOr) Exec(frame *ExecutionFrame) ref.Val { var err ref.Val = nil var unk *types.Unknown isTrue := false for _, term := range or.terms { - val := term.Eval(ctx) + val := term.Exec(frame) boolVal, ok := val.(types.Bool) // flag the result as true if ok && boolVal == types.True { @@ -1103,10 +1313,15 @@ func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { return types.False } +// Eval implements the Interpretable interface method. +func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { + return or.Exec(AsFrame(ctx)) +} + // evalExhaustiveAnd is just like evalAnd, but does not short-circuit argument evaluation. type evalExhaustiveAnd struct { id int64 - terms []Interpretable + terms []InterpretableV2 } // ID implements the Interpretable interface method. @@ -1114,13 +1329,13 @@ func (and *evalExhaustiveAnd) ID() int64 { return and.id } -// Eval implements the Interpretable interface method. -func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { +// Exec implements the InterpretableV2 interface method. +func (and *evalExhaustiveAnd) Exec(frame *ExecutionFrame) ref.Val { var err ref.Val = nil var unk *types.Unknown isFalse := false for _, term := range and.terms { - val := term.Eval(ctx) + val := term.Exec(frame) boolVal, ok := val.(types.Bool) // short-circuit on false. if ok && boolVal == types.False { @@ -1150,6 +1365,11 @@ func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { return types.True } +// Eval implements the Interpretable interface method. +func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { + return and.Exec(AsFrame(ctx)) +} + // evalExhaustiveConditional is like evalConditional, but does not short-circuit argument // evaluation. type evalExhaustiveConditional struct { @@ -1163,11 +1383,11 @@ func (cond *evalExhaustiveConditional) ID() int64 { return cond.id } -// Eval implements the Interpretable interface method. -func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val { - cVal := cond.attr.expr.Eval(ctx) - tVal, tErr := cond.attr.truthy.Resolve(ctx) - fVal, fErr := cond.attr.falsy.Resolve(ctx) +// Exec implements the InterpretableV2 interface method. +func (cond *evalExhaustiveConditional) Exec(frame *ExecutionFrame) ref.Val { + cVal := cond.attr.expr.Exec(frame) + tVal, tErr := cond.attr.truthy.Resolve(frame) + fVal, fErr := cond.attr.falsy.Resolve(frame) cBool, ok := cVal.(types.Bool) if !ok { return types.ValOrErr(cVal, "no such overload") @@ -1184,6 +1404,11 @@ func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val { return cond.adapter.NativeToValue(fVal) } +// Eval implements the Interpretable interface method. +func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val { + return cond.Exec(AsFrame(ctx)) +} + // evalAttr evaluates an Attribute value. type evalAttr struct { adapter types.Adapter @@ -1215,15 +1440,20 @@ func (a *evalAttr) Adapter() types.Adapter { return a.adapter } -// Eval implements the Interpretable interface method. -func (a *evalAttr) Eval(ctx Activation) ref.Val { - v, err := a.attr.Resolve(ctx) +// Exec implements the InterpretableV2 interface method. +func (a *evalAttr) Exec(frame *ExecutionFrame) ref.Val { + v, err := a.attr.Resolve(frame) if err != nil { return types.LabelErrNode(a.ID(), types.WrapErr(err)) } return a.adapter.NativeToValue(v) } +// Eval implements the Interpretable interface method. +func (a *evalAttr) Eval(ctx Activation) ref.Val { + return a.Exec(AsFrame(ctx)) +} + // Qualify proxies to the Attribute's Qualify method. func (a *evalAttr) Qualify(vars Activation, obj any) (any, error) { return a.attr.Qualify(vars, obj) @@ -1249,7 +1479,7 @@ type evalWatchConstructor struct { } // InitVals implements the InterpretableConstructor InitVals function. -func (c *evalWatchConstructor) InitVals() []Interpretable { +func (c *evalWatchConstructor) InitVals() []InterpretableV2 { return c.constructor.InitVals() } @@ -1263,11 +1493,16 @@ func (c *evalWatchConstructor) ID() int64 { return c.constructor.ID() } +// Exec implements the InterpretableV2 interface method. +func (c *evalWatchConstructor) Exec(frame *ExecutionFrame) ref.Val { + val := c.constructor.Exec(frame) + c.observer(frame, c.ID(), c.constructor, val) + return val +} + // Eval implements the Interpretable Eval function. func (c *evalWatchConstructor) Eval(vars Activation) ref.Val { - val := c.constructor.Eval(vars) - c.observer(vars, c.ID(), c.constructor, val) - return val + return c.Exec(AsFrame(vars)) } func invalidOptionalEntryInit(field any, value ref.Val) ref.Val { @@ -1279,10 +1514,10 @@ func invalidOptionalElementInit(value ref.Val) ref.Val { } // newFolder creates or initializes a pooled folder instance. -func newFolder(eval *evalFold, ctx Activation) *folder { +func newFolder(eval *evalFold, frame *ExecutionFrame) *folder { f := folderPool.Get().(*folder) f.evalFold = eval - f.activation = ctx + f.frame = frame.Push(f) return f } @@ -1303,7 +1538,7 @@ func releaseFolder(f *folder) { // cel.bind or cel.@block. type folder struct { *evalFold - activation Activation + frame *ExecutionFrame // fold state objects. accuVal ref.Val @@ -1322,16 +1557,16 @@ func (f *folder) foldIterable(iterable traits.Iterable) ref.Val { for it.HasNext() == types.True { f.iterVar1Val = it.Next() - cond := f.cond.Eval(f) + cond := f.cond.Exec(f.frame) condBool, ok := cond.(types.Bool) if f.interrupted || (!f.exhaustive && ok && condBool != types.True) { return f.evalResult() } // Update the accumulation value and check for eval interuption. - f.accuVal = f.step.Eval(f) + f.accuVal = f.step.Exec(f.frame) f.initialized = true - if f.interruptable && checkInterrupt(f.activation) { + if f.interruptable && f.frame.CheckInterrupt() { f.interrupted = true return f.evalResult() } @@ -1348,16 +1583,16 @@ func (f *folder) FoldEntry(key, val any) bool { // Terminate evaluation if evaluation is interrupted or the condition is not true and exhaustive // eval is not enabled. - cond := f.cond.Eval(f) + cond := f.cond.Exec(f.frame) condBool, ok := cond.(types.Bool) if f.interrupted || (!f.exhaustive && ok && condBool != types.True) { return false } // Update the accumulation value and check for eval interuption. - f.accuVal = f.step.Eval(f) + f.accuVal = f.step.Exec(f.frame) f.initialized = true - if f.interruptable && checkInterrupt(f.activation) { + if f.interruptable && f.frame.CheckInterrupt() { f.interrupted = true return false } @@ -1371,7 +1606,7 @@ func (f *folder) ResolveName(name string) (any, bool) { if name == f.accuVar { if !f.initialized { f.initialized = true - initVal := f.accu.Eval(f.activation) + initVal := f.accu.Exec(f.frame.parent) if !f.exhaustive { if l, isList := initVal.(traits.Lister); isList && l.Size() == types.IntZero { initVal = types.NewMutableList(f.adapter) @@ -1396,23 +1631,43 @@ func (f *folder) ResolveName(name string) (any, bool) { return f.iterVar2Val, true } } - return f.activation.ResolveName(name) + return f.frame.parent.ResolveName(name) } // Parent returns the activation embedded into the folder. func (f *folder) Parent() Activation { - return f.activation + return f.frame.parent } // Unwrap returns the parent activation, thus omitting access to local state func (f *folder) Unwrap() Activation { - return f.activation + return f.frame.parent +} + +// IsLocalVariable reports whether the variable name is locally bound by the folder scope. +func (f *folder) IsLocalVariable(name string) bool { + if name == f.accuVar { + return true + } + if !f.computeResult && (name == f.iterVar || name == f.iterVar2) { + return true + } + parent := f.Parent() + if parent == nil { + return false + } + if varHolder, ok := parent.(localVariableHolder); ok { + if varHolder.IsLocalVariable(name) { + return true + } + } + return false } // UnknownAttributePatterns implements the PartialActivation interface returning the unknown patterns // if they were provided to the input activation, or an empty set if the proxied activation is not partial. func (f *folder) UnknownAttributePatterns() []*AttributePattern { - if pv, ok := f.activation.(partialActivationConverter); ok { + if pv, ok := f.frame.parent.Activation.(partialActivationConverter); ok { if partial, isPartial := pv.AsPartialActivation(); isPartial { return partial.UnknownAttributePatterns() } @@ -1421,7 +1676,7 @@ func (f *folder) UnknownAttributePatterns() []*AttributePattern { } func (f *folder) AsPartialActivation() (PartialActivation, bool) { - if pv, ok := f.activation.(partialActivationConverter); ok { + if pv, ok := f.frame.parent.Activation.(partialActivationConverter); ok { if _, isPartial := pv.AsPartialActivation(); isPartial { return f, true } @@ -1435,7 +1690,7 @@ func (f *folder) evalResult() ref.Val { if f.interrupted { return types.WrapErr(InterruptError{}) } - res := f.result.Eval(f) + res := f.result.Exec(f.frame) // Convert a mutable list or map to an immutable one if the comprehension has generated a list or // map as a result. if !types.IsUnknownOrError(res) && f.mutableValue { @@ -1452,7 +1707,8 @@ func (f *folder) evalResult() ref.Val { // reset clears any state associated with folder evaluation. func (f *folder) reset() { f.evalFold = nil - f.activation = nil + f.frame.Pop() + f.frame = nil f.accuVal = nil f.iterVar1Val = nil f.iterVar2Val = nil @@ -1463,11 +1719,6 @@ func (f *folder) reset() { f.computeResult = false } -func checkInterrupt(a Activation) bool { - stop, found := a.ResolveName("#interrupted") - return found && stop == true -} - // InterruptError is a specialized error type used to signal that program evaluation should check // whether a context cancellation is responsible for the error. type InterruptError struct{} diff --git a/vendor/github.com/google/cel-go/interpreter/interpreter.go b/vendor/github.com/google/cel-go/interpreter/interpreter.go index d81ef128..ef13ab92 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpreter.go +++ b/vendor/github.com/google/cel-go/interpreter/interpreter.go @@ -29,11 +29,11 @@ import ( // PlannerOption configures the program plan options during interpretable setup. type PlannerOption func(*planner) (*planner, error) -// Interpreter generates a new Interpretable from a checked or unchecked expression. +// Interpreter generates a new InterpretableV2 from a checked or unchecked expression. type Interpreter interface { - // NewInterpretable creates an Interpretable from a checked expression and an + // NewInterpretable creates an InterpretableV2 from a checked expression and an // optional list of PlannerOption values. - NewInterpretable(exprAST *ast.AST, opts ...PlannerOption) (Interpretable, error) + NewInterpretable(exprAST *ast.AST, opts ...PlannerOption) (InterpretableV2, error) } // EvalObserver is a functional interface that accepts an expression id and an observed value. @@ -43,16 +43,16 @@ type EvalObserver func(vars Activation, id int64, programStep any, value ref.Val // StatefulObserver observes evaluation while tracking or utilizing stateful behavior. type StatefulObserver interface { - // InitState configures stateful metadata on the activation. - InitState(Activation) (Activation, error) + // InitState configures stateful metadata on the execution frame. + InitState(*ExecutionFrame) (any, error) - // GetState retrieves the stateful metadata from the activation. - GetState(Activation) any + // GetState retrieves the stateful metadata from the execution frame. + GetState(*ExecutionFrame) any // Observe passes the activation and relevant evaluation metadata to the observer. - // The observe method is expected to do the equivalent of GetState(vars) in order + // The observe method is expected to do the equivalent of GetState(AsFrame(activation)) // to find the metadata that needs to be updated upon invocation. - Observe(vars Activation, id int64, programStep any, value ref.Val) + Observe(Activation, int64, any, ref.Val) } // EvalCancelledError represents a cancelled program evaluation operation. @@ -106,37 +106,6 @@ func EvalStateObserver(opts ...evalStateOption) PlannerOption { } } -// evalStateConverter identifies an object which is convertible to an EvalState instance. -type evalStateConverter interface { - asEvalState() EvalState -} - -// evalStateActivation hides state in the Activation in a manner not accessible to expressions. -type evalStateActivation struct { - vars Activation - state EvalState -} - -// ResolveName proxies variable lookups to the backing activation. -func (esa evalStateActivation) ResolveName(name string) (any, bool) { - return esa.vars.ResolveName(name) -} - -// Parent proxies parent lookups to the backing activation. -func (esa evalStateActivation) Parent() Activation { - return esa.vars -} - -// AsPartialActivation supports conversion to a partial activation in order to detect unknown attributes. -func (esa evalStateActivation) AsPartialActivation() (PartialActivation, bool) { - return AsPartialActivation(esa.vars) -} - -// asEvalState implements the evalStateConverter method. -func (esa evalStateActivation) asEvalState() EvalState { - return esa.state -} - // activationWrapper identifies an object carrying local variables which should not be exposed to the user // Activations used for such purposes can be unwrapped to return the activation which omits local state. type activationWrapper interface { @@ -144,24 +113,10 @@ type activationWrapper interface { Unwrap() Activation } -// asEvalState walks the Activation hierarchy and returns the first EvalState found, if present. -func asEvalState(vars Activation) (EvalState, bool) { - if conv, ok := vars.(evalStateConverter); ok { - return conv.asEvalState(), true - } - // Check if the current activation wraps another activation. This is used to support - // wrappers such as the @block() activation which may be composed of a dynamicSlotActivation or a - // constantSlotActivation. In this case, the underlying activation is the portion which interacts - // with the EvalState. - if wrapper, ok := vars.(activationWrapper); ok { - unwrapped := wrapper.Unwrap() - // Recursively call asEvalState on the unwrapped activation. This will check the unwrapped value and its parents. - return asEvalState(unwrapped) - } - if vars.Parent() != nil { - return asEvalState(vars.Parent()) - } - return nil, false +// localVariableHolder identifies an Activation scope that holds local variables and supports testing +// whether a variable name is local to this scope. +type localVariableHolder interface { + IsLocalVariable(name string) bool } // evalStateFactory holds a reference to a factory function that produces an EvalState instance. @@ -169,32 +124,54 @@ type evalStateFactory struct { factory func() EvalState } -// InitState produces an EvalState instance and bundles it into the Activation in a way which is +// InitState produces an EvalState instance and bundles it into the ExecutionFrame in a way which is // not visible to expression evaluation. -func (et *evalStateFactory) InitState(vars Activation) (Activation, error) { +func (et *evalStateFactory) InitState(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.state != nil { + return frame.ctx.state, nil + } state := et.factory() - return evalStateActivation{vars: vars, state: state}, nil + if frame.ctx == nil { + frame.ctx = evalContextPool.Get().(*evalContext) + } + frame.ctx.state = state + return state, nil } // GetState extracts the EvalState from the Activation. -func (et *evalStateFactory) GetState(vars Activation) any { - if state, found := asEvalState(vars); found { - return state +func (et *evalStateFactory) GetState(frame *ExecutionFrame) any { + if frame.ctx == nil { + return nil } - return nil + return frame.ctx.state } // Observe records the evaluation state for a given expression node and program step. func (et *evalStateFactory) Observe(vars Activation, id int64, programStep any, val ref.Val) { - state, found := asEvalState(vars) - if !found { + frame := AsFrame(vars) + if frame.ctx == nil || frame.ctx.state == nil { return } - state.SetValue(id, val) + frame.ctx.state.SetValue(id, val) } // CustomDecorator configures a custom interpretable decorator for the program. func CustomDecorator(dec InterpretableDecorator) PlannerOption { + return func(p *planner) (*planner, error) { + dec2 := func(i InterpretableV2) (InterpretableV2, error) { + legacy, err := dec(i) + if err != nil { + return nil, err + } + return adaptToV2(legacy), nil + } + p.decorators = append(p.decorators, dec2) + return p, nil + } +} + +// CustomDecoratorV2 configures a custom V2 interpretable decorator for the program. +func CustomDecoratorV2(dec InterpretableDecoratorV2) PlannerOption { return func(p *planner) (*planner, error) { p.decorators = append(p.decorators, dec) return p, nil @@ -207,7 +184,7 @@ func CustomDecorator(dec InterpretableDecorator) PlannerOption { // provided to the decorator. This decorator is not thread-safe, and the EvalState // must be reset between Eval() calls. func ExhaustiveEval() PlannerOption { - return CustomDecorator(decDisableShortcircuits()) + return CustomDecoratorV2(decDisableShortcircuits()) } // InterruptableEval annotates comprehension loops with information that indicates they @@ -216,13 +193,13 @@ func ExhaustiveEval() PlannerOption { // The custom activation is currently managed higher up in the stack within the 'cel' package // and should not require any custom support on behalf of callers. func InterruptableEval() PlannerOption { - return CustomDecorator(decInterruptFolds()) + return CustomDecoratorV2(decInterruptFolds()) } // Optimize will pre-compute operations such as list and map construction and optimize // call arguments to set membership tests. The set of optimizations will increase over time. func Optimize() PlannerOption { - return CustomDecorator(decOptimize()) + return CustomDecoratorV2(decOptimize()) } // RegexOptimization provides a way to replace an InterpretableCall for a regex function when the @@ -247,7 +224,7 @@ type RegexOptimization struct { // CompileRegexConstants compiles regex pattern string constants at program creation time and reports any regex pattern // compile errors. func CompileRegexConstants(regexOptimizations ...*RegexOptimization) PlannerOption { - return CustomDecorator(decRegexOptimizer(regexOptimizations...)) + return CustomDecoratorV2(decRegexOptimizer(regexOptimizations...)) } type exprInterpreter struct { @@ -273,10 +250,10 @@ func NewInterpreter(dispatcher Dispatcher, attrFactory: attrFactory} } -// NewIntepretable implements the Interpreter interface method. +// NewInterpretable implements the Interpreter interface method. func (i *exprInterpreter) NewInterpretable( checked *ast.AST, - opts ...PlannerOption) (Interpretable, error) { + opts ...PlannerOption) (InterpretableV2, error) { p := newPlanner(i.dispatcher, i.provider, i.adapter, i.attrFactory, i.container, checked) var err error for _, o := range opts { diff --git a/vendor/github.com/google/cel-go/interpreter/planner.go b/vendor/github.com/google/cel-go/interpreter/planner.go index 0bc38449..396a9803 100644 --- a/vendor/github.com/google/cel-go/interpreter/planner.go +++ b/vendor/github.com/google/cel-go/interpreter/planner.go @@ -43,7 +43,7 @@ func newPlanner(disp Dispatcher, container: cont, refMap: exprAST.ReferenceMap(), typeMap: exprAST.TypeMap(), - decorators: make([]InterpretableDecorator, 0), + decorators: make([]InterpretableDecoratorV2, 0), observers: make([]StatefulObserver, 0), } } @@ -57,7 +57,7 @@ type planner struct { container *containers.Container refMap map[int64]*ast.ReferenceInfo typeMap map[int64]*types.Type - decorators []InterpretableDecorator + decorators []InterpretableDecoratorV2 observers []StatefulObserver } @@ -72,7 +72,7 @@ type planBuilder struct { // useful for layering functionality into the evaluation that is not natively understood by CEL, // such as state-tracking, expression re-write, and possibly efficient thread-safe memoization of // repeated expressions. -func (p *planner) Plan(expr ast.Expr) (Interpretable, error) { +func (p *planner) Plan(expr ast.Expr) (InterpretableV2, error) { pb := &planBuilder{planner: p, localVars: make(map[string]int)} i, err := pb.plan(expr) if err != nil { @@ -81,10 +81,10 @@ func (p *planner) Plan(expr ast.Expr) (Interpretable, error) { if len(p.observers) == 0 { return i, nil } - return &ObservableInterpretable{Interpretable: i, observers: p.observers}, nil + return &ObservableInterpretable{InterpretableV2: i, observers: p.observers}, nil } -func (p *planBuilder) plan(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) plan(expr ast.Expr) (InterpretableV2, error) { switch expr.Kind() { case ast.CallKind: return p.decorate(p.planCall(expr)) @@ -109,7 +109,7 @@ func (p *planBuilder) plan(expr ast.Expr) (Interpretable, error) { // decorate applies the InterpretableDecorator functions to the given Interpretable. // Both the Interpretable and error generated by a Plan step are accepted as arguments // for convenience. -func (p *planBuilder) decorate(i Interpretable, err error) (Interpretable, error) { +func (p *planBuilder) decorate(i InterpretableV2, err error) (InterpretableV2, error) { if err != nil { return nil, err } @@ -123,7 +123,7 @@ func (p *planBuilder) decorate(i Interpretable, err error) (Interpretable, error } // planIdent creates an Interpretable that resolves an identifier from an Activation. -func (p *planBuilder) planIdent(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planIdent(expr ast.Expr) (InterpretableV2, error) { // Establish whether the identifier is in the reference map. if identRef, found := p.refMap[expr.ID()]; found { return p.planCheckedIdent(expr.ID(), identRef) @@ -142,7 +142,7 @@ func (p *planBuilder) planIdent(expr ast.Expr) (Interpretable, error) { }, nil } -func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (Interpretable, error) { +func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (InterpretableV2, error) { // Plan a constant reference if this is the case for this simple identifier. if identRef.Value != nil { return NewConstValue(id, identRef.Value), nil @@ -171,7 +171,7 @@ func (p *planBuilder) planCheckedIdent(id int64, identRef *ast.ReferenceInfo) (I // a) selects a field from a map or proto. // b) creates a field presence test for a select within a has() macro. // c) resolves the select expression to a namespaced identifier. -func (p *planBuilder) planSelect(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planSelect(expr ast.Expr) (InterpretableV2, error) { // If the Select id appears in the reference map from the CheckedExpr proto then it is either // a namespaced identifier or enum value. if identRef, found := p.refMap[expr.ID()]; found { @@ -227,7 +227,7 @@ func (p *planBuilder) planSelect(expr ast.Expr) (Interpretable, error) { // planCall creates a callable Interpretable while specializing for common functions and invocation // patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality functions result in // optimized Interpretable values. -func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) { call := expr.AsCall() target, fnName, oName := p.resolveFunction(expr) argCount := len(call.Args()) @@ -237,7 +237,7 @@ func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) { offset++ } - args := make([]Interpretable, argCount) + args := make([]InterpretableV2, argCount) if target != nil { arg, err := p.plan(target) if err != nil { @@ -281,6 +281,10 @@ func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) { if fnDef == nil { fnDef, _ = p.disp.FindOverload(fnName) } + // Async overloads are planned into an evalAsyncFunc regardless of arity. + if fnDef != nil && fnDef.Async != nil { + return p.planCallAsync(expr, fnName, oName, fnDef, args) + } switch argCount { case 0: return p.planCallZero(expr, fnName, oName, fnDef) @@ -303,11 +307,29 @@ func (p *planBuilder) planCall(expr ast.Expr) (Interpretable, error) { } } +// planCallAsync generates an asynchronous callable Interpretable. +func (p *planBuilder) planCallAsync(expr ast.Expr, + function string, + overload string, + impl *functions.Overload, + args []InterpretableV2) (InterpretableV2, error) { + if impl == nil || impl.Async == nil { + return nil, fmt.Errorf("no such overload: %s()", function) + } + return &evalAsyncFunc{ + id: expr.ID(), + function: function, + overload: overload, + args: args, + impl: impl.Async, + }, nil +} + // planCallZero generates a zero-arity callable Interpretable. func (p *planBuilder) planCallZero(expr ast.Expr, function string, overload string, - impl *functions.Overload) (Interpretable, error) { + impl *functions.Overload) (InterpretableV2, error) { if impl == nil || impl.Function == nil { return nil, fmt.Errorf("no such overload: %s()", function) } @@ -324,7 +346,7 @@ func (p *planBuilder) planCallUnary(expr ast.Expr, function string, overload string, impl *functions.Overload, - args []Interpretable) (Interpretable, error) { + args []InterpretableV2) (InterpretableV2, error) { var fn functions.UnaryOp var trait int var nonStrict bool @@ -352,7 +374,7 @@ func (p *planBuilder) planCallBinary(expr ast.Expr, function string, overload string, impl *functions.Overload, - args []Interpretable) (Interpretable, error) { + args []InterpretableV2) (InterpretableV2, error) { var fn functions.BinaryOp var trait int var nonStrict bool @@ -381,7 +403,7 @@ func (p *planBuilder) planCallVarArgs(expr ast.Expr, function string, overload string, impl *functions.Overload, - args []Interpretable) (Interpretable, error) { + args []InterpretableV2) (InterpretableV2, error) { var fn functions.FunctionOp var trait int var nonStrict bool @@ -405,7 +427,7 @@ func (p *planBuilder) planCallVarArgs(expr ast.Expr, } // planCallEqual generates an equals (==) Interpretable. -func (p *planBuilder) planCallEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) { +func (p *planBuilder) planCallEqual(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) { return &evalEq{ id: expr.ID(), lhs: args[0], @@ -414,7 +436,7 @@ func (p *planBuilder) planCallEqual(expr ast.Expr, args []Interpretable) (Interp } // planCallNotEqual generates a not equals (!=) Interpretable. -func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []Interpretable) (Interpretable, error) { +func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) { return &evalNe{ id: expr.ID(), lhs: args[0], @@ -423,7 +445,7 @@ func (p *planBuilder) planCallNotEqual(expr ast.Expr, args []Interpretable) (Int } // planCallLogicalAnd generates a logical and (&&) Interpretable. -func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []Interpretable) (Interpretable, error) { +func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) { return &evalAnd{ id: expr.ID(), terms: args, @@ -431,7 +453,7 @@ func (p *planBuilder) planCallLogicalAnd(expr ast.Expr, args []Interpretable) (I } // planCallLogicalOr generates a logical or (||) Interpretable. -func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []Interpretable) (Interpretable, error) { +func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) { return &evalOr{ id: expr.ID(), terms: args, @@ -439,7 +461,7 @@ func (p *planBuilder) planCallLogicalOr(expr ast.Expr, args []Interpretable) (In } // planCallConditional generates a conditional / ternary (c ? t : f) Interpretable. -func (p *planBuilder) planCallConditional(expr ast.Expr, args []Interpretable) (Interpretable, error) { +func (p *planBuilder) planCallConditional(expr ast.Expr, args []InterpretableV2) (InterpretableV2, error) { cond := args[0] t := args[1] var tAttr Attribute @@ -467,7 +489,7 @@ func (p *planBuilder) planCallConditional(expr ast.Expr, args []Interpretable) ( // planCallIndex either extends an attribute with the argument to the index operation, or creates // a relative attribute based on the return of a function call or operation. -func (p *planBuilder) planCallIndex(expr ast.Expr, args []Interpretable, optional bool) (Interpretable, error) { +func (p *planBuilder) planCallIndex(expr ast.Expr, args []InterpretableV2, optional bool) (InterpretableV2, error) { op := args[0] ind := args[1] opType := p.typeMap[op.ID()] @@ -502,7 +524,7 @@ func (p *planBuilder) planCallIndex(expr ast.Expr, args []Interpretable, optiona } // planCreateList generates a list construction Interpretable. -func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planCreateList(expr ast.Expr) (InterpretableV2, error) { list := expr.AsList() optionalIndices := list.OptionalIndices() elements := list.Elements() @@ -513,7 +535,7 @@ func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) { } optionals[index] = true } - elems := make([]Interpretable, len(elements)) + elems := make([]InterpretableV2, len(elements)) for i, elem := range elements { elemVal, err := p.plan(elem) if err != nil { @@ -531,12 +553,12 @@ func (p *planBuilder) planCreateList(expr ast.Expr) (Interpretable, error) { } // planCreateStruct generates a map or object construction Interpretable. -func (p *planBuilder) planCreateMap(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planCreateMap(expr ast.Expr) (InterpretableV2, error) { m := expr.AsMap() entries := m.Entries() optionals := make([]bool, len(entries)) - keys := make([]Interpretable, len(entries)) - vals := make([]Interpretable, len(entries)) + keys := make([]InterpretableV2, len(entries)) + vals := make([]InterpretableV2, len(entries)) hasOptionals := false for i, e := range entries { entry := e.AsMapEntry() @@ -565,7 +587,7 @@ func (p *planBuilder) planCreateMap(expr ast.Expr) (Interpretable, error) { } // planCreateObj generates an object construction Interpretable. -func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planCreateStruct(expr ast.Expr) (InterpretableV2, error) { obj := expr.AsStruct() typeName, defined := p.resolveTypeName(obj.TypeName()) if !defined { @@ -574,7 +596,7 @@ func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) { objFields := obj.Fields() optionals := make([]bool, len(objFields)) fields := make([]string, len(objFields)) - vals := make([]Interpretable, len(objFields)) + vals := make([]InterpretableV2, len(objFields)) hasOptionals := false for i, f := range objFields { field := f.AsStructField() @@ -599,7 +621,7 @@ func (p *planBuilder) planCreateStruct(expr ast.Expr) (Interpretable, error) { } // planComprehension generates an Interpretable fold operation. -func (p *planBuilder) planComprehension(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planComprehension(expr ast.Expr) (InterpretableV2, error) { fold := expr.AsComprehension() accu, err := p.plan(fold.AccuInit()) if err != nil { @@ -639,7 +661,7 @@ func (p *planBuilder) planComprehension(expr ast.Expr) (Interpretable, error) { } // planConst generates a constant valued Interpretable. -func (p *planBuilder) planConst(expr ast.Expr) (Interpretable, error) { +func (p *planBuilder) planConst(expr ast.Expr) (InterpretableV2, error) { return NewConstValue(expr.ID(), expr.AsLiteral()), nil } @@ -726,7 +748,7 @@ func (p *planBuilder) resolveFunction(expr ast.Expr) (ast.Expr, string, string) // relativeAttr indicates that the attribute in this case acts as a qualifier and as such needs to // be observed to ensure that it's evaluation value is properly recorded for state tracking. -func (p *planBuilder) relativeAttr(id int64, eval Interpretable, opt bool) (InterpretableAttribute, error) { +func (p *planBuilder) relativeAttr(id int64, eval InterpretableV2, opt bool) (InterpretableAttribute, error) { eAttr, ok := eval.(InterpretableAttribute) if !ok { eAttr = &evalAttr{ diff --git a/vendor/github.com/google/cel-go/interpreter/runtimecost.go b/vendor/github.com/google/cel-go/interpreter/runtimecost.go index 6c44cd79..81e4ef63 100644 --- a/vendor/github.com/google/cel-go/interpreter/runtimecost.go +++ b/vendor/github.com/google/cel-go/interpreter/runtimecost.go @@ -62,48 +62,6 @@ func CostObserver(opts ...costTrackPlanOption) PlannerOption { } } -// costTrackerConverter identifies an object which is convertible to a CostTracker instance. -type costTrackerConverter interface { - asCostTracker() *CostTracker -} - -// costTrackActivation hides state in the Activation in a manner not accessible to expressions. -type costTrackActivation struct { - vars Activation - costTracker *CostTracker -} - -// ResolveName proxies variable lookups to the backing activation. -func (cta costTrackActivation) ResolveName(name string) (any, bool) { - return cta.vars.ResolveName(name) -} - -// Parent proxies parent lookups to the backing activation. -func (cta costTrackActivation) Parent() Activation { - return cta.vars -} - -// AsPartialActivation supports conversion to a partial activation in order to detect unknown attributes. -func (cta costTrackActivation) AsPartialActivation() (PartialActivation, bool) { - return AsPartialActivation(cta.vars) -} - -// asCostTracker implements the costTrackerConverter method. -func (cta costTrackActivation) asCostTracker() *CostTracker { - return cta.costTracker -} - -// asCostTracker walks the Activation hierarchy and returns the first cost tracker found, if present. -func asCostTracker(vars Activation) (*CostTracker, bool) { - if conv, ok := vars.(costTrackerConverter); ok { - return conv.asCostTracker(), true - } - if vars.Parent() != nil { - return asCostTracker(vars.Parent()) - } - return nil, false -} - // costTrackerFactory holds a factory for producing new CostTracker instances on each Eval call. type costTrackerFactory struct { factory func() (*CostTracker, error) @@ -111,27 +69,40 @@ type costTrackerFactory struct { // InitState produces a CostTracker and bundles it into an Activation in a way which is not visible // to expression evaluation. -func (ct *costTrackerFactory) InitState(vars Activation) (Activation, error) { +func (ct *costTrackerFactory) InitState(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.costs != nil { + return frame.ctx.costs, nil + } tracker, err := ct.factory() if err != nil { return nil, err } - return costTrackActivation{vars: vars, costTracker: tracker}, nil + if frame.ctx == nil { + frame.ctx = evalContextPool.Get().(*evalContext) + } + frame.ctx.costs = tracker + return tracker, nil } // GetState extracts the CostTracker from the Activation. -func (ct *costTrackerFactory) GetState(vars Activation) any { - if tracker, found := asCostTracker(vars); found { - return tracker +func (ct *costTrackerFactory) GetState(frame *ExecutionFrame) any { + if frame == nil || frame.ctx == nil { + return nil } - return nil + return frame.ctx.costs } // Observe computes the incremental cost of each step and records it into the CostTracker associated // with the evaluation. func (ct *costTrackerFactory) Observe(vars Activation, id int64, programStep any, val ref.Val) { - tracker, found := asCostTracker(vars) - if !found { + frame := AsFrame(vars) + state := ct.GetState(frame) + if state == nil { + return + } + tracker, ok := state.(*CostTracker) + if !ok { + // The state is configured with CostTrackFactory so this shouldn't happen. return } switch t := programStep.(type) { @@ -265,6 +236,19 @@ type CostTracker struct { stack refValStack } +// Clone makes a shallow copy of the tracker. +// The different clones can be used independently from +// each other. +func (c *CostTracker) Clone() (*CostTracker, error) { + tracker := &CostTracker{ + Estimator: c.Estimator, + overloadTrackers: c.overloadTrackers, + Limit: c.Limit, + presenceTestHasCost: c.presenceTestHasCost, + } + return tracker, nil +} + // ActualCost returns the runtime cost func (c *CostTracker) ActualCost() uint64 { return c.cost @@ -276,7 +260,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re if tracker, found := c.overloadTrackers[call.OverloadID()]; found { callCost := tracker(args, result) if callCost != nil { - cost += *callCost + cost = safeAdd(cost, *callCost) return cost } } @@ -284,7 +268,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re if c.Estimator != nil { callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), args, result) if callCost != nil { - cost += *callCost + cost = safeAdd(cost, *callCost) return cost } } @@ -292,12 +276,14 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // if user has their own implementation of ActualCostEstimator, make sure to cover the mapping between overloadId and cost calculation switch call.OverloadID() { // O(n) functions - case overloads.StartsWithString, overloads.EndsWithString, overloads.StringToBytes, overloads.BytesToString, overloads.ExtQuoteString, overloads.ExtFormatString: - cost += uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) + case overloads.StartsWithString, overloads.EndsWithString: + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[1]))*common.StringTraversalCostFactor))) + case overloads.StringToBytes, overloads.BytesToString, overloads.ExtQuoteString, overloads.ExtFormatString: + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[0]))*common.StringTraversalCostFactor))) case overloads.InList: // If a list is composed entirely of constant values this is O(1), but we don't account for that here. // We just assume all list containment checks are O(n). - cost += actualSize(args[1]) + cost = safeAdd(cost, actualSize(args[1])) // O(min(m, n)) functions case overloads.LessString, overloads.GreaterString, overloads.LessEqualsString, overloads.GreaterEqualsString, overloads.LessBytes, overloads.GreaterBytes, overloads.LessEqualsBytes, overloads.GreaterEqualsBytes, @@ -307,17 +293,14 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // of 1. lhsSize := actualSize(args[0]) rhsSize := actualSize(args[1]) - minSize := lhsSize - if rhsSize < minSize { - minSize = rhsSize - } - cost += uint64(math.Ceil(float64(minSize) * common.StringTraversalCostFactor)) + minSize := min(rhsSize, lhsSize) + cost = safeAdd(cost, uint64(math.Ceil(float64(minSize)*common.StringTraversalCostFactor))) // O(m+n) functions case overloads.AddString, overloads.AddBytes: // In the worst case scenario, we would need to reallocate a new backing store and copy both operands over. - cost += uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1])) * common.StringTraversalCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1]))*common.StringTraversalCostFactor))) // O(nm) functions - case overloads.MatchesString: + case overloads.Matches, overloads.MatchesString: // https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL // Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0 // in case where string is empty but regex is still expensive. @@ -328,11 +311,11 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // For now, we're making a guess that each expression in a regex is typically at least 4 chars // in length. regexCost := uint64(math.Ceil(float64(actualSize(args[1])) * common.RegexStringLengthCostFactor)) - cost += strCost * regexCost + cost = safeAdd(cost, strCost*regexCost) case overloads.ContainsString: strCost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) substrCost := uint64(math.Ceil(float64(actualSize(args[1])) * common.StringTraversalCostFactor)) - cost += strCost * substrCost + cost = safeAdd(cost, strCost*substrCost) default: // The following operations are assumed to have O(1) complexity. @@ -342,7 +325,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // - Computing the size of strings, byte sequences, lists and maps. // - Logical operations and all operators on fixed width scalars (comparisons, equality) // - Any functions that don't have a declared cost either here or in provided ActualCostEstimator. - cost++ + cost = safeAdd(cost, 1) } return cost @@ -397,7 +380,7 @@ func (s *refValStack) drop(ids ...int64) { // the stack. // WARNING: It is possible for multiple expressions with the same ID to exist (due to how macros are implemented) so it's // possible that a dropped ID will remain on the stack. They should be removed when IDs on the stack are popped. -func (s *refValStack) dropArgs(args []Interpretable) ([]ref.Val, bool) { +func (s *refValStack) dropArgs(args []InterpretableV2) ([]ref.Val, bool) { result := make([]ref.Val, len(args)) argloop: for nIdx := len(args) - 1; nIdx >= 0; nIdx-- { @@ -413,3 +396,21 @@ argloop: } return result, true } + +func safeAdd(x, y uint64, rest ...uint64) uint64 { + if y > 0 && x > math.MaxUint64-y { + return math.MaxUint64 + } + next := x + y + if len(rest) == 0 { + return next + } + return safeAdd(next, rest[0], rest[1:]...) +} + +func safeMul(x, y uint64) uint64 { + if y != 0 && x > math.MaxUint64/y { + return math.MaxUint64 + } + return x * y +} diff --git a/vendor/github.com/google/cel-go/parser/helper.go b/vendor/github.com/google/cel-go/parser/helper.go index f960be20..84bef80d 100644 --- a/vendor/github.com/google/cel-go/parser/helper.go +++ b/vendor/github.com/google/cel-go/parser/helper.go @@ -45,6 +45,10 @@ func (p *parserHelper) getSourceInfo() *ast.SourceInfo { return p.sourceInfo } +func (p *parserHelper) expressionCount() int64 { + return p.nextID - 1 +} + func (p *parserHelper) newLiteral(ctx any, value ref.Val) ast.Expr { return p.exprFactory.NewLiteral(p.newID(ctx), value) } diff --git a/vendor/github.com/google/cel-go/parser/options.go b/vendor/github.com/google/cel-go/parser/options.go index 4eb30f83..281021f1 100644 --- a/vendor/github.com/google/cel-go/parser/options.go +++ b/vendor/github.com/google/cel-go/parser/options.go @@ -22,6 +22,7 @@ type options struct { errorRecoveryTokenLookaheadLimit int errorRecoveryLimit int expressionSizeCodePointLimit int + maxExpressionNodeCount int macros map[string]Macro populateMacroCalls bool enableOptionalSyntax bool @@ -97,6 +98,18 @@ func ExpressionSizeCodePointLimit(expressionSizeCodePointLimit int) Option { } } +// MaxExpressionNodeCount limits the maximum number of expression nodes that may be emitted by the parser, +// including nodes created by macro expansion. +func MaxExpressionNodeCount(limit int) Option { + return func(opts *options) error { + if limit < -1 { + return fmt.Errorf("max expression node count must be greater than or equal to -1: %d", limit) + } + opts.maxExpressionNodeCount = limit + return nil + } +} + // Macros adds the given macros to the parser. func Macros(macros ...Macro) Option { return func(opts *options) error { diff --git a/vendor/github.com/google/cel-go/parser/parser.go b/vendor/github.com/google/cel-go/parser/parser.go index d1567b5f..33823354 100644 --- a/vendor/github.com/google/cel-go/parser/parser.go +++ b/vendor/github.com/google/cel-go/parser/parser.go @@ -72,6 +72,12 @@ func NewParser(opts ...Option) (*Parser, error) { if p.expressionSizeCodePointLimit == -1 { p.expressionSizeCodePointLimit = int((^uint(0)) >> 1) } + if p.maxExpressionNodeCount == 0 { + p.maxExpressionNodeCount = 100_000 + } + if p.maxExpressionNodeCount == -1 { + p.maxExpressionNodeCount = int((^uint(0)) >> 1) + } // Bool is false by default, so populateMacroCalls will be false by default return p, nil } @@ -102,6 +108,7 @@ func (p *Parser) Parse(source common.Source) (*ast.AST, *common.Errors) { helper: newParserHelper(source, fac), macros: p.macros, maxRecursionDepth: p.maxRecursionDepth, + maxExpressionNodeCount: p.maxExpressionNodeCount, errorReportingLimit: p.errorReportingLimit, errorRecoveryLimit: p.errorRecoveryLimit, errorRecoveryLookaheadTokenLimit: p.errorRecoveryTokenLookaheadLimit, @@ -319,6 +326,7 @@ type parser struct { recursionDepth int errorReports int maxRecursionDepth int + maxExpressionNodeCount int errorReportingLimit int errorRecoveryLimit int errorRecoveryLookaheadTokenLimit int @@ -964,11 +972,21 @@ func (p *parser) expandMacro(exprID int64, function string, target ast.Expr, arg return nil, false } } + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } eh := exprHelperPool.Get().(*exprHelper) defer exprHelperPool.Put(eh) eh.parserHelper = p.helper eh.id = exprID expr, err := macro.Expander()(eh, target, args) + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } // An error indicates that the macro was matched, but the arguments were not well-formed. if err != nil { loc := err.Location diff --git a/vendor/github.com/google/cel-go/parser/unparser.go b/vendor/github.com/google/cel-go/parser/unparser.go index ffd5b18e..d503a450 100644 --- a/vendor/github.com/google/cel-go/parser/unparser.go +++ b/vendor/github.com/google/cel-go/parser/unparser.go @@ -297,7 +297,7 @@ func (un *unparser) visitConstVal(val ref.Val) error { // represent the float using the minimum required digits d := strconv.FormatFloat(float64(val), 'g', -1, 64) un.str.WriteString(d) - if !strings.Contains(d, ".") { + if !strings.ContainsAny(d, ".eE") { un.str.WriteString(".0") } case types.Int: diff --git a/vendor/modules.txt b/vendor/modules.txt index fd05e2cc..cb48376b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,10 +1,11 @@ # buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 ## explicit; go 1.23 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate -# buf.build/go/protovalidate v1.2.0 -## explicit; go 1.24.0 +# buf.build/go/protovalidate v1.3.0 +## explicit; go 1.25.0 buf.build/go/protovalidate buf.build/go/protovalidate/cel +buf.build/go/protovalidate/internal/rules # cel.dev/expr v0.25.2 ## explicit; go 1.23.0 cel.dev/expr @@ -404,9 +405,10 @@ github.com/goccy/go-yaml/token # github.com/gogo/protobuf v1.3.2 ## explicit; go 1.15 github.com/gogo/protobuf/proto -# github.com/google/cel-go v0.28.0 +# github.com/google/cel-go v0.30.0 ## explicit; go 1.23.0 github.com/google/cel-go/cel +github.com/google/cel-go/cel/async github.com/google/cel-go/checker github.com/google/cel-go/checker/decls github.com/google/cel-go/common