diff --git a/caching_compiler_test.go b/caching_compiler_test.go index 7f2c6b1..2be82c1 100644 --- a/caching_compiler_test.go +++ b/caching_compiler_test.go @@ -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()} diff --git a/lift.go b/lift.go index 2907d86..6deff96 100644 --- a/lift.go +++ b/lift.go @@ -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() @@ -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++ @@ -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} } diff --git a/lift_test.go b/lift_test.go index 79d4f95..18ab02a 100644 --- a/lift_test.go +++ b/lift_test.go @@ -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{}, }, } diff --git a/parser_test.go b/parser_test.go index a2b57cb..b0a35de 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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, @@ -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(),