diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go index 809681abc8b..a6656256965 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values_test.go @@ -619,3 +619,47 @@ func TestMapper(t *testing.T) { }) } } + +func BenchmarkUnstructuredToVal(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + if val := UnstructuredToVal([]interface{}{ + map[string]interface{}{ + "key": "a", + "val": 1, + }, + map[string]interface{}{ + "key": "b", + "val": 2, + }, + map[string]interface{}{ + "key": "@b", + "val": 2, + }, + }, &mapListSchema); val == nil { + b.Fatal(val) + } + } +} + +func BenchmarkUnstructuredToValWithEscape(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + if val := UnstructuredToVal([]interface{}{ + map[string]interface{}{ + "key": "a.1", + "val": "__i.1", + }, + map[string]interface{}{ + "key": "b.1", + "val": 2, + }, + }, &mapListSchema); val == nil { + b.Fatal(val) + } + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping.go index 2f841b58bfc..60571202d75 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping.go @@ -36,6 +36,57 @@ var celReservedSymbols = sets.NewString( // expandMatcher matches the escape sequence, characters that are escaped, and characters that are unsupported var expandMatcher = regexp.MustCompile(`(__|[-./]|[^a-zA-Z0-9-./_])`) +// newCharacterFilter returns a boolean array to indicate the allowed characters +func newCharacterFilter(characters string) []bool { + maxChar := 0 + for _, c := range characters { + if maxChar < int(c) { + maxChar = int(c) + } + } + filter := make([]bool, maxChar+1) + + for _, c := range characters { + filter[int(c)] = true + } + + return filter +} + +type escapeCheck struct { + canSkipRegex bool + invalidCharFound bool +} + +// skipRegexCheck checks if escape would be skipped. +// if invalidCharFound is true, it must have invalid character; if invalidCharFound is false, not sure if it has invalid character or not +func skipRegexCheck(ident string) escapeCheck { + escapeCheck := escapeCheck{canSkipRegex: true, invalidCharFound: false} + // skip escape if possible + previous_underscore := false + for _, c := range ident { + if c == '/' || c == '-' || c == '.' { + escapeCheck.canSkipRegex = false + return escapeCheck + } + intc := int(c) + if intc < 0 || intc >= len(validCharacterFilter) || !validCharacterFilter[intc] { + escapeCheck.invalidCharFound = true + return escapeCheck + } + if c == '_' && previous_underscore { + escapeCheck.canSkipRegex = false + return escapeCheck + } + + previous_underscore = c == '_' + } + return escapeCheck +} + +// validCharacterFilter indicates the allowed characters. +var validCharacterFilter = newCharacterFilter("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") + // Escape escapes ident and returns a CEL identifier (of the form '[a-zA-Z_][a-zA-Z0-9_]*'), or returns // false if the ident does not match the supported input format of `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*`. // Escaping Rules: @@ -53,6 +104,15 @@ func Escape(ident string) (string, bool) { if celReservedSymbols.Has(ident) { return "__" + ident + "__", true } + + escapeCheck := skipRegexCheck(ident) + if escapeCheck.invalidCharFound { + return "", false + } + if escapeCheck.canSkipRegex { + return ident, true + } + ok := true ident = expandMatcher.ReplaceAllStringFunc(ident, func(s string) string { switch s { diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping_test.go index 04f26b639f1..699da05bf24 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/escaping_test.go @@ -170,3 +170,37 @@ func TestEscapingFuzz(t *testing.T) { } var validCelIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + +func TestCanSkipRegex(t *testing.T) { + cases := []struct { + unescaped string + canSkip bool + invalidCharFound bool + }{ + {unescaped: "a.a", canSkip: false}, + {unescaped: "a-a", canSkip: false}, + {unescaped: "a__a", canSkip: false}, + {unescaped: "a.-/__a", canSkip: false}, + {unescaped: "a_a", canSkip: true}, + {unescaped: "a_a_a", canSkip: true}, + {unescaped: "@", invalidCharFound: true}, + {unescaped: "👑", invalidCharFound: true}, + // if escape keyword is detected before invalid character, invalidCharFound + {unescaped: "/👑", canSkip: false}, + } + + for _, tc := range cases { + t.Run(tc.unescaped, func(t *testing.T) { + escapeCheck := skipRegexCheck(tc.unescaped) + if escapeCheck.invalidCharFound { + if escapeCheck.invalidCharFound != tc.invalidCharFound { + t.Errorf("Expected input validation to be %v, but got %t", tc.invalidCharFound, escapeCheck.invalidCharFound) + } + } else { + if escapeCheck.canSkipRegex != tc.canSkip { + t.Errorf("Expected %v, but got %t", tc.canSkip, escapeCheck.canSkipRegex) + } + } + }) + } +}