Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions caching_compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,56 @@ func TestCachingCompile(t *testing.T) {
})
}

func TestCachingCompile_StringEscapeSemantics(t *testing.T) {
tests := []struct {
name string
expression string
value string
}{
{name: "single quote", expression: `event.value == 'it\'s'`, value: `it's`},
{name: "triple quote", expression: `event.value == """line\nbreak"""`, value: "line\nbreak"},
{name: "regex", expression: `event.value.matches("^prefix\\/suffix$")`, value: "prefix/suffix"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
env := newEnv()
input := map[string]any{
"event": map[string]any{"value": test.value},
}

directAST, directIssues := env.Compile(test.expression)
require.Nil(t, directIssues)
directProgram, err := env.Program(directAST)
require.NoError(t, err)
direct, _, err := directProgram.Eval(input)
require.NoError(t, err)
require.Equal(t, true, direct.Value())

cachedAST, cachedIssues, vars := NewCachingCompiler(env, nil).Compile(test.expression)
require.Nil(t, cachedIssues)
cachedProgram, err := env.Program(cachedAST)
require.NoError(t, err)
input[VarPrefix] = vars.Map()
cached, _, err := cachedProgram.Eval(input)
require.NoError(t, err)

require.Equal(t, direct.Value(), cached.Value())
})
}
}

func TestCachingCompile_InvalidStringEscape(t *testing.T) {
for _, expression := range []string{
`event.value == "bad\z"`,
`event.value == "trailing\`,
} {
_, issues, _ := NewCachingCompiler(newEnv(), nil).Compile(expression)
require.NotNil(t, issues)
require.Error(t, issues.Err())
}
}

func TestCachingCompile_IntegerLiteralDedup(t *testing.T) {
c := cachingCompiler{env: newEnv()}

Expand Down
19 changes: 4 additions & 15 deletions lift.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func liftLiterals(expr string) (string, LiftedArgs) {
// expression.
return expr, nil
}
if strings.ContainsRune(expr, '\\') {
// CEL must decode and validate escape syntax before it can become a variable value.
return expr, regularArgMap{}
}

lp := liftParser{expr: expr}
return lp.lift()
Expand Down Expand Up @@ -302,14 +306,6 @@ func (l *liftParser) consumeString(quoteChar byte) argMapValue {
for l.idx < len(l.expr) {
char := l.expr[l.idx]

if char == '\\' && l.idx+1 < len(l.expr) {
// Escape sequence: skip the backslash and whatever follows it.
// This correctly handles \\, \", \', \n, \t, etc.
l.idx += 2
length += 2
continue
}

if char == quoteChar {
// Skip over the end quote.
l.idx++
Expand All @@ -324,13 +320,6 @@ func (l *liftParser) consumeString(quoteChar byte) argMapValue {
length++
}

// this is a grossly invalid expr, eg: `event.data.id == "foo\"`
// in this case, we can never parse this string. we always fix this by treating the last backslash
// as a \ literal, innit bruv
if length > 0 && offset+length <= len(l.expr) && l.expr[offset+length-1] == quoteChar {
length--
}

return argMapValue{offset: offset, length: length}
}

Expand Down
31 changes: 12 additions & 19 deletions lift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,29 +174,22 @@ func TestLiftLiterals(t *testing.T) {
},
},
{
name: "escaped backslash before closing quote",
expr: `event.name == "foo\\"`,
expectedStr: "event.name == vars.a",
expectedArgs: map[string]any{
"a": `foo\\`,
},
name: "escaped backslash before closing quote",
expr: `event.name == "foo\\"`,
expectedStr: `event.name == "foo\\"`,
expectedArgs: map[string]any{},
},
{
name: "escaped backslash before closing quote in compound expression",
expr: `async.data.branch.playgroundId == "oqdqzbuppgbtrljtpbmyi\\" && "team/mly6i259eym3jkyvq6txyciu/repo/qhq2ioy772sqo9sboe48kfwv" == async.data.spaceID`,
expectedStr: `async.data.branch.playgroundId == vars.a && vars.b == async.data.spaceID`,
expectedArgs: map[string]any{
"a": `oqdqzbuppgbtrljtpbmyi\\`,
"b": "team/mly6i259eym3jkyvq6txyciu/repo/qhq2ioy772sqo9sboe48kfwv",
},
name: "escaped backslash before closing quote in compound expression",
expr: `async.data.branch.playgroundId == "oqdqzbuppgbtrljtpbmyi\\" && "team/mly6i259eym3jkyvq6txyciu/repo/qhq2ioy772sqo9sboe48kfwv" == async.data.spaceID`,
expectedStr: `async.data.branch.playgroundId == "oqdqzbuppgbtrljtpbmyi\\" && "team/mly6i259eym3jkyvq6txyciu/repo/qhq2ioy772sqo9sboe48kfwv" == async.data.spaceID`,
expectedArgs: map[string]any{},
},
{
name: "trailing escaped quote treated as literal backslash",
expr: `event.data.id == "foo\"`,
expectedStr: `event.data.id == vars.a`,
expectedArgs: map[string]any{
"a": `foo\`,
},
name: "unterminated escaped quote not lifted",
expr: `event.data.id == "foo\"`,
expectedStr: `event.data.id == "foo\"`,
expectedArgs: map[string]any{},
},
}

Expand Down
4 changes: 2 additions & 2 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ func TestParsedCELAST(t *testing.T) {
require.Nil(t, iss)
require.NotNil(t, ast)
require.NotNil(t, args)
require.EqualValues(t, map[string]any{"a": `ok\" please`}, args.Map())
require.Empty(t, args.Map())

program, err := env.Program(
ast,
Expand All @@ -1293,7 +1293,7 @@ func TestParsedCELAST(t *testing.T) {
result, _, err := program.Eval(map[string]any{
"event": map[string]any{
"data": map[string]any{
"id": `ok\" please`,
"id": `ok" please`,
},
},
"vars": args.Map(),
Expand Down