207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
/*
|
|
Copyright 2021 The Kubernetes Authors.
|
|
|
|
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 cel
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"testing"
|
|
|
|
fuzz "github.com/google/gofuzz"
|
|
)
|
|
|
|
// TestEscaping tests that property names are escaped as expected.
|
|
func TestEscaping(t *testing.T) {
|
|
cases := []struct {
|
|
unescaped string
|
|
escaped string
|
|
unescapable bool
|
|
}{
|
|
// '.', '-', '/' and '__' are escaped since
|
|
// CEL only allows identifiers of the form: [a-zA-Z_][a-zA-Z0-9_]*
|
|
{unescaped: "a.a", escaped: "a__dot__a"},
|
|
{unescaped: "a-a", escaped: "a__dash__a"},
|
|
{unescaped: "a__a", escaped: "a__underscores__a"},
|
|
{unescaped: "a.-/__a", escaped: "a__dot____dash____slash____underscores__a"},
|
|
{unescaped: "a._a", escaped: "a__dot___a"},
|
|
{unescaped: "a__.__a", escaped: "a__underscores____dot____underscores__a"},
|
|
{unescaped: "a___a", escaped: "a__underscores___a"},
|
|
{unescaped: "a____a", escaped: "a__underscores____underscores__a"},
|
|
{unescaped: "a__dot__a", escaped: "a__underscores__dot__underscores__a"},
|
|
{unescaped: "a__underscores__a", escaped: "a__underscores__underscores__underscores__a"},
|
|
// CEL lexer RESERVED keywords must be escaped
|
|
{unescaped: "true", escaped: "__true__"},
|
|
{unescaped: "false", escaped: "__false__"},
|
|
{unescaped: "null", escaped: "__null__"},
|
|
{unescaped: "in", escaped: "__in__"},
|
|
{unescaped: "as", escaped: "__as__"},
|
|
{unescaped: "break", escaped: "__break__"},
|
|
{unescaped: "const", escaped: "__const__"},
|
|
{unescaped: "continue", escaped: "__continue__"},
|
|
{unescaped: "else", escaped: "__else__"},
|
|
{unescaped: "for", escaped: "__for__"},
|
|
{unescaped: "function", escaped: "__function__"},
|
|
{unescaped: "if", escaped: "__if__"},
|
|
{unescaped: "import", escaped: "__import__"},
|
|
{unescaped: "let", escaped: "__let__"},
|
|
{unescaped: "loop", escaped: "__loop__"},
|
|
{unescaped: "package", escaped: "__package__"},
|
|
{unescaped: "namespace", escaped: "__namespace__"},
|
|
{unescaped: "return", escaped: "__return__"},
|
|
{unescaped: "var", escaped: "__var__"},
|
|
{unescaped: "void", escaped: "__void__"},
|
|
{unescaped: "while", escaped: "__while__"},
|
|
// Not all property names are escapable
|
|
{unescaped: "@", unescapable: true},
|
|
{unescaped: "1up", unescapable: true},
|
|
{unescaped: "👑", unescapable: true},
|
|
// CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
|
|
// different namespace than function and macro names.
|
|
{unescaped: "has", escaped: "has"},
|
|
{unescaped: "all", escaped: "all"},
|
|
{unescaped: "exists", escaped: "exists"},
|
|
{unescaped: "exists_one", escaped: "exists_one"},
|
|
{unescaped: "filter", escaped: "filter"},
|
|
{unescaped: "size", escaped: "size"},
|
|
{unescaped: "contains", escaped: "contains"},
|
|
{unescaped: "startsWith", escaped: "startsWith"},
|
|
{unescaped: "endsWith", escaped: "endsWith"},
|
|
{unescaped: "matches", escaped: "matches"},
|
|
{unescaped: "duration", escaped: "duration"},
|
|
{unescaped: "timestamp", escaped: "timestamp"},
|
|
{unescaped: "getDate", escaped: "getDate"},
|
|
{unescaped: "getDayOfMonth", escaped: "getDayOfMonth"},
|
|
{unescaped: "getDayOfWeek", escaped: "getDayOfWeek"},
|
|
{unescaped: "getFullYear", escaped: "getFullYear"},
|
|
{unescaped: "getHours", escaped: "getHours"},
|
|
{unescaped: "getMilliseconds", escaped: "getMilliseconds"},
|
|
{unescaped: "getMinutes", escaped: "getMinutes"},
|
|
{unescaped: "getMonth", escaped: "getMonth"},
|
|
{unescaped: "getSeconds", escaped: "getSeconds"},
|
|
// we don't escape a single _
|
|
{unescaped: "_if", escaped: "_if"},
|
|
{unescaped: "_has", escaped: "_has"},
|
|
{unescaped: "_int", escaped: "_int"},
|
|
{unescaped: "_anything", escaped: "_anything"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.unescaped, func(t *testing.T) {
|
|
e, escapable := Escape(tc.unescaped)
|
|
if tc.unescapable {
|
|
if escapable {
|
|
t.Errorf("Expected escapable=false, but got %t", escapable)
|
|
}
|
|
return
|
|
}
|
|
if !escapable {
|
|
t.Fatalf("Expected escapable=true, but got %t", escapable)
|
|
}
|
|
if tc.escaped != e {
|
|
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
|
|
}
|
|
|
|
if !validCelIdent.MatchString(e) {
|
|
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
|
|
}
|
|
|
|
u, ok := Unescape(tc.escaped)
|
|
if !ok {
|
|
t.Fatalf("Expected %s to be escapable, but it was not", tc.escaped)
|
|
}
|
|
if tc.unescaped != u {
|
|
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, u)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnescapeMalformed(t *testing.T) {
|
|
for _, s := range []string{"__int__extra", "__illegal__"} {
|
|
t.Run(s, func(t *testing.T) {
|
|
e, ok := Unescape(s)
|
|
if ok {
|
|
t.Fatalf("Expected %s to be unescapable, but it escaped to: %s", s, e)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEscapingFuzz(t *testing.T) {
|
|
fuzzer := fuzz.New()
|
|
for i := 0; i < 1000; i++ {
|
|
var unescaped string
|
|
fuzzer.Fuzz(&unescaped)
|
|
t.Run(fmt.Sprintf("%d - '%s'", i, unescaped), func(t *testing.T) {
|
|
if len(unescaped) == 0 {
|
|
return
|
|
}
|
|
escaped, ok := Escape(unescaped)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if !validCelIdent.MatchString(escaped) {
|
|
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", unescaped, escaped)
|
|
}
|
|
u, ok := Unescape(escaped)
|
|
if !ok {
|
|
t.Fatalf("Expected %s to be unescapable, but it was not", escaped)
|
|
}
|
|
if unescaped != u {
|
|
t.Errorf("Expected %s to unescape to %s, but got %s", escaped, unescaped, u)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|