Skip to content

Commit c548e8f

Browse files
committed
Add expr.MaxNodes() option
1 parent 5fbfe72 commit c548e8f

File tree

5 files changed

+73
-45
lines changed

5 files changed

+73
-45
lines changed

conf/config.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,41 @@ import (
1010
"github.com/expr-lang/expr/vm/runtime"
1111
)
1212

13-
const (
13+
var (
1414
// DefaultMemoryBudget represents an upper limit of memory usage
1515
DefaultMemoryBudget uint = 1e6
1616

1717
// DefaultMaxNodes represents an upper limit of AST nodes
18-
DefaultMaxNodes uint = 10000
18+
DefaultMaxNodes uint = 1e4
1919
)
2020

2121
type FunctionsTable map[string]*builtin.Function
2222

2323
type Config struct {
24-
EnvObject any
25-
Env nature.Nature
26-
Expect reflect.Kind
27-
ExpectAny bool
28-
Optimize bool
29-
Strict bool
30-
Profile bool
31-
MaxNodes uint
32-
MemoryBudget uint
33-
ConstFns map[string]reflect.Value
34-
Visitors []ast.Visitor
35-
Functions FunctionsTable
36-
Builtins FunctionsTable
37-
Disabled map[string]bool // disabled builtins
24+
EnvObject any
25+
Env nature.Nature
26+
Expect reflect.Kind
27+
ExpectAny bool
28+
Optimize bool
29+
Strict bool
30+
Profile bool
31+
MaxNodes uint
32+
ConstFns map[string]reflect.Value
33+
Visitors []ast.Visitor
34+
Functions FunctionsTable
35+
Builtins FunctionsTable
36+
Disabled map[string]bool // disabled builtins
3837
}
3938

4039
// CreateNew creates new config with default values.
4140
func CreateNew() *Config {
4241
c := &Config{
43-
Optimize: true,
44-
MaxNodes: DefaultMaxNodes,
45-
MemoryBudget: DefaultMemoryBudget,
46-
ConstFns: make(map[string]reflect.Value),
47-
Functions: make(map[string]*builtin.Function),
48-
Builtins: make(map[string]*builtin.Function),
49-
Disabled: make(map[string]bool),
42+
Optimize: true,
43+
MaxNodes: DefaultMaxNodes,
44+
ConstFns: make(map[string]reflect.Value),
45+
Functions: make(map[string]*builtin.Function),
46+
Builtins: make(map[string]*builtin.Function),
47+
Disabled: make(map[string]bool),
5048
}
5149
for _, f := range builtin.Builtins {
5250
c.Builtins[f.Name] = f

expr.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ func Timezone(name string) Option {
195195
})
196196
}
197197

198+
// MaxNodes sets the maximum number of nodes allowed in the expression.
199+
// By default, the maximum number of nodes is conf.DefaultMaxNodes.
200+
func MaxNodes(n uint) Option {
201+
return func(c *conf.Config) {
202+
c.MaxNodes = n
203+
}
204+
}
205+
198206
// Compile parses and compiles given input expression to bytecode program.
199207
func Compile(input string, ops ...Option) (*vm.Program, error) {
200208
config := conf.CreateNew()

expr_test.go

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/expr-lang/expr/internal/testify/assert"
1414
"github.com/expr-lang/expr/internal/testify/require"
1515
"github.com/expr-lang/expr/types"
16+
"github.com/expr-lang/expr/vm"
1617

1718
"github.com/expr-lang/expr"
1819
"github.com/expr-lang/expr/ast"
@@ -2225,26 +2226,6 @@ func TestEval_slices_out_of_bound(t *testing.T) {
22252226
}
22262227
}
22272228

2228-
func TestMemoryBudget(t *testing.T) {
2229-
tests := []struct {
2230-
code string
2231-
}{
2232-
{`map(1..100, {map(1..100, {map(1..100, {0})})})`},
2233-
{`len(1..10000000)`},
2234-
}
2235-
2236-
for _, tt := range tests {
2237-
t.Run(tt.code, func(t *testing.T) {
2238-
program, err := expr.Compile(tt.code)
2239-
require.NoError(t, err, "compile error")
2240-
2241-
_, err = expr.Run(program, nil)
2242-
assert.Error(t, err, "run error")
2243-
assert.Contains(t, err.Error(), "memory budget exceeded")
2244-
})
2245-
}
2246-
}
2247-
22482229
func TestExpr_custom_tests(t *testing.T) {
22492230
f, err := os.Open("custom_tests.json")
22502231
if os.IsNotExist(err) {
@@ -2731,3 +2712,45 @@ func TestIssue785_get_nil(t *testing.T) {
27312712
})
27322713
}
27332714
}
2715+
2716+
func TestMaxNodes(t *testing.T) {
2717+
maxNodes := uint(100)
2718+
2719+
code := ""
2720+
for i := 0; i < int(maxNodes); i++ {
2721+
code += "1; "
2722+
}
2723+
2724+
_, err := expr.Compile(code, expr.MaxNodes(maxNodes))
2725+
require.Error(t, err)
2726+
assert.Contains(t, err.Error(), "exceeds maximum allowed nodes")
2727+
2728+
_, err = expr.Compile(code, expr.MaxNodes(maxNodes+1))
2729+
require.NoError(t, err)
2730+
}
2731+
2732+
func TestMemoryBudget(t *testing.T) {
2733+
tests := []struct {
2734+
code string
2735+
max int
2736+
}{
2737+
{`map(1..100, {map(1..100, {map(1..100, {0})})})`, -1},
2738+
{`len(1..10000000)`, -1},
2739+
{`1..100`, 100},
2740+
}
2741+
2742+
for _, tt := range tests {
2743+
t.Run(tt.code, func(t *testing.T) {
2744+
program, err := expr.Compile(tt.code)
2745+
require.NoError(t, err, "compile error")
2746+
2747+
vm := vm.VM{}
2748+
if tt.max > 0 {
2749+
vm.MemoryBudget = uint(tt.max)
2750+
}
2751+
_, err = vm.Run(program, nil)
2752+
require.Error(t, err, "run error")
2753+
assert.Contains(t, err.Error(), "memory budget exceeded")
2754+
})
2755+
}
2756+
}

parser/parser_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,6 @@ func TestNodeBudgetDisabled(t *testing.T) {
12401240
_, err := parser.ParseWithConfig(expr, config)
12411241

12421242
if err != nil && strings.Contains(err.Error(), "exceeds maximum allowed nodes") {
1243-
t.Error("Node budget check should be disabled when MaxNodes is 0")
1243+
t.Error("Node budget check should be disabled when DefaultMaxNodes is 0")
12441244
}
12451245
}

vm/vm.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
7575
if len(vm.Variables) < program.variables {
7676
vm.Variables = make([]any, program.variables)
7777
}
78-
7978
if vm.MemoryBudget == 0 {
8079
vm.MemoryBudget = conf.DefaultMemoryBudget
8180
}

0 commit comments

Comments
 (0)