detached e17d561

feat(expr)!: add context.Context, an output budget and safety caps to evaluation

commit
e17d5613d156960f1d0b42dac2e4df8ec54d0a9d
author
brice · · 2026-10-12 15:12 UTC
parent
a1598a3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +652 -270
M expr/eval.go +162 -35
@@ -3,15 +3,35 @@
33 package expr
44
55 import (
6+ "context"
7+ "errors"
8+ "fmt"
69 "math"
710 "sort"
811 )
912
13+// MaxResultBytes is the default budget of an evaluation: the total size, in
14+// bytes, of the strings that format, join and toJSON build plus a nominal
15+// cost for every element of the arrays that the * filter builds. The
16+// reference implementation has no such limit; without one a few kilobytes of
17+// expression can allocate gigabytes (format('{0}{0}{0}...', format(...))),
18+// and arrays shared in a context can multiply under .* filters.
19+const MaxResultBytes = 1 << 20
20+
21+// filterElementCost is what one element of a filtered array costs against the
22+// budget: the size of an interface value.
23+const filterElementCost = 16
24+
25+// maxEvalDepth bounds the recursion of the evaluator. The parser never
26+// produces trees anywhere near it; it protects against hand-built ASTs.
27+const maxEvalDepth = 1000
28+
1029 // Func implements a function callable from expressions. It receives the
11-// evaluated arguments in order and returns the result or an error, which
12-// [Evaluate] returns as is. Arity is not checked at call time: declare it in
13-// a [FuncInfo] given to [Parse].
14-type Func func(args []Value) (Value, error)
30+// evaluation's context, which it should honour if it blocks or loops, and the
31+// evaluated arguments in order, and returns the result or an error.
32+// [Evaluate] wraps the error in an [*EvalError] that unwraps to it. Arity is
33+// not checked at call time: declare it in a [FuncInfo] given to [Parse].
34+type Func func(ctx context.Context, args []Value) (Value, error)
1535
1636 // EvalContext is the environment an expression is evaluated in.
1737 type EvalContext struct {
@@ -25,38 +45,55 @@ type EvalContext struct {
2545 // case-insensitively. As in the reference implementation these take
2646 // precedence over the built-in functions.
2747 Funcs map[string]Func
48+ // MaxResultBytes is the output budget of the evaluation, see
49+ // [MaxResultBytes]. Zero means the default; a negative value removes the
50+ // limit. The budget is shared by the whole evaluation, not per call.
51+ MaxResultBytes int
2852 }
2953
3054 // Evaluate computes the value of a parsed expression.
3155 //
32-// Values of the context are never modified, and results may share them with
33-// the context: treat the result as read-only. The error is an [*EvalError].
34-func Evaluate(node Node, ctx EvalContext) (Value, error) {
56+// Evaluation stops with an error that wraps ctx.Err() when ctx is done, and
57+// with an [*EvalError] when the output budget is exceeded. Values of the
58+// context are never modified, and results may share them with the context:
59+// treat the result as read-only. The error is an [*EvalError].
60+func Evaluate(ctx context.Context, node Node, ec EvalContext) (Value, error) {
61+ if ctx == nil {
62+ ctx = context.Background()
63+ }
3564 if node == nil {
3665 return nil, &EvalError{Message: "cannot evaluate an empty expression"}
3766 }
67+ if err := ctx.Err(); err != nil {
68+ return nil, cancelError(err)
69+ }
3870 ev := &evaluator{
39- contexts: make(map[string]Value, len(ctx.Contexts)),
40- funcs: make(map[string]Func, len(ctx.Funcs)),
71+ ctx: ctx,
72+ contexts: make(map[string]Value, len(ec.Contexts)),
73+ funcs: make(map[string]Func, len(ec.Funcs)),
74+ limit: ec.MaxResultBytes,
75+ }
76+ if ev.limit == 0 {
77+ ev.limit = MaxResultBytes
4178 }
42- names := make([]string, 0, len(ctx.Contexts))
43- for k := range ctx.Contexts {
79+ names := make([]string, 0, len(ec.Contexts))
80+ for k := range ec.Contexts {
4481 names = append(names, k)
4582 }
4683 sort.Strings(names)
4784 for _, k := range names {
4885 if _, dup := ev.contexts[foldKey(k)]; !dup {
49- ev.contexts[foldKey(k)] = ctx.Contexts[k]
86+ ev.contexts[foldKey(k)] = ec.Contexts[k]
5087 }
5188 }
5289 names = names[:0]
53- for k := range ctx.Funcs {
90+ for k := range ec.Funcs {
5491 names = append(names, k)
5592 }
5693 sort.Strings(names)
5794 for _, k := range names {
5895 if _, dup := ev.funcs[foldKey(k)]; !dup {
59- ev.funcs[foldKey(k)] = ctx.Funcs[k]
96+ ev.funcs[foldKey(k)] = ec.Funcs[k]
6097 }
6198 }
6299 v, err := ev.eval(node)
@@ -66,15 +103,60 @@ func Evaluate(node Node, ctx EvalContext) (Value, error) {
66103 return normalize(v), nil
67104 }
68105
106+func cancelError(err error) *EvalError {
107+ return &EvalError{Message: "evaluation canceled: " + err.Error(), Err: err}
108+}
109+
69110 type evaluator struct {
111+ ctx context.Context
70112 contexts map[string]Value
71113 funcs map[string]Func
114+ limit int // output budget; negative: unlimited
115+ used int
116+ depth int
117+ ticks int
118+}
119+
120+// tick is called in loops and on every node: it checks the context once in a
121+// while.
122+func (e *evaluator) tick() error {
123+ e.ticks++
124+ if e.ticks&63 == 0 {
125+ if err := e.ctx.Err(); err != nil {
126+ return cancelError(err)
127+ }
128+ }
129+ return nil
130+}
131+
132+// charge spends n bytes of the output budget.
133+func (e *evaluator) charge(n int) error {
134+ if e.limit < 0 {
135+ return nil
136+ }
137+ if n > e.limit-e.used { // written to be overflow-proof
138+ return e.budgetError()
139+ }
140+ e.used += n
141+ return nil
142+}
143+
144+func (e *evaluator) budgetError() *EvalError {
145+ return &EvalError{Message: fmt.Sprintf("expression output exceeds the limit of %d bytes", e.limit)}
72146 }
73147
74148 func (e *evaluator) eval(n Node) (Value, error) {
149+ if err := e.tick(); err != nil {
150+ return nil, err
151+ }
152+ e.depth++
153+ defer func() { e.depth-- }()
154+ if e.depth > maxEvalDepth {
155+ return nil, &EvalError{Message: fmt.Sprintf("expression tree deeper than %d levels", maxEvalDepth)}
156+ }
75157 switch n := n.(type) {
76158 case *Literal:
77- return n.Value, nil
159+ return normalize(n.Value), nil
78160 case *Unary:
79161 r, err := e.eval(n.Expr)
80162 if err != nil {
@@ -143,10 +225,30 @@ func (e *evaluator) logical(l *Logical) (Value, error) {
143225 break
144226 }
145227 }
228+ if result == nil {
229+ return Null{}, nil // a Logical without operands, possible only in a hand-built tree
230+ }
146231 return result, nil
147232 }
148233
149234 func (e *evaluator) call(c *FunctionCall) (Value, error) {
235+ name := foldKey(c.Name.Lexeme)
236+ ext, isExt := e.funcs[name]
237+ isExt = isExt && ext != nil
238+ var impl builtinFunc
239+ if !isExt {
240+ // Arity of the built-ins is checked by the parser, and again here
241+ // so that a hand-built tree cannot reach an implementation with the
242+ // wrong number of arguments.
243+ f, ok := builtinFuncs[name]
244+ if !ok {
245+ return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme)
246+ }
247+ if t, bad := arityError(name, f, len(c.Args)); bad {
248+ return nil, &EvalError{Message: t.description() + ": '" + c.Name.Lexeme + "'"}
249+ }
250+ impl = builtinImpls[name]
251+ }
150252 args := make([]Value, len(c.Args))
151253 for i, a := range c.Args {
152254 v, err := e.eval(a)
@@ -155,14 +257,18 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) {
155257 }
156258 args[i] = v
157259 }
158- name := foldKey(c.Name.Lexeme)
159- if f, ok := e.funcs[name]; ok && f != nil {
160- return f(args)
161- }
162- if f, ok := builtinImpls[name]; ok {
163- return f(args)
260+ if isExt {
261+ v, err := ext(e.ctx, args)
262+ if err != nil {
263+ var ee *EvalError
264+ if errors.As(err, &ee) {
265+ return nil, err
266+ }
267+ return nil, &EvalError{Message: err.Error(), Err: err}
268+ }
269+ return v, nil
164270 }
165- return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme)
271+ return impl(e, args)
166272 }
167273
168274 // indexer is idxHelper of the reference: how an index value is read, as a
@@ -221,11 +327,11 @@ func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) {
221327 switch o := normalize(obj).(type) {
222328 case *Array:
223329 if o.filtered {
224- return filteredArrayAccess(o, ix), nil
330+ return e.filteredArrayAccess(o, ix)
225331 }
226- return arrayAccess(o, ix), nil
332+ return e.arrayAccess(o, ix)
227333 case *Dictionary:
228- return objectAccess(o, ix), nil
334+ return e.objectAccess(o, ix)
229335 }
230336 if ix.star {
231337 return &Array{filtered: true}, nil
@@ -235,49 +341,70 @@ func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) {
235341
236342 // filteredArrayAccess applies an index to each element of a filtered array
237343 // and collects the hits: "a.*.b" is the b of every element that has one.
238-func filteredArrayAccess(fa *Array, ix indexer) *Array {
344+func (e *evaluator) filteredArrayAccess(fa *Array, ix indexer) (Value, error) {
239345 result := &Array{filtered: true}
240346 for _, item := range fa.items {
347+ if err := e.tick(); err != nil {
348+ return nil, err
349+ }
241350 switch it := normalize(item).(type) {
242351 case *Dictionary:
243352 if ix.star {
353+ if err := e.charge(len(it.vals) * filterElementCost); err != nil {
354+ return nil, err
355+ }
244356 result.items = append(result.items, it.vals...)
245357 } else if ix.hasStr {
246358 if v, ok := it.Get(ix.str); ok {
359+ if err := e.charge(filterElementCost); err != nil {
360+ return nil, err
361+ }
247362 result.items = append(result.items, v)
248363 }
249364 }
250365 case *Array:
251366 if ix.star {
367+ if err := e.charge(len(it.items) * filterElementCost); err != nil {
368+ return nil, err
369+ }
252370 result.items = append(result.items, it.items...)
253371 } else if i, ok := ix.pos(len(it.items)); ok {
372+ if err := e.charge(filterElementCost); err != nil {
373+ return nil, err
374+ }
254375 result.items = append(result.items, it.items[i])
255376 }
256377 }
257378 }
258- return result
379+ return result, nil
259380 }
260381
261-func arrayAccess(a *Array, ix indexer) Value {
382+func (e *evaluator) arrayAccess(a *Array, ix indexer) (Value, error) {
262383 if ix.star {
263- return &Array{items: append([]Value(nil), a.items...), filtered: true}
384+ if err := e.charge(len(a.items) * filterElementCost); err != nil {
385+ return nil, err
386+ }
387+ return &Array{items: append([]Value(nil), a.items...), filtered: true}, nil
264388 }
265389 if i, ok := ix.pos(len(a.items)); ok {
266- return normalize(a.items[i])
390+ return normalize(a.items[i]), nil
267391 }
268- return Null{}
392+ return Null{}, nil
269393 }
270394
271-func objectAccess(d *Dictionary, ix indexer) Value {
395+func (e *evaluator) objectAccess(d *Dictionary, ix indexer) (Value, error) {
272396 if ix.star {
273- return &Array{items: append([]Value(nil), d.vals...), filtered: true}
397+ if err := e.charge(len(d.vals) * filterElementCost); err != nil {
398+ return nil, err
399+ }
400+ return &Array{items: append([]Value(nil), d.vals...), filtered: true}, nil
274401 }
275402 if ix.hasStr {
276403 if v, ok := d.Get(ix.str); ok {
277- return v
404+ return v, nil
278405 }
279406 }
280- return Null{}
407+ return Null{}, nil
281408 }
282409
283410 // coerceTypes converts two values of different kinds to a common kind the
@@ -3,15 +3,35 @@
3 package expr3 package expr
4 4
5 import (5 import (
6+ "context"
7+ "errors"
8+ "fmt"
6 "math"9 "math"
7 "sort"10 "sort"
8 )11 )
9 12
13+// MaxResultBytes is the default budget of an evaluation: the total size, in
14+// bytes, of the strings that format, join and toJSON build plus a nominal
15+// cost for every element of the arrays that the * filter builds. The
16+// reference implementation has no such limit; without one a few kilobytes of
17+// expression can allocate gigabytes (format('{0}{0}{0}...', format(...))),
18+// and arrays shared in a context can multiply under .* filters.
19+const MaxResultBytes = 1 << 20
20+
21+// filterElementCost is what one element of a filtered array costs against the
22+// budget: the size of an interface value.
23+const filterElementCost = 16
24+
25+// maxEvalDepth bounds the recursion of the evaluator. The parser never
26+// produces trees anywhere near it; it protects against hand-built ASTs.
27+const maxEvalDepth = 1000
28+
10 // Func implements a function callable from expressions. It receives the29 // Func implements a function callable from expressions. It receives the
11-// evaluated arguments in order and returns the result or an error, which30+// evaluation's context, which it should honour if it blocks or loops, and the
12-// [Evaluate] returns as is. Arity is not checked at call time: declare it in31+// evaluated arguments in order, and returns the result or an error.
13-// a [FuncInfo] given to [Parse].32+// [Evaluate] wraps the error in an [*EvalError] that unwraps to it. Arity is
14-type Func func(args []Value) (Value, error)33+// not checked at call time: declare it in a [FuncInfo] given to [Parse].
34+type Func func(ctx context.Context, args []Value) (Value, error)
15 35
16 // EvalContext is the environment an expression is evaluated in.36 // EvalContext is the environment an expression is evaluated in.
17 type EvalContext struct {37 type EvalContext struct {
@@ -25,38 +45,55 @@ type EvalContext struct {
25 // case-insensitively. As in the reference implementation these take45 // case-insensitively. As in the reference implementation these take
26 // precedence over the built-in functions.46 // precedence over the built-in functions.
27 Funcs map[string]Func47 Funcs map[string]Func
48+ // MaxResultBytes is the output budget of the evaluation, see
49+ // [MaxResultBytes]. Zero means the default; a negative value removes the
50+ // limit. The budget is shared by the whole evaluation, not per call.
51+ MaxResultBytes int
28 }52 }
29 53
30 // Evaluate computes the value of a parsed expression.54 // Evaluate computes the value of a parsed expression.
31 //55 //
32-// Values of the context are never modified, and results may share them with56+// Evaluation stops with an error that wraps ctx.Err() when ctx is done, and
33-// the context: treat the result as read-only. The error is an [*EvalError].57+// with an [*EvalError] when the output budget is exceeded. Values of the
34-func Evaluate(node Node, ctx EvalContext) (Value, error) {58+// context are never modified, and results may share them with the context:
59+// treat the result as read-only. The error is an [*EvalError].
60+func Evaluate(ctx context.Context, node Node, ec EvalContext) (Value, error) {
61+ if ctx == nil {
62+ ctx = context.Background()
63+ }
35 if node == nil {64 if node == nil {
36 return nil, &EvalError{Message: "cannot evaluate an empty expression"}65 return nil, &EvalError{Message: "cannot evaluate an empty expression"}
37 }66 }
67+ if err := ctx.Err(); err != nil {
68+ return nil, cancelError(err)
69+ }
38 ev := &evaluator{70 ev := &evaluator{
39- contexts: make(map[string]Value, len(ctx.Contexts)),71+ ctx: ctx,
40- funcs: make(map[string]Func, len(ctx.Funcs)),72+ contexts: make(map[string]Value, len(ec.Contexts)),
73+ funcs: make(map[string]Func, len(ec.Funcs)),
74+ limit: ec.MaxResultBytes,
75+ }
76+ if ev.limit == 0 {
77+ ev.limit = MaxResultBytes
41 }78 }
42- names := make([]string, 0, len(ctx.Contexts))79+ names := make([]string, 0, len(ec.Contexts))
43- for k := range ctx.Contexts {80+ for k := range ec.Contexts {
44 names = append(names, k)81 names = append(names, k)
45 }82 }
46 sort.Strings(names)83 sort.Strings(names)
47 for _, k := range names {84 for _, k := range names {
48 if _, dup := ev.contexts[foldKey(k)]; !dup {85 if _, dup := ev.contexts[foldKey(k)]; !dup {
49- ev.contexts[foldKey(k)] = ctx.Contexts[k]86+ ev.contexts[foldKey(k)] = ec.Contexts[k]
50 }87 }
51 }88 }
52 names = names[:0]89 names = names[:0]
53- for k := range ctx.Funcs {90+ for k := range ec.Funcs {
54 names = append(names, k)91 names = append(names, k)
55 }92 }
56 sort.Strings(names)93 sort.Strings(names)
57 for _, k := range names {94 for _, k := range names {
58 if _, dup := ev.funcs[foldKey(k)]; !dup {95 if _, dup := ev.funcs[foldKey(k)]; !dup {
59- ev.funcs[foldKey(k)] = ctx.Funcs[k]96+ ev.funcs[foldKey(k)] = ec.Funcs[k]
60 }97 }
61 }98 }
62 v, err := ev.eval(node)99 v, err := ev.eval(node)
@@ -66,15 +103,60 @@ func Evaluate(node Node, ctx EvalContext) (Value, error) {
66 return normalize(v), nil103 return normalize(v), nil
67 }104 }
68 105
106+func cancelError(err error) *EvalError {
107+ return &EvalError{Message: "evaluation canceled: " + err.Error(), Err: err}
108+}
109+
69 type evaluator struct {110 type evaluator struct {
111+ ctx context.Context
70 contexts map[string]Value112 contexts map[string]Value
71 funcs map[string]Func113 funcs map[string]Func
114+ limit int // output budget; negative: unlimited
115+ used int
116+ depth int
117+ ticks int
118+}
119+
120+// tick is called in loops and on every node: it checks the context once in a
121+// while.
122+func (e *evaluator) tick() error {
123+ e.ticks++
124+ if e.ticks&63 == 0 {
125+ if err := e.ctx.Err(); err != nil {
126+ return cancelError(err)
127+ }
128+ }
129+ return nil
130+}
131+
132+// charge spends n bytes of the output budget.
133+func (e *evaluator) charge(n int) error {
134+ if e.limit < 0 {
135+ return nil
136+ }
137+ if n > e.limit-e.used { // written to be overflow-proof
138+ return e.budgetError()
139+ }
140+ e.used += n
141+ return nil
142+}
143+
144+func (e *evaluator) budgetError() *EvalError {
145+ return &EvalError{Message: fmt.Sprintf("expression output exceeds the limit of %d bytes", e.limit)}
72 }146 }
73 147
74 func (e *evaluator) eval(n Node) (Value, error) {148 func (e *evaluator) eval(n Node) (Value, error) {
149+ if err := e.tick(); err != nil {
150+ return nil, err
151+ }
152+ e.depth++
153+ defer func() { e.depth-- }()
154+ if e.depth > maxEvalDepth {
155+ return nil, &EvalError{Message: fmt.Sprintf("expression tree deeper than %d levels", maxEvalDepth)}
156+ }
75 switch n := n.(type) {157 switch n := n.(type) {
76 case *Literal:158 case *Literal:
77- return n.Value, nil159+ return normalize(n.Value), nil
78 case *Unary:160 case *Unary:
79 r, err := e.eval(n.Expr)161 r, err := e.eval(n.Expr)
80 if err != nil {162 if err != nil {
@@ -143,10 +225,30 @@ func (e *evaluator) logical(l *Logical) (Value, error) {
143 break225 break
144 }226 }
145 }227 }
228+ if result == nil {
229+ return Null{}, nil // a Logical without operands, possible only in a hand-built tree
230+ }
146 return result, nil231 return result, nil
147 }232 }
148 233
149 func (e *evaluator) call(c *FunctionCall) (Value, error) {234 func (e *evaluator) call(c *FunctionCall) (Value, error) {
235+ name := foldKey(c.Name.Lexeme)
236+ ext, isExt := e.funcs[name]
237+ isExt = isExt && ext != nil
238+ var impl builtinFunc
239+ if !isExt {
240+ // Arity of the built-ins is checked by the parser, and again here
241+ // so that a hand-built tree cannot reach an implementation with the
242+ // wrong number of arguments.
243+ f, ok := builtinFuncs[name]
244+ if !ok {
245+ return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme)
246+ }
247+ if t, bad := arityError(name, f, len(c.Args)); bad {
248+ return nil, &EvalError{Message: t.description() + ": '" + c.Name.Lexeme + "'"}
249+ }
250+ impl = builtinImpls[name]
251+ }
150 args := make([]Value, len(c.Args))252 args := make([]Value, len(c.Args))
151 for i, a := range c.Args {253 for i, a := range c.Args {
152 v, err := e.eval(a)254 v, err := e.eval(a)
@@ -155,14 +257,18 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) {
155 }257 }
156 args[i] = v258 args[i] = v
157 }259 }
158- name := foldKey(c.Name.Lexeme)260+ if isExt {
159- if f, ok := e.funcs[name]; ok && f != nil {261+ v, err := ext(e.ctx, args)
160- return f(args)262+ if err != nil {
161- }263+ var ee *EvalError
162- if f, ok := builtinImpls[name]; ok {264+ if errors.As(err, &ee) {
163- return f(args)265+ return nil, err
266+ }
267+ return nil, &EvalError{Message: err.Error(), Err: err}
268+ }
269+ return v, nil
164 }270 }
165- return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme)271+ return impl(e, args)
166 }272 }
167 273
168 // indexer is idxHelper of the reference: how an index value is read, as a274 // indexer is idxHelper of the reference: how an index value is read, as a
@@ -221,11 +327,11 @@ func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) {
221 switch o := normalize(obj).(type) {327 switch o := normalize(obj).(type) {
222 case *Array:328 case *Array:
223 if o.filtered {329 if o.filtered {
224- return filteredArrayAccess(o, ix), nil330+ return e.filteredArrayAccess(o, ix)
225 }331 }
226- return arrayAccess(o, ix), nil332+ return e.arrayAccess(o, ix)
227 case *Dictionary:333 case *Dictionary:
228- return objectAccess(o, ix), nil334+ return e.objectAccess(o, ix)
229 }335 }
230 if ix.star {336 if ix.star {
231 return &Array{filtered: true}, nil337 return &Array{filtered: true}, nil
@@ -235,49 +341,70 @@ func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) {
235 341
236 // filteredArrayAccess applies an index to each element of a filtered array342 // filteredArrayAccess applies an index to each element of a filtered array
237 // and collects the hits: "a.*.b" is the b of every element that has one.343 // and collects the hits: "a.*.b" is the b of every element that has one.
238-func filteredArrayAccess(fa *Array, ix indexer) *Array {344+func (e *evaluator) filteredArrayAccess(fa *Array, ix indexer) (Value, error) {
239 result := &Array{filtered: true}345 result := &Array{filtered: true}
240 for _, item := range fa.items {346 for _, item := range fa.items {
347+ if err := e.tick(); err != nil {
348+ return nil, err
349+ }
241 switch it := normalize(item).(type) {350 switch it := normalize(item).(type) {
242 case *Dictionary:351 case *Dictionary:
243 if ix.star {352 if ix.star {
353+ if err := e.charge(len(it.vals) * filterElementCost); err != nil {
354+ return nil, err
355+ }
244 result.items = append(result.items, it.vals...)356 result.items = append(result.items, it.vals...)
245 } else if ix.hasStr {357 } else if ix.hasStr {
246 if v, ok := it.Get(ix.str); ok {358 if v, ok := it.Get(ix.str); ok {
359+ if err := e.charge(filterElementCost); err != nil {
360+ return nil, err
361+ }
247 result.items = append(result.items, v)362 result.items = append(result.items, v)
248 }363 }
249 }364 }
250 case *Array:365 case *Array:
251 if ix.star {366 if ix.star {
367+ if err := e.charge(len(it.items) * filterElementCost); err != nil {
368+ return nil, err
369+ }
252 result.items = append(result.items, it.items...)370 result.items = append(result.items, it.items...)
253 } else if i, ok := ix.pos(len(it.items)); ok {371 } else if i, ok := ix.pos(len(it.items)); ok {
372+ if err := e.charge(filterElementCost); err != nil {
373+ return nil, err
374+ }
254 result.items = append(result.items, it.items[i])375 result.items = append(result.items, it.items[i])
255 }376 }
256 }377 }
257 }378 }
258- return result379+ return result, nil
259 }380 }
260 381
261-func arrayAccess(a *Array, ix indexer) Value {382+func (e *evaluator) arrayAccess(a *Array, ix indexer) (Value, error) {
262 if ix.star {383 if ix.star {
263- return &Array{items: append([]Value(nil), a.items...), filtered: true}384+ if err := e.charge(len(a.items) * filterElementCost); err != nil {
385+ return nil, err
386+ }
387+ return &Array{items: append([]Value(nil), a.items...), filtered: true}, nil
264 }388 }
265 if i, ok := ix.pos(len(a.items)); ok {389 if i, ok := ix.pos(len(a.items)); ok {
266- return normalize(a.items[i])390+ return normalize(a.items[i]), nil
267 }391 }
268- return Null{}392+ return Null{}, nil
269 }393 }
270 394
271-func objectAccess(d *Dictionary, ix indexer) Value {395+func (e *evaluator) objectAccess(d *Dictionary, ix indexer) (Value, error) {
272 if ix.star {396 if ix.star {
273- return &Array{items: append([]Value(nil), d.vals...), filtered: true}397+ if err := e.charge(len(d.vals) * filterElementCost); err != nil {
398+ return nil, err
399+ }
400+ return &Array{items: append([]Value(nil), d.vals...), filtered: true}, nil
274 }401 }
275 if ix.hasStr {402 if ix.hasStr {
276 if v, ok := d.Get(ix.str); ok {403 if v, ok := d.Get(ix.str); ok {
277- return v404+ return v, nil
278 }405 }
279 }406 }
280- return Null{}407+ return Null{}, nil
281 }408 }
282 409
283 // coerceTypes converts two values of different kinds to a common kind the410 // coerceTypes converts two values of different kinds to a common kind the
M expr/eval_test.go +14 -13
@@ -3,6 +3,7 @@
33 package expr
44
55 import (
6+ "context"
67 "errors"
78 "math"
89 "strings"
@@ -539,7 +540,7 @@ func TestEmptyExpression(t *testing.T) {
539540 t.Errorf("Parse(%q) = %v, %v; want nil, nil", s, node, err)
540541 }
541542 }
542- if _, err := Evaluate(nil, EvalContext{}); err == nil {
543+ if _, err := Evaluate(context.Background(), nil, EvalContext{}); err == nil {
543544 t.Error("Evaluate(nil) succeeded")
544545 }
545546 }
@@ -686,21 +687,21 @@ func TestExtensionFunctions(t *testing.T) {
686687 ctx := EvalContext{
687688 Contexts: map[string]Value{"G": String("v")},
688689 Funcs: map[string]Func{
689- "HashFiles": func(args []Value) (Value, error) {
690+ "HashFiles": func(_ context.Context, args []Value) (Value, error) {
690691 calls++
691692 return String("h:" + CoerceString(args[0])), nil
692693 },
693- "success": func([]Value) (Value, error) { return Boolean(true), nil },
694- "boom": func([]Value) (Value, error) { return nil, errors.New("kaput") },
694+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
695+ "boom": func(context.Context, []Value) (Value, error) { return nil, errors.New("kaput") },
695696 // An extra function replaces a built-in of the same name.
696- "contains": func([]Value) (Value, error) { return String("mine"), nil },
697+ "contains": func(context.Context, []Value) (Value, error) { return String("mine"), nil },
697698 },
698699 }
699700 node, err := Parse("hashFiles(g) == 'h:v' && success() && contains('a','b')", []string{"g"}, funcs)
700701 if err != nil {
701702 t.Fatal(err)
702703 }
703- v, err := Evaluate(node, ctx)
704+ v, err := Evaluate(context.Background(), node, ctx)
704705 if err != nil || CoerceString(v) != "mine" || calls != 1 {
705706 t.Errorf("got %v, %v, %d calls", v, err, calls)
706707 }
@@ -708,12 +709,12 @@ func TestExtensionFunctions(t *testing.T) {
708709 if err != nil {
709710 t.Fatal(err)
710711 }
711- if _, err = Evaluate(node, ctx); err == nil || err.Error() != "kaput" {
712+ if _, err = Evaluate(context.Background(), node, ctx); err == nil || err.Error() != "kaput" {
712713 t.Errorf("function error not propagated: %v", err)
713714 }
714715 // A declared function without an implementation is an evaluation error.
715716 node, _ = Parse("success()", nil, funcs)
716- if _, err = Evaluate(node, EvalContext{}); err == nil {
717+ if _, err = Evaluate(context.Background(), node, EvalContext{}); err == nil {
717718 t.Error("calling an unimplemented function succeeded")
718719 }
719720 }
@@ -726,13 +727,13 @@ func TestContextNames(t *testing.T) {
726727 for _, key := range []string{"github", "GITHUB", "GitHub"} {
727728 d := NewDictionary()
728729 d.Add("ref", String("main"))
729- v, err := Evaluate(node, EvalContext{Contexts: map[string]Value{key: d}})
730+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{key: d}})
730731 if err != nil || CoerceString(v) != "main" {
731732 t.Errorf("context key %q: %v, %v", key, v, err)
732733 }
733734 }
734735 // A declared but absent context is null.
735- v, err := Evaluate(node, EvalContext{})
736+ v, err := Evaluate(context.Background(), node, EvalContext{})
736737 if err != nil || v.Kind() != KindNull {
737738 t.Errorf("absent context: %v, %v", v, err)
738739 }
@@ -742,14 +743,14 @@ func TestContextNames(t *testing.T) {
742743 }
743744 // A nil Value in a context behaves as null.
744745 node, _ = Parse("github.a.b", []string{"github"}, nil)
745- if v, err := Evaluate(node, EvalContext{Contexts: map[string]Value{"github": nil}}); err != nil || v.Kind() != KindNull {
746+ if v, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"github": nil}}); err != nil || v.Kind() != KindNull {
746747 t.Errorf("nil context value: %v, %v", v, err)
747748 }
748749 var nilDict *Dictionary
749750 var nilArr *Array
750751 node, _ = Parse("(github)[0] || x", []string{"github", "x"}, nil)
751752 for _, bad := range []Value{nilDict, nilArr} {
752- if _, err := Evaluate(node, EvalContext{Contexts: map[string]Value{"github": bad}}); err != nil {
753+ if _, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"github": bad}}); err != nil {
753754 t.Errorf("typed nil %T: %v", bad, err)
754755 }
755756 }
@@ -766,7 +767,7 @@ func TestEvaluateDoesNotMutateContext(t *testing.T) {
766767 if err != nil {
767768 t.Fatal(err)
768769 }
769- v, err := Evaluate(node, EvalContext{Contexts: ctxs})
770+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: ctxs})
770771 if err != nil {
771772 t.Fatal(err)
772773 }
@@ -3,6 +3,7 @@
3 package expr3 package expr
4 4
5 import (5 import (
6+ "context"
6 "errors"7 "errors"
7 "math"8 "math"
8 "strings"9 "strings"
@@ -539,7 +540,7 @@ func TestEmptyExpression(t *testing.T) {
539 t.Errorf("Parse(%q) = %v, %v; want nil, nil", s, node, err)540 t.Errorf("Parse(%q) = %v, %v; want nil, nil", s, node, err)
540 }541 }
541 }542 }
542- if _, err := Evaluate(nil, EvalContext{}); err == nil {543+ if _, err := Evaluate(context.Background(), nil, EvalContext{}); err == nil {
543 t.Error("Evaluate(nil) succeeded")544 t.Error("Evaluate(nil) succeeded")
544 }545 }
545 }546 }
@@ -686,21 +687,21 @@ func TestExtensionFunctions(t *testing.T) {
686 ctx := EvalContext{687 ctx := EvalContext{
687 Contexts: map[string]Value{"G": String("v")},688 Contexts: map[string]Value{"G": String("v")},
688 Funcs: map[string]Func{689 Funcs: map[string]Func{
689- "HashFiles": func(args []Value) (Value, error) {690+ "HashFiles": func(_ context.Context, args []Value) (Value, error) {
690 calls++691 calls++
691 return String("h:" + CoerceString(args[0])), nil692 return String("h:" + CoerceString(args[0])), nil
692 },693 },
693- "success": func([]Value) (Value, error) { return Boolean(true), nil },694+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
694- "boom": func([]Value) (Value, error) { return nil, errors.New("kaput") },695+ "boom": func(context.Context, []Value) (Value, error) { return nil, errors.New("kaput") },
695 // An extra function replaces a built-in of the same name.696 // An extra function replaces a built-in of the same name.
696- "contains": func([]Value) (Value, error) { return String("mine"), nil },697+ "contains": func(context.Context, []Value) (Value, error) { return String("mine"), nil },
697 },698 },
698 }699 }
699 node, err := Parse("hashFiles(g) == 'h:v' && success() && contains('a','b')", []string{"g"}, funcs)700 node, err := Parse("hashFiles(g) == 'h:v' && success() && contains('a','b')", []string{"g"}, funcs)
700 if err != nil {701 if err != nil {
701 t.Fatal(err)702 t.Fatal(err)
702 }703 }
703- v, err := Evaluate(node, ctx)704+ v, err := Evaluate(context.Background(), node, ctx)
704 if err != nil || CoerceString(v) != "mine" || calls != 1 {705 if err != nil || CoerceString(v) != "mine" || calls != 1 {
705 t.Errorf("got %v, %v, %d calls", v, err, calls)706 t.Errorf("got %v, %v, %d calls", v, err, calls)
706 }707 }
@@ -708,12 +709,12 @@ func TestExtensionFunctions(t *testing.T) {
708 if err != nil {709 if err != nil {
709 t.Fatal(err)710 t.Fatal(err)
710 }711 }
711- if _, err = Evaluate(node, ctx); err == nil || err.Error() != "kaput" {712+ if _, err = Evaluate(context.Background(), node, ctx); err == nil || err.Error() != "kaput" {
712 t.Errorf("function error not propagated: %v", err)713 t.Errorf("function error not propagated: %v", err)
713 }714 }
714 // A declared function without an implementation is an evaluation error.715 // A declared function without an implementation is an evaluation error.
715 node, _ = Parse("success()", nil, funcs)716 node, _ = Parse("success()", nil, funcs)
716- if _, err = Evaluate(node, EvalContext{}); err == nil {717+ if _, err = Evaluate(context.Background(), node, EvalContext{}); err == nil {
717 t.Error("calling an unimplemented function succeeded")718 t.Error("calling an unimplemented function succeeded")
718 }719 }
719 }720 }
@@ -726,13 +727,13 @@ func TestContextNames(t *testing.T) {
726 for _, key := range []string{"github", "GITHUB", "GitHub"} {727 for _, key := range []string{"github", "GITHUB", "GitHub"} {
727 d := NewDictionary()728 d := NewDictionary()
728 d.Add("ref", String("main"))729 d.Add("ref", String("main"))
729- v, err := Evaluate(node, EvalContext{Contexts: map[string]Value{key: d}})730+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{key: d}})
730 if err != nil || CoerceString(v) != "main" {731 if err != nil || CoerceString(v) != "main" {
731 t.Errorf("context key %q: %v, %v", key, v, err)732 t.Errorf("context key %q: %v, %v", key, v, err)
732 }733 }
733 }734 }
734 // A declared but absent context is null.735 // A declared but absent context is null.
735- v, err := Evaluate(node, EvalContext{})736+ v, err := Evaluate(context.Background(), node, EvalContext{})
736 if err != nil || v.Kind() != KindNull {737 if err != nil || v.Kind() != KindNull {
737 t.Errorf("absent context: %v, %v", v, err)738 t.Errorf("absent context: %v, %v", v, err)
738 }739 }
@@ -742,14 +743,14 @@ func TestContextNames(t *testing.T) {
742 }743 }
743 // A nil Value in a context behaves as null.744 // A nil Value in a context behaves as null.
744 node, _ = Parse("github.a.b", []string{"github"}, nil)745 node, _ = Parse("github.a.b", []string{"github"}, nil)
745- if v, err := Evaluate(node, EvalContext{Contexts: map[string]Value{"github": nil}}); err != nil || v.Kind() != KindNull {746+ if v, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"github": nil}}); err != nil || v.Kind() != KindNull {
746 t.Errorf("nil context value: %v, %v", v, err)747 t.Errorf("nil context value: %v, %v", v, err)
747 }748 }
748 var nilDict *Dictionary749 var nilDict *Dictionary
749 var nilArr *Array750 var nilArr *Array
750 node, _ = Parse("(github)[0] || x", []string{"github", "x"}, nil)751 node, _ = Parse("(github)[0] || x", []string{"github", "x"}, nil)
751 for _, bad := range []Value{nilDict, nilArr} {752 for _, bad := range []Value{nilDict, nilArr} {
752- if _, err := Evaluate(node, EvalContext{Contexts: map[string]Value{"github": bad}}); err != nil {753+ if _, err := Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"github": bad}}); err != nil {
753 t.Errorf("typed nil %T: %v", bad, err)754 t.Errorf("typed nil %T: %v", bad, err)
754 }755 }
755 }756 }
@@ -766,7 +767,7 @@ func TestEvaluateDoesNotMutateContext(t *testing.T) {
766 if err != nil {767 if err != nil {
767 t.Fatal(err)768 t.Fatal(err)
768 }769 }
769- v, err := Evaluate(node, EvalContext{Contexts: ctxs})770+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: ctxs})
770 if err != nil {771 if err != nil {
771 t.Fatal(err)772 t.Fatal(err)
772 }773 }
M expr/funcs.go +69 -18
@@ -3,15 +3,20 @@
33 package expr
44
55 import (
6+ "errors"
67 "math"
78 "strconv"
89 "strings"
910 )
1011
12+// builtinFunc implements a built-in function. It runs inside the evaluator so
13+// that it can charge the output budget and watch the context.
14+type builtinFunc func(e *evaluator, args []Value) (Value, error)
15+
1116 // builtinImpls are the implementations of the built-in functions, keyed by
1217 // lower-cased name. Arity is validated by the parser, so each may assume its
1318 // documented argument count.
14-var builtinImpls = map[string]Func{
19+var builtinImpls = map[string]builtinFunc{
1520 "case": funcCase,
1621 "contains": funcContains,
1722 "endswith": funcEndsWith,
@@ -27,7 +32,7 @@ var builtinImpls = map[string]Func{
2732 // The JavaScript reference ships it behind an experimental feature flag
2833 // (allowCaseFunction) that only affects editor completion; the parser always
2934 // accepts it, and so does this package.
30-func funcCase(args []Value) (Value, error) {
35+func funcCase(e *evaluator, args []Value) (Value, error) {
3136 for i := 0; i < len(args)-1; i += 2 {
3237 pred, ok := normalize(args[i]).(Boolean)
3338 if !ok {
@@ -43,15 +48,18 @@ func funcCase(args []Value) (Value, error) {
4348 // funcContains is contains(search, item): substring test, case-insensitive
4449 // via toLowerCase, when both are primitive; membership by == in an array;
4550 // false otherwise.
46-func funcContains(args []Value) (Value, error) {
51+func funcContains(e *evaluator, args []Value) (Value, error) {
4752 left, right := normalize(args[0]), normalize(args[1])
4853 switch {
4954 case isPrimitive(left):
5055 if isPrimitive(right) {
51- return Boolean(strings.Contains(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil
56+ return Boolean(jsIncludes(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil
5257 }
5358 case left.Kind() == KindArray:
5459 for _, v := range left.(*Array).items {
60+ if err := e.tick(); err != nil {
61+ return nil, err
62+ }
5563 if equals(right, v) {
5664 return Boolean(true), nil
5765 }
@@ -60,14 +68,14 @@ func funcContains(args []Value) (Value, error) {
6068 return Boolean(false), nil
6169 }
6270
63-func funcStartsWith(args []Value) (Value, error) {
71+func funcStartsWith(e *evaluator, args []Value) (Value, error) {
6472 ls, rs, ok := primitivePair(args)
65- return Boolean(ok && strings.HasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
73+ return Boolean(ok && jsHasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
6674 }
6775
68-func funcEndsWith(args []Value) (Value, error) {
76+func funcEndsWith(e *evaluator, args []Value) (Value, error) {
6977 ls, rs, ok := primitivePair(args)
70- return Boolean(ok && strings.HasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
78+ return Boolean(ok && jsHasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
7179 }
7280
7381 // primitivePair returns the string forms of the two arguments, and false
@@ -82,7 +90,7 @@ func primitivePair(args []Value) (string, string, bool) {
8290 // funcJoin is join(array, separator). A primitive first argument is just
8391 // converted to a string, ignoring the separator; a dictionary gives "". A
8492 // separator that is not primitive means ",".
85-func funcJoin(args []Value) (Value, error) {
93+func funcJoin(e *evaluator, args []Value) (Value, error) {
8694 first := normalize(args[0])
8795 if isPrimitive(first) {
8896 return String(CoerceString(first)), nil
@@ -95,21 +103,51 @@ func funcJoin(args []Value) (Value, error) {
95103 if len(args) > 1 && isPrimitive(args[1]) {
96104 sep = CoerceString(args[1])
97105 }
98- parts := make([]string, len(arr.items))
106+ var out strings.Builder
99107 for i, it := range arr.items {
100- parts[i] = CoerceString(it)
108+ if err := e.tick(); err != nil {
109+ return nil, err
110+ }
111+ part := CoerceString(it)
112+ n := len(part)
113+ if i > 0 {
114+ n += len(sep)
115+ }
116+ if err := e.charge(n); err != nil {
117+ return nil, err
118+ }
119+ if i > 0 {
120+ out.WriteString(sep)
121+ }
122+ out.WriteString(part)
101123 }
102- return String(strings.Join(parts, sep)), nil
124+ return String(normalizeWTF8(out.String())), nil
103125 }
104126
105-func funcToJSON(args []Value) (Value, error) {
106- return String(ToJSON(args[0])), nil
127+// funcToJSON is toJSON(value). The output counts against the budget and is
128+// cut off as soon as it exceeds what is left.
129+func funcToJSON(e *evaluator, args []Value) (Value, error) {
130+ max := -1
131+ if e.limit >= 0 {
132+ max = e.limit - e.used
133+ }
134+ out, err := EncodeJSON(args[0], max)
135+ switch {
136+ case errors.Is(err, errJSONTooBig):
137+ return nil, e.budgetError()
138+ case err != nil:
139+ return nil, &EvalError{Message: "toJSON: " + err.Error(), Err: err}
140+ }
141+ if err := e.charge(len(out)); err != nil {
142+ return nil, err
143+ }
144+ return String(out), nil
107145 }
108146
109147 // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything
110148 // that is not valid JSON is "Error parsing JSON when evaluating fromJson"
111149 // wrapping the syntax error.
112-func funcFromJSON(args []Value) (Value, error) {
150+func funcFromJSON(e *evaluator, args []Value) (Value, error) {
113151 in := CoerceString(args[0])
114152 if jsTrim(in) == "" {
115153 return nil, evalErrorf("empty input")
@@ -125,7 +163,7 @@ func funcFromJSON(args []Value) (Value, error) {
125163 // are literal braces. Anything between the digits of "{N" and the closing
126164 // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an
127165 // error, as is a reference to a missing argument.
128-func funcFormat(args []Value) (Value, error) {
166+func funcFormat(e *evaluator, args []Value) (Value, error) {
129167 fs := CoerceString(args[0])
130168 var out strings.Builder
131169 index := 0
@@ -136,6 +174,9 @@ func funcFormat(args []Value) (Value, error) {
136174 case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace):
137175 // Escaped left brace.
138176 if charAt(fs, lbrace+1) == '{' {
177+ if err := e.charge(lbrace + 1 - index); err != nil {
178+ return nil, err
179+ }
139180 out.WriteString(fs[index : lbrace+1])
140181 index = lbrace + 2
141182 continue
@@ -146,8 +187,12 @@ func funcFormat(args []Value) (Value, error) {
146187 if 1+argIndex > float64(len(args)-1) {
147188 return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs)
148189 }
190+ arg := CoerceString(args[1+int(argIndex)])
191+ if err := e.charge(lbrace - index + len(arg)); err != nil {
192+ return nil, err
193+ }
149194 out.WriteString(fs[index:lbrace])
150- out.WriteString(CoerceString(args[1+int(argIndex)]))
195+ out.WriteString(arg)
151196 index = rbrace + 1
152197 continue
153198 }
@@ -158,14 +203,20 @@ func funcFormat(args []Value) (Value, error) {
158203 if charAt(fs, rbrace+1) != '}' {
159204 return nil, evalErrorf("The following format string is invalid: %s", fs)
160205 }
206+ if err := e.charge(rbrace + 1 - index); err != nil {
207+ return nil, err
208+ }
161209 out.WriteString(fs[index : rbrace+1])
162210 index = rbrace + 2
163211 default:
212+ if err := e.charge(len(fs) - index); err != nil {
213+ return nil, err
214+ }
164215 out.WriteString(fs[index:])
165216 index = len(fs)
166217 }
167218 }
168- return String(out.String()), nil
219+ return String(normalizeWTF8(out.String())), nil
169220 }
170221
171222 func indexByteFrom(s string, c byte, from int) int {
@@ -3,15 +3,20 @@
3 package expr3 package expr
4 4
5 import (5 import (
6+ "errors"
6 "math"7 "math"
7 "strconv"8 "strconv"
8 "strings"9 "strings"
9 )10 )
10 11
12+// builtinFunc implements a built-in function. It runs inside the evaluator so
13+// that it can charge the output budget and watch the context.
14+type builtinFunc func(e *evaluator, args []Value) (Value, error)
15+
11 // builtinImpls are the implementations of the built-in functions, keyed by16 // builtinImpls are the implementations of the built-in functions, keyed by
12 // lower-cased name. Arity is validated by the parser, so each may assume its17 // lower-cased name. Arity is validated by the parser, so each may assume its
13 // documented argument count.18 // documented argument count.
14-var builtinImpls = map[string]Func{19+var builtinImpls = map[string]builtinFunc{
15 "case": funcCase,20 "case": funcCase,
16 "contains": funcContains,21 "contains": funcContains,
17 "endswith": funcEndsWith,22 "endswith": funcEndsWith,
@@ -27,7 +32,7 @@ var builtinImpls = map[string]Func{
27 // The JavaScript reference ships it behind an experimental feature flag32 // The JavaScript reference ships it behind an experimental feature flag
28 // (allowCaseFunction) that only affects editor completion; the parser always33 // (allowCaseFunction) that only affects editor completion; the parser always
29 // accepts it, and so does this package.34 // accepts it, and so does this package.
30-func funcCase(args []Value) (Value, error) {35+func funcCase(e *evaluator, args []Value) (Value, error) {
31 for i := 0; i < len(args)-1; i += 2 {36 for i := 0; i < len(args)-1; i += 2 {
32 pred, ok := normalize(args[i]).(Boolean)37 pred, ok := normalize(args[i]).(Boolean)
33 if !ok {38 if !ok {
@@ -43,15 +48,18 @@ func funcCase(args []Value) (Value, error) {
43 // funcContains is contains(search, item): substring test, case-insensitive48 // funcContains is contains(search, item): substring test, case-insensitive
44 // via toLowerCase, when both are primitive; membership by == in an array;49 // via toLowerCase, when both are primitive; membership by == in an array;
45 // false otherwise.50 // false otherwise.
46-func funcContains(args []Value) (Value, error) {51+func funcContains(e *evaluator, args []Value) (Value, error) {
47 left, right := normalize(args[0]), normalize(args[1])52 left, right := normalize(args[0]), normalize(args[1])
48 switch {53 switch {
49 case isPrimitive(left):54 case isPrimitive(left):
50 if isPrimitive(right) {55 if isPrimitive(right) {
51- return Boolean(strings.Contains(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil56+ return Boolean(jsIncludes(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil
52 }57 }
53 case left.Kind() == KindArray:58 case left.Kind() == KindArray:
54 for _, v := range left.(*Array).items {59 for _, v := range left.(*Array).items {
60+ if err := e.tick(); err != nil {
61+ return nil, err
62+ }
55 if equals(right, v) {63 if equals(right, v) {
56 return Boolean(true), nil64 return Boolean(true), nil
57 }65 }
@@ -60,14 +68,14 @@ func funcContains(args []Value) (Value, error) {
60 return Boolean(false), nil68 return Boolean(false), nil
61 }69 }
62 70
63-func funcStartsWith(args []Value) (Value, error) {71+func funcStartsWith(e *evaluator, args []Value) (Value, error) {
64 ls, rs, ok := primitivePair(args)72 ls, rs, ok := primitivePair(args)
65- return Boolean(ok && strings.HasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil73+ return Boolean(ok && jsHasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
66 }74 }
67 75
68-func funcEndsWith(args []Value) (Value, error) {76+func funcEndsWith(e *evaluator, args []Value) (Value, error) {
69 ls, rs, ok := primitivePair(args)77 ls, rs, ok := primitivePair(args)
70- return Boolean(ok && strings.HasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil78+ return Boolean(ok && jsHasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil
71 }79 }
72 80
73 // primitivePair returns the string forms of the two arguments, and false81 // primitivePair returns the string forms of the two arguments, and false
@@ -82,7 +90,7 @@ func primitivePair(args []Value) (string, string, bool) {
82 // funcJoin is join(array, separator). A primitive first argument is just90 // funcJoin is join(array, separator). A primitive first argument is just
83 // converted to a string, ignoring the separator; a dictionary gives "". A91 // converted to a string, ignoring the separator; a dictionary gives "". A
84 // separator that is not primitive means ",".92 // separator that is not primitive means ",".
85-func funcJoin(args []Value) (Value, error) {93+func funcJoin(e *evaluator, args []Value) (Value, error) {
86 first := normalize(args[0])94 first := normalize(args[0])
87 if isPrimitive(first) {95 if isPrimitive(first) {
88 return String(CoerceString(first)), nil96 return String(CoerceString(first)), nil
@@ -95,21 +103,51 @@ func funcJoin(args []Value) (Value, error) {
95 if len(args) > 1 && isPrimitive(args[1]) {103 if len(args) > 1 && isPrimitive(args[1]) {
96 sep = CoerceString(args[1])104 sep = CoerceString(args[1])
97 }105 }
98- parts := make([]string, len(arr.items))106+ var out strings.Builder
99 for i, it := range arr.items {107 for i, it := range arr.items {
100- parts[i] = CoerceString(it)108+ if err := e.tick(); err != nil {
109+ return nil, err
110+ }
111+ part := CoerceString(it)
112+ n := len(part)
113+ if i > 0 {
114+ n += len(sep)
115+ }
116+ if err := e.charge(n); err != nil {
117+ return nil, err
118+ }
119+ if i > 0 {
120+ out.WriteString(sep)
121+ }
122+ out.WriteString(part)
101 }123 }
102- return String(strings.Join(parts, sep)), nil124+ return String(normalizeWTF8(out.String())), nil
103 }125 }
104 126
105-func funcToJSON(args []Value) (Value, error) {127+// funcToJSON is toJSON(value). The output counts against the budget and is
106- return String(ToJSON(args[0])), nil128+// cut off as soon as it exceeds what is left.
129+func funcToJSON(e *evaluator, args []Value) (Value, error) {
130+ max := -1
131+ if e.limit >= 0 {
132+ max = e.limit - e.used
133+ }
134+ out, err := EncodeJSON(args[0], max)
135+ switch {
136+ case errors.Is(err, errJSONTooBig):
137+ return nil, e.budgetError()
138+ case err != nil:
139+ return nil, &EvalError{Message: "toJSON: " + err.Error(), Err: err}
140+ }
141+ if err := e.charge(len(out)); err != nil {
142+ return nil, err
143+ }
144+ return String(out), nil
107 }145 }
108 146
109 // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything147 // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything
110 // that is not valid JSON is "Error parsing JSON when evaluating fromJson"148 // that is not valid JSON is "Error parsing JSON when evaluating fromJson"
111 // wrapping the syntax error.149 // wrapping the syntax error.
112-func funcFromJSON(args []Value) (Value, error) {150+func funcFromJSON(e *evaluator, args []Value) (Value, error) {
113 in := CoerceString(args[0])151 in := CoerceString(args[0])
114 if jsTrim(in) == "" {152 if jsTrim(in) == "" {
115 return nil, evalErrorf("empty input")153 return nil, evalErrorf("empty input")
@@ -125,7 +163,7 @@ func funcFromJSON(args []Value) (Value, error) {
125 // are literal braces. Anything between the digits of "{N" and the closing163 // are literal braces. Anything between the digits of "{N" and the closing
126 // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an164 // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an
127 // error, as is a reference to a missing argument.165 // error, as is a reference to a missing argument.
128-func funcFormat(args []Value) (Value, error) {166+func funcFormat(e *evaluator, args []Value) (Value, error) {
129 fs := CoerceString(args[0])167 fs := CoerceString(args[0])
130 var out strings.Builder168 var out strings.Builder
131 index := 0169 index := 0
@@ -136,6 +174,9 @@ func funcFormat(args []Value) (Value, error) {
136 case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace):174 case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace):
137 // Escaped left brace.175 // Escaped left brace.
138 if charAt(fs, lbrace+1) == '{' {176 if charAt(fs, lbrace+1) == '{' {
177+ if err := e.charge(lbrace + 1 - index); err != nil {
178+ return nil, err
179+ }
139 out.WriteString(fs[index : lbrace+1])180 out.WriteString(fs[index : lbrace+1])
140 index = lbrace + 2181 index = lbrace + 2
141 continue182 continue
@@ -146,8 +187,12 @@ func funcFormat(args []Value) (Value, error) {
146 if 1+argIndex > float64(len(args)-1) {187 if 1+argIndex > float64(len(args)-1) {
147 return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs)188 return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs)
148 }189 }
190+ arg := CoerceString(args[1+int(argIndex)])
191+ if err := e.charge(lbrace - index + len(arg)); err != nil {
192+ return nil, err
193+ }
149 out.WriteString(fs[index:lbrace])194 out.WriteString(fs[index:lbrace])
150- out.WriteString(CoerceString(args[1+int(argIndex)]))195+ out.WriteString(arg)
151 index = rbrace + 1196 index = rbrace + 1
152 continue197 continue
153 }198 }
@@ -158,14 +203,20 @@ func funcFormat(args []Value) (Value, error) {
158 if charAt(fs, rbrace+1) != '}' {203 if charAt(fs, rbrace+1) != '}' {
159 return nil, evalErrorf("The following format string is invalid: %s", fs)204 return nil, evalErrorf("The following format string is invalid: %s", fs)
160 }205 }
206+ if err := e.charge(rbrace + 1 - index); err != nil {
207+ return nil, err
208+ }
161 out.WriteString(fs[index : rbrace+1])209 out.WriteString(fs[index : rbrace+1])
162 index = rbrace + 2210 index = rbrace + 2
163 default:211 default:
212+ if err := e.charge(len(fs) - index); err != nil {
213+ return nil, err
214+ }
164 out.WriteString(fs[index:])215 out.WriteString(fs[index:])
165 index = len(fs)216 index = len(fs)
166 }217 }
167 }218 }
168- return String(out.String()), nil219+ return String(normalizeWTF8(out.String())), nil
169 }220 }
170 221
171 func indexByteFrom(s string, c byte, from int) int {222 func indexByteFrom(s string, c byte, from int) int {
M expr/fuzz_test.go +5 -4
@@ -3,6 +3,7 @@
33 package expr
44
55 import (
6+ "context"
67 "strings"
78 "testing"
89 "unicode/utf8"
@@ -16,7 +17,7 @@ var fuzzSeeds = []string{
1617 "fromJSON('[1,{\"a\":null}]')[1].a", "toJSON(fromJSON('{\"b\":1,\"1\":2}'))", "join(matrix.node.*, ', ')", "case(true, 1, 2)", "case(false, 1, true, 2, 3)",
1718 "((((((((((1))))))))))", "!!!!!!true", "1 || 2 && 3 == 4 < 5", "hashFiles('**/*.go', '!vendor/**')", "success() && always()", "\n\t \r", "\u00a0", "'😀' == '\U00010000'",
1819 "=", "&", "|", "!=", "<=", ">=", "()", "(1", "1)", "a.", ".a", "a..b", "a[", "a[]", "a[*]", "*", "a.*.*", "1.2.3", "--1", "1_000", "'\xed\xa0\x80'",
19- "format('{0:x}{', 1)", "format('}}{{', 1)", "contains(1,2,3)", "ß == 'SS'", "İ", "'ı' == 'I'", "'\u03a3' < 'z'",
20+ "fromJSON('\"\\ud800\"') == fromJSON('\"\\udc00\"')", "format('{0}{1}', fromJSON('\"\\ud83d\"'), fromJSON('\"\\ude00\"'))", "contains(fromJSON('\"\\ud83d\\ude00\"'), fromJSON('\"\\ude00\"'))", "format('{0}{0}', format('{0}{0}', 'xxxxxxxx'))", "github.event.commits.*.*.*", "format('{0:x}{', 1)", "format('}}{{', 1)", "contains(1,2,3)", "ß == 'SS'", "İ", "'ı' == 'I'", "'\u03a3' < 'z'",
2021 }
2122
2223 func addSeeds(f *testing.F) {
@@ -127,8 +128,8 @@ func FuzzEvaluate(f *testing.F) {
127128 "env": mustJSON(f, `{"A":"","B":"1","C":"x","J":"{\"a\":[1,2]}"}`),
128129 },
129130 Funcs: map[string]Func{
130- "hashfiles": func(args []Value) (Value, error) { return String("h"), nil },
131- "success": func([]Value) (Value, error) { return Boolean(true), nil },
131+ "hashfiles": func(_ context.Context, args []Value) (Value, error) { return String("h"), nil },
132+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
132133 },
133134 }
134135 before := map[string]string{}
@@ -140,7 +141,7 @@ func FuzzEvaluate(f *testing.F) {
140141 if err != nil || node == nil {
141142 return
142143 }
143- v, err := Evaluate(node, ctx)
144+ v, err := Evaluate(context.Background(), node, ctx)
144145 if err != nil {
145146 if _, ok := err.(*EvalError); !ok {
146147 t.Fatalf("error is %T, not *EvalError", err)
@@ -3,6 +3,7 @@
3 package expr3 package expr
4 4
5 import (5 import (
6+ "context"
6 "strings"7 "strings"
7 "testing"8 "testing"
8 "unicode/utf8"9 "unicode/utf8"
@@ -16,7 +17,7 @@ var fuzzSeeds = []string{
16 "fromJSON('[1,{\"a\":null}]')[1].a", "toJSON(fromJSON('{\"b\":1,\"1\":2}'))", "join(matrix.node.*, ', ')", "case(true, 1, 2)", "case(false, 1, true, 2, 3)",17 "fromJSON('[1,{\"a\":null}]')[1].a", "toJSON(fromJSON('{\"b\":1,\"1\":2}'))", "join(matrix.node.*, ', ')", "case(true, 1, 2)", "case(false, 1, true, 2, 3)",
17 "((((((((((1))))))))))", "!!!!!!true", "1 || 2 && 3 == 4 < 5", "hashFiles('**/*.go', '!vendor/**')", "success() && always()", "\n\t \r", "\u00a0", "'😀' == '\U00010000'",18 "((((((((((1))))))))))", "!!!!!!true", "1 || 2 && 3 == 4 < 5", "hashFiles('**/*.go', '!vendor/**')", "success() && always()", "\n\t \r", "\u00a0", "'😀' == '\U00010000'",
18 "=", "&", "|", "!=", "<=", ">=", "()", "(1", "1)", "a.", ".a", "a..b", "a[", "a[]", "a[*]", "*", "a.*.*", "1.2.3", "--1", "1_000", "'\xed\xa0\x80'",19 "=", "&", "|", "!=", "<=", ">=", "()", "(1", "1)", "a.", ".a", "a..b", "a[", "a[]", "a[*]", "*", "a.*.*", "1.2.3", "--1", "1_000", "'\xed\xa0\x80'",
19- "format('{0:x}{', 1)", "format('}}{{', 1)", "contains(1,2,3)", "ß == 'SS'", "İ", "'ı' == 'I'", "'\u03a3' < 'z'",20+ "fromJSON('\"\\ud800\"') == fromJSON('\"\\udc00\"')", "format('{0}{1}', fromJSON('\"\\ud83d\"'), fromJSON('\"\\ude00\"'))", "contains(fromJSON('\"\\ud83d\\ude00\"'), fromJSON('\"\\ude00\"'))", "format('{0}{0}', format('{0}{0}', 'xxxxxxxx'))", "github.event.commits.*.*.*", "format('{0:x}{', 1)", "format('}}{{', 1)", "contains(1,2,3)", "ß == 'SS'", "İ", "'ı' == 'I'", "'\u03a3' < 'z'",
20 }21 }
21 22
22 func addSeeds(f *testing.F) {23 func addSeeds(f *testing.F) {
@@ -127,8 +128,8 @@ func FuzzEvaluate(f *testing.F) {
127 "env": mustJSON(f, `{"A":"","B":"1","C":"x","J":"{\"a\":[1,2]}"}`),128 "env": mustJSON(f, `{"A":"","B":"1","C":"x","J":"{\"a\":[1,2]}"}`),
128 },129 },
129 Funcs: map[string]Func{130 Funcs: map[string]Func{
130- "hashfiles": func(args []Value) (Value, error) { return String("h"), nil },131+ "hashfiles": func(_ context.Context, args []Value) (Value, error) { return String("h"), nil },
131- "success": func([]Value) (Value, error) { return Boolean(true), nil },132+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
132 },133 },
133 }134 }
134 before := map[string]string{}135 before := map[string]string{}
@@ -140,7 +141,7 @@ func FuzzEvaluate(f *testing.F) {
140 if err != nil || node == nil {141 if err != nil || node == nil {
141 return142 return
142 }143 }
143- v, err := Evaluate(node, ctx)144+ v, err := Evaluate(context.Background(), node, ctx)
144 if err != nil {145 if err != nil {
145 if _, ok := err.(*EvalError); !ok {146 if _, ok := err.(*EvalError); !ok {
146 t.Fatalf("error is %T, not *EvalError", err)147 t.Fatalf("error is %T, not *EvalError", err)
M expr/gen/oracle.mjs +52 -3
@@ -5,11 +5,19 @@
55 // tests (oracle_test.go) replay the corpus and compare token by token what
66 // GitHub's JavaScript library does with what this package does.
77 //
8-// node gen/oracle.mjs /path/to/@actions/expressions/dist | gzip -9 -n > testdata/oracle.json.gz
8+// node gen/oracle.mjs /path/to/@actions/expressions/dist [/path/to/actions-expressions-X.tgz] \\
9+// | gzip -9 -n > testdata/oracle.json.gz
10+//
11+// The optional tarball is the npm package the dist directory came from: its
12+// sha512 (base64, the "integrity" field of the npm registry) is recorded next
13+// to the Node version in the output, so the provenance of the oracle is
14+// checkable.
915 //
1016 // The corpus is hand-written edge cases plus a seeded pseudo-random walk over
1117 // the grammar and over mutations of it, so the file is reproducible.
1218
19+import { createHash } from "node:crypto";
20+import { readFileSync } from "node:fs";
1321 import { pathToFileURL } from "node:url";
1422 import path from "node:path";
1523
@@ -17,6 +25,18 @@ const dist = path.resolve(process.argv[2] ?? "node_modules/@actions/expressions/
1725 const ex = await import(pathToFileURL(path.join(dist, "index.js")).href);
1826 const { Lexer, Parser, Evaluator, data } = ex;
1927
28+const pkgJson = JSON.parse(readFileSync(path.join(dist, "..", "package.json"), "utf8"));
29+const integrity = process.argv[3]
30+ ? "sha512-" + createHash("sha512").update(readFileSync(process.argv[3])).digest("base64")
31+ : "";
32+const reference = {
33+ package: `${pkgJson.name} ${pkgJson.version}`,
34+ integrity,
35+ node: process.version,
36+ v8: process.versions.v8,
37+ unicode: process.versions.unicode,
38+};
39+
2040 // ---- contexts -------------------------------------------------------------
2141
2242 const contexts = {
@@ -128,7 +148,14 @@ function run(expr) {
128148 return out;
129149 }
130150 if (res === undefined) { out.skip = "undefined result"; return out; }
131- out.r = { k: kindOf(res), s: res.coerceString(), j: JSON.stringify(res, data.replacer, " ") };
151+ const str = res.coerceString();
152+ out.r = { k: kindOf(res), s: str, j: JSON.stringify(res, data.replacer, " ") };
153+ if (!str.isWellFormed()) {
154+ // A string holding a lone surrogate cannot travel through JSON text and
155+ // UTF-8: give its UTF-16 code units instead.
156+ out.r.s = "";
157+ out.r.u = Array.from({ length: str.length }, (_, i) => str.charCodeAt(i));
158+ }
132159 return out;
133160 }
134161
@@ -233,6 +260,28 @@ add("contains('a', 'b',)", "contains(1,)", "contains(,1)", "contains('a' 'b')",
233260 "join(fromJSON(toJSON(github.event.labels.*.name)), '+')", "toJSON(fromJSON(toJSON(github.event.commits)))", "format('{0}', toJSON(null))", "fromJSON(format('[{0},{1}]', 1, 2))[1]", "fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a", "toJSON(fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a.*)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a[0])", "toJSON(fromJSON('[[1,2],[3]]').*[0])", "toJSON(fromJSON('[[1,2],[3]]')[*][1])",
234261 "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}').*.x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}')[*].x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":2}').*.x)", "toJSON(fromJSON('[1,2]').*.x)", "toJSON(fromJSON('1').*)", "toJSON(null.*)", "toJSON('abc'.*)", "toJSON(fromJSON('[]').*.a)", "toJSON(fromJSON('[]').*)", "toJSON(fromJSON('{}').*)");
235262
263+// lone surrogates (kept as WTF-8 by the Go port)
264+{
265+ const H = "fromJSON('\"\\ud800\"')", L = "fromJSON('\"\\udc00\"')", H2 = "fromJSON('\"\\ud83d\"')", L2 = "fromJSON('\"\\ude00\"')";
266+ const lone = [H, L, H2, L2, "'\u{1F600}'", "'\ue000'", "'\uffff'", "'a'", "''", "'\ufffd'", "'\u{10000}'"];
267+ for (const a of lone) {
268+ add(`toJSON(${a})`, `format('{0}', ${a})`, `join(${a})`, `${a} || 'd'`, `startsWith(${a}, ${a})`, `case(true, ${a}, 1)`);
269+ for (const b of lone) {
270+ for (const op of ["==", "!=", "<", "<=", ">", ">="]) add(`${a} ${op} ${b}`);
271+ add(`contains(${a}, ${b})`, `startsWith(${a}, ${b})`, `endsWith(${a}, ${b})`, `format('{0}{1}', ${a}, ${b})`, `join(fromJSON('[]'), ${a}) == ''`,
272+ `toJSON(fromJSON(toJSON(${a})))`, `format('{0}', ${a}) == format('{0}', ${b})`);
273+ }
274+ }
275+ add(`${H} == ${L}`, `${H} != ${L}`, `${H} < ${L}`, `${H} > ${L}`, `contains('x${"\u{1F600}"}', ${H2})`, `contains('x\u{1F600}', ${L2})`, `startsWith('\u{1F600}', ${H2})`, `endsWith('\u{1F600}', ${L2})`,
276+ `format('{0}{1}', ${H2}, ${L2}) == '\u{1F600}'`, `format('{0}{1}', ${L2}, ${H2}) == '\u{1F600}'`, `join(fromJSON('["\\ud83d","\\ude00"]'), '') == '\u{1F600}'`,
277+ `join(fromJSON('["\\ud83d","\\ude00"]'), '')`, `format('{0}{1}', ${H2}, ${L2})`, `format('{0}{1}{0}', ${H2}, ${L2})`, `toJSON(format('{0}{1}', ${H2}, ${L2}))`,
278+ `toJSON(fromJSON('{"\\ud800":1,"\\ud800x":2,"a\\udc00":3}'))`, `fromJSON('{"\\ud800":1}')[${H}]`, `fromJSON('{"\\ud800":1}')[${L}]`, `toJSON(fromJSON('["\\ud800\\ud800","\\udc00\\ud800"]'))`,
279+ `${H} == '\ufffd'`, `'\ufffd' == ${H}`, `'\ufffd' < ${H}`, `${H} < '\ufffd'`, `${H} < '\u{1F600}'`, `'\u{1F600}' < ${H}`, `${L} < '\ue000'`, `'\ue000' < ${L}`,
280+ `fromJSON('"\\ud800\\u0041"') == 'A'`, `contains(fromJSON('"A\\ud800"'), 'a')`, `startsWith(fromJSON('"a\\ud800"'), 'A')`, `endsWith(fromJSON('"\\ud800a"'), 'A')`,
281+ `contains(fromJSON('"\\ud800\\udc00"'), ${H})`, `contains(fromJSON('"\\ud800\\udc00"'), ${L})`, `startsWith(fromJSON('"\\ud800\\udc00"'), ${H})`, `endsWith(fromJSON('"\\ud800\\udc00"'), ${L})`,
282+ `contains(fromJSON('["\\ud800"]'), ${H})`, `contains(fromJSON('["\\ud800"]'), ${L})`, `fromJSON('"\\ud800"') == fromJSON('"\\ud800"')`);
283+}
284+
236285 // depth and length limits
237286 const nest = (n, open, body, close) => open.repeat(n) + body + close.repeat(n);
238287 for (const n of [1, 10, 24, 25, 26, 48, 49, 50, 51, 52, 100, 200]) {
@@ -314,4 +363,4 @@ for (const e of corpus) {
314363 }
315364 const skipped = cases.filter(c => c.skip).length;
316365 console.error(`cases=${cases.length} skipped(JS crash)=${skipped}`);
317-process.stdout.write(JSON.stringify({ reference: "@actions/expressions 0.3.61", contexts, cases: cases.filter(c => !c.skip) }) + "\n");
366+process.stdout.write(JSON.stringify({ reference, contexts, cases: cases.filter(c => !c.skip) }) + "\n");
@@ -5,11 +5,19 @@
5 // tests (oracle_test.go) replay the corpus and compare token by token what5 // tests (oracle_test.go) replay the corpus and compare token by token what
6 // GitHub's JavaScript library does with what this package does.6 // GitHub's JavaScript library does with what this package does.
7 //7 //
8-// node gen/oracle.mjs /path/to/@actions/expressions/dist | gzip -9 -n > testdata/oracle.json.gz8+// node gen/oracle.mjs /path/to/@actions/expressions/dist [/path/to/actions-expressions-X.tgz] \\
9+// | gzip -9 -n > testdata/oracle.json.gz
10+//
11+// The optional tarball is the npm package the dist directory came from: its
12+// sha512 (base64, the "integrity" field of the npm registry) is recorded next
13+// to the Node version in the output, so the provenance of the oracle is
14+// checkable.
9 //15 //
10 // The corpus is hand-written edge cases plus a seeded pseudo-random walk over16 // The corpus is hand-written edge cases plus a seeded pseudo-random walk over
11 // the grammar and over mutations of it, so the file is reproducible.17 // the grammar and over mutations of it, so the file is reproducible.
12 18
19+import { createHash } from "node:crypto";
20+import { readFileSync } from "node:fs";
13 import { pathToFileURL } from "node:url";21 import { pathToFileURL } from "node:url";
14 import path from "node:path";22 import path from "node:path";
15 23
@@ -17,6 +25,18 @@ const dist = path.resolve(process.argv[2] ?? "node_modules/@actions/expressions/
17 const ex = await import(pathToFileURL(path.join(dist, "index.js")).href);25 const ex = await import(pathToFileURL(path.join(dist, "index.js")).href);
18 const { Lexer, Parser, Evaluator, data } = ex;26 const { Lexer, Parser, Evaluator, data } = ex;
19 27
28+const pkgJson = JSON.parse(readFileSync(path.join(dist, "..", "package.json"), "utf8"));
29+const integrity = process.argv[3]
30+ ? "sha512-" + createHash("sha512").update(readFileSync(process.argv[3])).digest("base64")
31+ : "";
32+const reference = {
33+ package: `${pkgJson.name} ${pkgJson.version}`,
34+ integrity,
35+ node: process.version,
36+ v8: process.versions.v8,
37+ unicode: process.versions.unicode,
38+};
39+
20 // ---- contexts -------------------------------------------------------------40 // ---- contexts -------------------------------------------------------------
21 41
22 const contexts = {42 const contexts = {
@@ -128,7 +148,14 @@ function run(expr) {
128 return out;148 return out;
129 }149 }
130 if (res === undefined) { out.skip = "undefined result"; return out; }150 if (res === undefined) { out.skip = "undefined result"; return out; }
131- out.r = { k: kindOf(res), s: res.coerceString(), j: JSON.stringify(res, data.replacer, " ") };151+ const str = res.coerceString();
152+ out.r = { k: kindOf(res), s: str, j: JSON.stringify(res, data.replacer, " ") };
153+ if (!str.isWellFormed()) {
154+ // A string holding a lone surrogate cannot travel through JSON text and
155+ // UTF-8: give its UTF-16 code units instead.
156+ out.r.s = "";
157+ out.r.u = Array.from({ length: str.length }, (_, i) => str.charCodeAt(i));
158+ }
132 return out;159 return out;
133 }160 }
134 161
@@ -233,6 +260,28 @@ add("contains('a', 'b',)", "contains(1,)", "contains(,1)", "contains('a' 'b')",
233 "join(fromJSON(toJSON(github.event.labels.*.name)), '+')", "toJSON(fromJSON(toJSON(github.event.commits)))", "format('{0}', toJSON(null))", "fromJSON(format('[{0},{1}]', 1, 2))[1]", "fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a", "toJSON(fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a.*)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a[0])", "toJSON(fromJSON('[[1,2],[3]]').*[0])", "toJSON(fromJSON('[[1,2],[3]]')[*][1])",260 "join(fromJSON(toJSON(github.event.labels.*.name)), '+')", "toJSON(fromJSON(toJSON(github.event.commits)))", "format('{0}', toJSON(null))", "fromJSON(format('[{0},{1}]', 1, 2))[1]", "fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a", "toJSON(fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a.*)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a[0])", "toJSON(fromJSON('[[1,2],[3]]').*[0])", "toJSON(fromJSON('[[1,2],[3]]')[*][1])",
234 "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}').*.x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}')[*].x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":2}').*.x)", "toJSON(fromJSON('[1,2]').*.x)", "toJSON(fromJSON('1').*)", "toJSON(null.*)", "toJSON('abc'.*)", "toJSON(fromJSON('[]').*.a)", "toJSON(fromJSON('[]').*)", "toJSON(fromJSON('{}').*)");261 "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}').*.x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}')[*].x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":2}').*.x)", "toJSON(fromJSON('[1,2]').*.x)", "toJSON(fromJSON('1').*)", "toJSON(null.*)", "toJSON('abc'.*)", "toJSON(fromJSON('[]').*.a)", "toJSON(fromJSON('[]').*)", "toJSON(fromJSON('{}').*)");
235 262
263+// lone surrogates (kept as WTF-8 by the Go port)
264+{
265+ const H = "fromJSON('\"\\ud800\"')", L = "fromJSON('\"\\udc00\"')", H2 = "fromJSON('\"\\ud83d\"')", L2 = "fromJSON('\"\\ude00\"')";
266+ const lone = [H, L, H2, L2, "'\u{1F600}'", "'\ue000'", "'\uffff'", "'a'", "''", "'\ufffd'", "'\u{10000}'"];
267+ for (const a of lone) {
268+ add(`toJSON(${a})`, `format('{0}', ${a})`, `join(${a})`, `${a} || 'd'`, `startsWith(${a}, ${a})`, `case(true, ${a}, 1)`);
269+ for (const b of lone) {
270+ for (const op of ["==", "!=", "<", "<=", ">", ">="]) add(`${a} ${op} ${b}`);
271+ add(`contains(${a}, ${b})`, `startsWith(${a}, ${b})`, `endsWith(${a}, ${b})`, `format('{0}{1}', ${a}, ${b})`, `join(fromJSON('[]'), ${a}) == ''`,
272+ `toJSON(fromJSON(toJSON(${a})))`, `format('{0}', ${a}) == format('{0}', ${b})`);
273+ }
274+ }
275+ add(`${H} == ${L}`, `${H} != ${L}`, `${H} < ${L}`, `${H} > ${L}`, `contains('x${"\u{1F600}"}', ${H2})`, `contains('x\u{1F600}', ${L2})`, `startsWith('\u{1F600}', ${H2})`, `endsWith('\u{1F600}', ${L2})`,
276+ `format('{0}{1}', ${H2}, ${L2}) == '\u{1F600}'`, `format('{0}{1}', ${L2}, ${H2}) == '\u{1F600}'`, `join(fromJSON('["\\ud83d","\\ude00"]'), '') == '\u{1F600}'`,
277+ `join(fromJSON('["\\ud83d","\\ude00"]'), '')`, `format('{0}{1}', ${H2}, ${L2})`, `format('{0}{1}{0}', ${H2}, ${L2})`, `toJSON(format('{0}{1}', ${H2}, ${L2}))`,
278+ `toJSON(fromJSON('{"\\ud800":1,"\\ud800x":2,"a\\udc00":3}'))`, `fromJSON('{"\\ud800":1}')[${H}]`, `fromJSON('{"\\ud800":1}')[${L}]`, `toJSON(fromJSON('["\\ud800\\ud800","\\udc00\\ud800"]'))`,
279+ `${H} == '\ufffd'`, `'\ufffd' == ${H}`, `'\ufffd' < ${H}`, `${H} < '\ufffd'`, `${H} < '\u{1F600}'`, `'\u{1F600}' < ${H}`, `${L} < '\ue000'`, `'\ue000' < ${L}`,
280+ `fromJSON('"\\ud800\\u0041"') == 'A'`, `contains(fromJSON('"A\\ud800"'), 'a')`, `startsWith(fromJSON('"a\\ud800"'), 'A')`, `endsWith(fromJSON('"\\ud800a"'), 'A')`,
281+ `contains(fromJSON('"\\ud800\\udc00"'), ${H})`, `contains(fromJSON('"\\ud800\\udc00"'), ${L})`, `startsWith(fromJSON('"\\ud800\\udc00"'), ${H})`, `endsWith(fromJSON('"\\ud800\\udc00"'), ${L})`,
282+ `contains(fromJSON('["\\ud800"]'), ${H})`, `contains(fromJSON('["\\ud800"]'), ${L})`, `fromJSON('"\\ud800"') == fromJSON('"\\ud800"')`);
283+}
284+
236 // depth and length limits285 // depth and length limits
237 const nest = (n, open, body, close) => open.repeat(n) + body + close.repeat(n);286 const nest = (n, open, body, close) => open.repeat(n) + body + close.repeat(n);
238 for (const n of [1, 10, 24, 25, 26, 48, 49, 50, 51, 52, 100, 200]) {287 for (const n of [1, 10, 24, 25, 26, 48, 49, 50, 51, 52, 100, 200]) {
@@ -314,4 +363,4 @@ for (const e of corpus) {
314 }363 }
315 const skipped = cases.filter(c => c.skip).length;364 const skipped = cases.filter(c => c.skip).length;
316 console.error(`cases=${cases.length} skipped(JS crash)=${skipped}`);365 console.error(`cases=${cases.length} skipped(JS crash)=${skipped}`);
317-process.stdout.write(JSON.stringify({ reference: "@actions/expressions 0.3.61", contexts, cases: cases.filter(c => !c.skip) }) + "\n");366+process.stdout.write(JSON.stringify({ reference, contexts, cases: cases.filter(c => !c.skip) }) + "\n");
M expr/json.go +142 -77
@@ -12,10 +12,12 @@ import (
1212 "unicode/utf8"
1313 )
1414
15-// maxJSONDepth bounds the nesting FromJSON accepts. JavaScript engines fail
16-// with a stack overflow somewhere in that region, which fromJSON reports like
17-// any other parse error.
18-const maxJSONDepth = 10000
15+// MaxJSONDepth bounds the nesting that FromJSON accepts and that ToJSON and
16+// EncodeJSON write. JavaScript engines fail with a stack overflow somewhere
17+// in that region; fromJSON reports it like any other parse error. The bound
18+// also stops the encoder on cyclic values, which can be built through
19+// [Array.Append].
20+const MaxJSONDepth = 1000
1921
2022 // FromJSON parses JSON text into a [Value], like the fromJSON function.
2123 //
@@ -80,7 +82,7 @@ func (p *jsonParser) literal(word string, v Value) (Value, error) {
8082 }
8183
8284 func (p *jsonParser) value(depth int) (Value, error) {
83- if depth > maxJSONDepth {
85+ if depth > MaxJSONDepth {
8486 return nil, errors.New("maximum JSON nesting depth exceeded")
8587 }
8688 if p.i >= len(p.s) {
@@ -204,16 +206,10 @@ func (p *jsonParser) str() (string, error) {
204206 p.i = save // not a pair; the second escape is read on its own
205207 }
206208 }
207- if r >= 0xD800 && r < 0xE000 {
208- // A lone surrogate cannot be a Go rune. Keep it as its
209- // three-byte WTF-8 form so that toJSON writes it back as
210- // the same \uXXXX escape, like JSON.stringify.
211- b.WriteByte(0xE0 | byte(r>>12))
212- b.WriteByte(0x80 | byte(r>>6)&0x3F)
213- b.WriteByte(0x80 | byte(r)&0x3F)
214- continue
215- }
216- b.WriteRune(r)
209+ // A lone surrogate cannot be a Go rune: writeRune keeps it in
210+ // its three-byte WTF-8 form so that toJSON writes it back as
211+ // the same \uXXXX escape, like JSON.stringify.
212+ writeRune(&b, r)
217213 default:
218214 p.i--
219215 return "", p.unexpected()
@@ -376,116 +372,185 @@ func jsKeyOrder(keys []string) []string {
376372 // JavaScript object. Control characters, quotes and backslashes in strings
377373 // are escaped; everything else, including non-ASCII text, is written as is.
378374 // Lone surrogates, which FromJSON keeps in WTF-8 form, are written as \uXXXX.
375+//
376+// ToJSON suits the finite values that FromJSON and Evaluate produce. A value
377+// nested deeper than [MaxJSONDepth], which only a cyclic value built by hand
378+// can be, makes it return "null"; use [EncodeJSON] to get an error instead
379+// and to bound the output.
379380 func ToJSON(v Value) string {
380- var b strings.Builder
381- writeJSON(&b, v, 0)
382- return b.String()
381+ s, err := EncodeJSON(v, -1)
382+ if err != nil {
383+ return "null"
384+ }
385+ return s
386+}
387+
388+// EncodeJSON is [ToJSON] with limits: it fails when v is nested deeper than
389+// [MaxJSONDepth] and, when maxBytes is not negative, as soon as the output
390+// would exceed maxBytes bytes, without building the rest.
391+func EncodeJSON(v Value, maxBytes int) (string, error) {
392+ w := &jsonWriter{limit: maxBytes}
393+ w.value(v, 0)
394+ if w.err != nil {
395+ return "", w.err
396+ }
397+ return w.b.String(), nil
398+}
399+
400+var (
401+ errJSONTooDeep = errors.New("value nested too deeply to encode as JSON (a cycle?)")
402+ errJSONTooBig = errors.New("JSON output exceeds the size limit")
403+)
404+
405+type jsonWriter struct {
406+ b strings.Builder
407+ limit int // negative: unlimited
408+ err error
383409 }
384410
385-func writeJSON(b *strings.Builder, v Value, depth int) {
411+// grow reports whether n more bytes may be written, recording the error
412+// otherwise.
413+func (w *jsonWriter) grow(n int) bool {
414+ if w.err != nil {
415+ return false
416+ }
417+ if w.limit >= 0 && n > w.limit-w.b.Len() {
418+ w.err = errJSONTooBig
419+ return false
420+ }
421+ return true
422+}
423+
424+func (w *jsonWriter) str(s string) {
425+ if w.grow(len(s)) {
426+ w.b.WriteString(s)
427+ }
428+}
429+
430+func (w *jsonWriter) indent(depth int) {
431+ if w.grow(2 * depth) {
432+ for i := 0; i < depth; i++ {
433+ w.b.WriteString(" ")
434+ }
435+ }
436+}
437+
438+func (w *jsonWriter) value(v Value, depth int) {
439+ if w.err != nil {
440+ return
441+ }
442+ if depth > MaxJSONDepth {
443+ w.err = errJSONTooDeep
444+ return
445+ }
386446 switch x := normalize(v).(type) {
387447 case Null:
388- b.WriteString("null")
448+ w.str("null")
389449 case Boolean:
390450 if x {
391- b.WriteString("true")
451+ w.str("true")
392452 } else {
393- b.WriteString("false")
453+ w.str("false")
394454 }
395455 case Number:
396456 f := float64(x)
397457 if math.IsNaN(f) || math.IsInf(f, 0) {
398- b.WriteString("null")
458+ w.str("null")
399459 } else {
400- b.WriteString(jsNumberToString(f))
460+ w.str(jsNumberToString(f))
401461 }
402462 case String:
403- writeJSONString(b, string(x))
463+ w.jsonString(string(x))
404464 case *Array:
405465 if len(x.items) == 0 {
406- b.WriteString("[]")
466+ w.str("[]")
407467 return
408468 }
409- b.WriteString("[\n")
469+ w.str("[\n")
410470 for i, it := range x.items {
411- indent(b, depth+1)
412- writeJSON(b, it, depth+1)
471+ w.indent(depth + 1)
472+ w.value(it, depth+1)
413473 if i < len(x.items)-1 {
414- b.WriteByte(',')
474+ w.str(",")
475+ }
476+ w.str("\n")
477+ if w.err != nil {
478+ return
415479 }
416- b.WriteByte('\n')
417480 }
418- indent(b, depth)
419- b.WriteByte(']')
481+ w.indent(depth)
482+ w.str("]")
420483 case *Dictionary:
421484 if len(x.keys) == 0 {
422- b.WriteString("{}")
485+ w.str("{}")
423486 return
424487 }
425488 keys := jsKeyOrder(x.keys)
426- b.WriteString("{\n")
489+ w.str("{\n")
427490 for i, k := range keys {
428491 val, _ := x.Get(k)
429- indent(b, depth+1)
430- writeJSONString(b, k)
431- b.WriteString(": ")
432- writeJSON(b, val, depth+1)
492+ w.indent(depth + 1)
493+ w.jsonString(k)
494+ w.str(": ")
495+ w.value(val, depth+1)
433496 if i < len(keys)-1 {
434- b.WriteByte(',')
497+ w.str(",")
498+ }
499+ w.str("\n")
500+ if w.err != nil {
501+ return
435502 }
436- b.WriteByte('\n')
437503 }
438- indent(b, depth)
439- b.WriteByte('}')
440- }
441-}
442-
443-func indent(b *strings.Builder, depth int) {
444- for i := 0; i < depth; i++ {
445- b.WriteString(" ")
504+ w.indent(depth)
505+ w.str("}")
446506 }
447507 }
448508
449509 const hexDigits = "0123456789abcdef"
450510
451-func writeJSONString(b *strings.Builder, s string) {
452- b.WriteByte('"')
511+func (w *jsonWriter) jsonString(s string) {
512+ if !w.grow(len(s) + 2) {
513+ return
514+ }
515+ w.b.WriteByte('"')
453516 for i := 0; i < len(s); {
454- r, w := utf8.DecodeRuneInString(s[i:])
455- if r == utf8.RuneError && w == 1 && i+2 < len(s) && s[i] == 0xED && s[i+1] >= 0xA0 && s[i+1] <= 0xBF && s[i+2]&0xC0 == 0x80 {
456- // WTF-8 lone surrogate (see FromJSON).
457- sur := rune(s[i]&0x0F)<<12 | rune(s[i+1]&0x3F)<<6 | rune(s[i+2]&0x3F)
458- b.WriteString(`\u`)
459- b.WriteByte(hexDigits[sur>>12])
460- b.WriteByte(hexDigits[sur>>8&0xF])
461- b.WriteByte(hexDigits[sur>>4&0xF])
462- b.WriteByte(hexDigits[sur&0xF])
463- i += 3
464- continue
465- }
466- i += w
517+ r, n := decodeRune(s[i:])
518+ i += n
519+ var esc string
467520 switch {
468521 case r == '"':
469- b.WriteString(`\"`)
522+ esc = `\"`
470523 case r == '\\':
471- b.WriteString(`\\`)
524+ esc = `\\`
472525 case r == '\b':
473- b.WriteString(`\b`)
526+ esc = `\b`
474527 case r == '\f':
475- b.WriteString(`\f`)
528+ esc = `\f`
476529 case r == '\n':
477- b.WriteString(`\n`)
530+ esc = `\n`
478531 case r == '\r':
479- b.WriteString(`\r`)
532+ esc = `\r`
480533 case r == '\t':
481- b.WriteString(`\t`)
534+ esc = `\t`
482535 case r < 0x20:
483- b.WriteString(`\u00`)
484- b.WriteByte(hexDigits[r>>4])
485- b.WriteByte(hexDigits[r&0xF])
486- default:
487- b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD
536+ esc = `\u00` + string([]byte{hexDigits[r>>4], hexDigits[r&0xF]})
537+ case r >= 0xD800 && r < 0xE000:
538+ // A lone surrogate (WTF-8, see FromJSON).
539+ esc = `\u` + string([]byte{hexDigits[r>>12], hexDigits[r>>8&0xF], hexDigits[r>>4&0xF], hexDigits[r&0xF]})
540+ }
541+ if esc != "" {
542+ if !w.grow(len(esc)) {
543+ return
544+ }
545+ w.b.WriteString(esc)
546+ continue
488547 }
548+ if !w.grow(utf8.RuneLen(r)) {
549+ return
550+ }
551+ w.b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD
552+ }
553+ if w.grow(1) {
554+ w.b.WriteByte('"')
489555 }
490- b.WriteByte('"')
491556 }
@@ -12,10 +12,12 @@ import (
12 "unicode/utf8"12 "unicode/utf8"
13 )13 )
14 14
15-// maxJSONDepth bounds the nesting FromJSON accepts. JavaScript engines fail15+// MaxJSONDepth bounds the nesting that FromJSON accepts and that ToJSON and
16-// with a stack overflow somewhere in that region, which fromJSON reports like16+// EncodeJSON write. JavaScript engines fail with a stack overflow somewhere
17-// any other parse error.17+// in that region; fromJSON reports it like any other parse error. The bound
18-const maxJSONDepth = 1000018+// also stops the encoder on cyclic values, which can be built through
19+// [Array.Append].
20+const MaxJSONDepth = 1000
19 21
20 // FromJSON parses JSON text into a [Value], like the fromJSON function.22 // FromJSON parses JSON text into a [Value], like the fromJSON function.
21 //23 //
@@ -80,7 +82,7 @@ func (p *jsonParser) literal(word string, v Value) (Value, error) {
80 }82 }
81 83
82 func (p *jsonParser) value(depth int) (Value, error) {84 func (p *jsonParser) value(depth int) (Value, error) {
83- if depth > maxJSONDepth {85+ if depth > MaxJSONDepth {
84 return nil, errors.New("maximum JSON nesting depth exceeded")86 return nil, errors.New("maximum JSON nesting depth exceeded")
85 }87 }
86 if p.i >= len(p.s) {88 if p.i >= len(p.s) {
@@ -204,16 +206,10 @@ func (p *jsonParser) str() (string, error) {
204 p.i = save // not a pair; the second escape is read on its own206 p.i = save // not a pair; the second escape is read on its own
205 }207 }
206 }208 }
207- if r >= 0xD800 && r < 0xE000 {209+ // A lone surrogate cannot be a Go rune: writeRune keeps it in
208- // A lone surrogate cannot be a Go rune. Keep it as its210+ // its three-byte WTF-8 form so that toJSON writes it back as
209- // three-byte WTF-8 form so that toJSON writes it back as211+ // the same \uXXXX escape, like JSON.stringify.
210- // the same \uXXXX escape, like JSON.stringify.212+ writeRune(&b, r)
211- b.WriteByte(0xE0 | byte(r>>12))
212- b.WriteByte(0x80 | byte(r>>6)&0x3F)
213- b.WriteByte(0x80 | byte(r)&0x3F)
214- continue
215- }
216- b.WriteRune(r)
217 default:213 default:
218 p.i--214 p.i--
219 return "", p.unexpected()215 return "", p.unexpected()
@@ -376,116 +372,185 @@ func jsKeyOrder(keys []string) []string {
376 // JavaScript object. Control characters, quotes and backslashes in strings372 // JavaScript object. Control characters, quotes and backslashes in strings
377 // are escaped; everything else, including non-ASCII text, is written as is.373 // are escaped; everything else, including non-ASCII text, is written as is.
378 // Lone surrogates, which FromJSON keeps in WTF-8 form, are written as \uXXXX.374 // Lone surrogates, which FromJSON keeps in WTF-8 form, are written as \uXXXX.
375+//
376+// ToJSON suits the finite values that FromJSON and Evaluate produce. A value
377+// nested deeper than [MaxJSONDepth], which only a cyclic value built by hand
378+// can be, makes it return "null"; use [EncodeJSON] to get an error instead
379+// and to bound the output.
379 func ToJSON(v Value) string {380 func ToJSON(v Value) string {
380- var b strings.Builder381+ s, err := EncodeJSON(v, -1)
381- writeJSON(&b, v, 0)382+ if err != nil {
382- return b.String()383+ return "null"
384+ }
385+ return s
386+}
387+
388+// EncodeJSON is [ToJSON] with limits: it fails when v is nested deeper than
389+// [MaxJSONDepth] and, when maxBytes is not negative, as soon as the output
390+// would exceed maxBytes bytes, without building the rest.
391+func EncodeJSON(v Value, maxBytes int) (string, error) {
392+ w := &jsonWriter{limit: maxBytes}
393+ w.value(v, 0)
394+ if w.err != nil {
395+ return "", w.err
396+ }
397+ return w.b.String(), nil
398+}
399+
400+var (
401+ errJSONTooDeep = errors.New("value nested too deeply to encode as JSON (a cycle?)")
402+ errJSONTooBig = errors.New("JSON output exceeds the size limit")
403+)
404+
405+type jsonWriter struct {
406+ b strings.Builder
407+ limit int // negative: unlimited
408+ err error
383 }409 }
384 410
385-func writeJSON(b *strings.Builder, v Value, depth int) {411+// grow reports whether n more bytes may be written, recording the error
412+// otherwise.
413+func (w *jsonWriter) grow(n int) bool {
414+ if w.err != nil {
415+ return false
416+ }
417+ if w.limit >= 0 && n > w.limit-w.b.Len() {
418+ w.err = errJSONTooBig
419+ return false
420+ }
421+ return true
422+}
423+
424+func (w *jsonWriter) str(s string) {
425+ if w.grow(len(s)) {
426+ w.b.WriteString(s)
427+ }
428+}
429+
430+func (w *jsonWriter) indent(depth int) {
431+ if w.grow(2 * depth) {
432+ for i := 0; i < depth; i++ {
433+ w.b.WriteString(" ")
434+ }
435+ }
436+}
437+
438+func (w *jsonWriter) value(v Value, depth int) {
439+ if w.err != nil {
440+ return
441+ }
442+ if depth > MaxJSONDepth {
443+ w.err = errJSONTooDeep
444+ return
445+ }
386 switch x := normalize(v).(type) {446 switch x := normalize(v).(type) {
387 case Null:447 case Null:
388- b.WriteString("null")448+ w.str("null")
389 case Boolean:449 case Boolean:
390 if x {450 if x {
391- b.WriteString("true")451+ w.str("true")
392 } else {452 } else {
393- b.WriteString("false")453+ w.str("false")
394 }454 }
395 case Number:455 case Number:
396 f := float64(x)456 f := float64(x)
397 if math.IsNaN(f) || math.IsInf(f, 0) {457 if math.IsNaN(f) || math.IsInf(f, 0) {
398- b.WriteString("null")458+ w.str("null")
399 } else {459 } else {
400- b.WriteString(jsNumberToString(f))460+ w.str(jsNumberToString(f))
401 }461 }
402 case String:462 case String:
403- writeJSONString(b, string(x))463+ w.jsonString(string(x))
404 case *Array:464 case *Array:
405 if len(x.items) == 0 {465 if len(x.items) == 0 {
406- b.WriteString("[]")466+ w.str("[]")
407 return467 return
408 }468 }
409- b.WriteString("[\n")469+ w.str("[\n")
410 for i, it := range x.items {470 for i, it := range x.items {
411- indent(b, depth+1)471+ w.indent(depth + 1)
412- writeJSON(b, it, depth+1)472+ w.value(it, depth+1)
413 if i < len(x.items)-1 {473 if i < len(x.items)-1 {
414- b.WriteByte(',')474+ w.str(",")
475+ }
476+ w.str("\n")
477+ if w.err != nil {
478+ return
415 }479 }
416- b.WriteByte('\n')
417 }480 }
418- indent(b, depth)481+ w.indent(depth)
419- b.WriteByte(']')482+ w.str("]")
420 case *Dictionary:483 case *Dictionary:
421 if len(x.keys) == 0 {484 if len(x.keys) == 0 {
422- b.WriteString("{}")485+ w.str("{}")
423 return486 return
424 }487 }
425 keys := jsKeyOrder(x.keys)488 keys := jsKeyOrder(x.keys)
426- b.WriteString("{\n")489+ w.str("{\n")
427 for i, k := range keys {490 for i, k := range keys {
428 val, _ := x.Get(k)491 val, _ := x.Get(k)
429- indent(b, depth+1)492+ w.indent(depth + 1)
430- writeJSONString(b, k)493+ w.jsonString(k)
431- b.WriteString(": ")494+ w.str(": ")
432- writeJSON(b, val, depth+1)495+ w.value(val, depth+1)
433 if i < len(keys)-1 {496 if i < len(keys)-1 {
434- b.WriteByte(',')497+ w.str(",")
498+ }
499+ w.str("\n")
500+ if w.err != nil {
501+ return
435 }502 }
436- b.WriteByte('\n')
437 }503 }
438- indent(b, depth)504+ w.indent(depth)
439- b.WriteByte('}')505+ w.str("}")
440- }
441-}
442-
443-func indent(b *strings.Builder, depth int) {
444- for i := 0; i < depth; i++ {
445- b.WriteString(" ")
446 }506 }
447 }507 }
448 508
449 const hexDigits = "0123456789abcdef"509 const hexDigits = "0123456789abcdef"
450 510
451-func writeJSONString(b *strings.Builder, s string) {511+func (w *jsonWriter) jsonString(s string) {
452- b.WriteByte('"')512+ if !w.grow(len(s) + 2) {
513+ return
514+ }
515+ w.b.WriteByte('"')
453 for i := 0; i < len(s); {516 for i := 0; i < len(s); {
454- r, w := utf8.DecodeRuneInString(s[i:])517+ r, n := decodeRune(s[i:])
455- if r == utf8.RuneError && w == 1 && i+2 < len(s) && s[i] == 0xED && s[i+1] >= 0xA0 && s[i+1] <= 0xBF && s[i+2]&0xC0 == 0x80 {518+ i += n
456- // WTF-8 lone surrogate (see FromJSON).519+ var esc string
457- sur := rune(s[i]&0x0F)<<12 | rune(s[i+1]&0x3F)<<6 | rune(s[i+2]&0x3F)
458- b.WriteString(`\u`)
459- b.WriteByte(hexDigits[sur>>12])
460- b.WriteByte(hexDigits[sur>>8&0xF])
461- b.WriteByte(hexDigits[sur>>4&0xF])
462- b.WriteByte(hexDigits[sur&0xF])
463- i += 3
464- continue
465- }
466- i += w
467 switch {520 switch {
468 case r == '"':521 case r == '"':
469- b.WriteString(`\"`)522+ esc = `\"`
470 case r == '\\':523 case r == '\\':
471- b.WriteString(`\\`)524+ esc = `\\`
472 case r == '\b':525 case r == '\b':
473- b.WriteString(`\b`)526+ esc = `\b`
474 case r == '\f':527 case r == '\f':
475- b.WriteString(`\f`)528+ esc = `\f`
476 case r == '\n':529 case r == '\n':
477- b.WriteString(`\n`)530+ esc = `\n`
478 case r == '\r':531 case r == '\r':
479- b.WriteString(`\r`)532+ esc = `\r`
480 case r == '\t':533 case r == '\t':
481- b.WriteString(`\t`)534+ esc = `\t`
482 case r < 0x20:535 case r < 0x20:
483- b.WriteString(`\u00`)536+ esc = `\u00` + string([]byte{hexDigits[r>>4], hexDigits[r&0xF]})
484- b.WriteByte(hexDigits[r>>4])537+ case r >= 0xD800 && r < 0xE000:
485- b.WriteByte(hexDigits[r&0xF])538+ // A lone surrogate (WTF-8, see FromJSON).
486- default:539+ esc = `\u` + string([]byte{hexDigits[r>>12], hexDigits[r>>8&0xF], hexDigits[r>>4&0xF], hexDigits[r&0xF]})
487- b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD540+ }
541+ if esc != "" {
542+ if !w.grow(len(esc)) {
543+ return
544+ }
545+ w.b.WriteString(esc)
546+ continue
488 }547 }
548+ if !w.grow(utf8.RuneLen(r)) {
549+ return
550+ }
551+ w.b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD
552+ }
553+ if w.grow(1) {
554+ w.b.WriteByte('"')
489 }555 }
490- b.WriteByte('"')
491 }556 }
M expr/json_test.go +3 -3
@@ -93,11 +93,11 @@ func TestFromJSONInvalidUTF8(t *testing.T) {
9393 }
9494
9595 func TestFromJSONDeepNesting(t *testing.T) {
96- deep := strings.Repeat("[", 5000) + strings.Repeat("]", 5000)
96+ deep := strings.Repeat("[", 900) + strings.Repeat("]", 900)
9797 if _, err := FromJSON([]byte(deep)); err != nil {
98- t.Errorf("depth 5000: %v", err)
98+ t.Errorf("depth 900: %v", err)
9999 }
100- tooDeep := strings.Repeat("[", maxJSONDepth+10) + strings.Repeat("]", maxJSONDepth+10)
100+ tooDeep := strings.Repeat("[", MaxJSONDepth+10) + strings.Repeat("]", MaxJSONDepth+10)
101101 if _, err := FromJSON([]byte(tooDeep)); err == nil {
102102 t.Error("absurd depth accepted")
103103 }
@@ -93,11 +93,11 @@ func TestFromJSONInvalidUTF8(t *testing.T) {
93 }93 }
94 94
95 func TestFromJSONDeepNesting(t *testing.T) {95 func TestFromJSONDeepNesting(t *testing.T) {
96- deep := strings.Repeat("[", 5000) + strings.Repeat("]", 5000)96+ deep := strings.Repeat("[", 900) + strings.Repeat("]", 900)
97 if _, err := FromJSON([]byte(deep)); err != nil {97 if _, err := FromJSON([]byte(deep)); err != nil {
98- t.Errorf("depth 5000: %v", err)98+ t.Errorf("depth 900: %v", err)
99 }99 }
100- tooDeep := strings.Repeat("[", maxJSONDepth+10) + strings.Repeat("]", maxJSONDepth+10)100+ tooDeep := strings.Repeat("[", MaxJSONDepth+10) + strings.Repeat("]", MaxJSONDepth+10)
101 if _, err := FromJSON([]byte(tooDeep)); err == nil {101 if _, err := FromJSON([]byte(tooDeep)); err == nil {
102 t.Error("absurd depth accepted")102 t.Error("absurd depth accepted")
103 }103 }
M expr/lexer.go +5 -3
@@ -84,12 +84,14 @@ func tokenString(t Token) string {
8484 // Lex splits an expression into tokens. The result always ends with a
8585 // TokenEOF token.
8686 func Lex(input string) ([]Token, error) {
87- units := utf16.Encode([]rune(input))
88- if len(units) > MaxExpressionLength {
87+ if exceedsUTF16Len(input, MaxExpressionLength) {
8988 // The reference throws a bare Error("ErrorExceededMaxLength") here,
90- // not the descriptive ExpressionError the parser would build.
89+ // not the descriptive ExpressionError the parser would build. The
90+ // length is checked before the input is converted so that a huge
91+ // input costs no allocation.
9192 return nil, &Error{Type: ErrorExceededMaxLength, Message: "ErrorExceededMaxLength"}
9293 }
94+ units := utf16.Encode([]rune(input))
9395 lx := &lexer{input: string(utf16.Decode(units)), units: units}
9496 if err := lx.lex(); err != nil {
9597 return nil, err
@@ -84,12 +84,14 @@ func tokenString(t Token) string {
84 // Lex splits an expression into tokens. The result always ends with a84 // Lex splits an expression into tokens. The result always ends with a
85 // TokenEOF token.85 // TokenEOF token.
86 func Lex(input string) ([]Token, error) {86 func Lex(input string) ([]Token, error) {
87- units := utf16.Encode([]rune(input))87+ if exceedsUTF16Len(input, MaxExpressionLength) {
88- if len(units) > MaxExpressionLength {
89 // The reference throws a bare Error("ErrorExceededMaxLength") here,88 // The reference throws a bare Error("ErrorExceededMaxLength") here,
90- // not the descriptive ExpressionError the parser would build.89+ // not the descriptive ExpressionError the parser would build. The
90+ // length is checked before the input is converted so that a huge
91+ // input costs no allocation.
91 return nil, &Error{Type: ErrorExceededMaxLength, Message: "ErrorExceededMaxLength"}92 return nil, &Error{Type: ErrorExceededMaxLength, Message: "ErrorExceededMaxLength"}
92 }93 }
94+ units := utf16.Encode([]rune(input))
93 lx := &lexer{input: string(utf16.Decode(units)), units: units}95 lx := &lexer{input: string(utf16.Decode(units)), units: units}
94 if err := lx.lex(); err != nil {96 if err := lx.lex(); err != nil {
95 return nil, err97 return nil, err
M expr/oracle_test.go +172 -104
@@ -4,9 +4,13 @@ package expr
44
55 import (
66 "compress/gzip"
7+ "context"
78 "encoding/json"
9+ "errors"
810 "fmt"
11+ "math"
912 "os"
13+ "strings"
1014 "testing"
1115 )
1216
@@ -18,11 +22,20 @@ import (
1822 // string coercions and toJSON output.
1923
2024 type oracleFile struct {
21- Reference string `json:"reference"`
25+ Reference oracleReference `json:"reference"`
2226 Contexts map[string]json.RawMessage `json:"contexts"`
2327 Cases []oracleCase `json:"cases"`
2428 }
2529
30+// oracleReference records where the oracle comes from.
31+type oracleReference struct {
32+ Package string `json:"package"`
33+ Integrity string `json:"integrity"`
34+ Node string `json:"node"`
35+ V8 string `json:"v8"`
36+ Unicode string `json:"unicode"`
37+}
38+
2639 type oracleCase struct {
2740 E string `json:"e"`
2841 Tokens [][]any `json:"tokens"`
@@ -38,6 +51,9 @@ type oracleResult struct {
3851 K string `json:"k"`
3952 S string `json:"s"`
4053 J string `json:"j"`
54+ // U holds the UTF-16 code units of the string form when it contains a
55+ // lone surrogate and S is therefore empty.
56+ U []uint16 `json:"u"`
4157 }
4258
4359 var oracleFuncInfos = []FuncInfo{
@@ -47,9 +63,9 @@ var oracleFuncInfos = []FuncInfo{
4763 }
4864
4965 var oracleFuncs = map[string]Func{
50- "success": func([]Value) (Value, error) { return Boolean(true), nil },
51- "always": func([]Value) (Value, error) { return Boolean(true), nil },
52- "hashFiles": func(args []Value) (Value, error) {
66+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
67+ "always": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
68+ "hashFiles": func(_ context.Context, args []Value) (Value, error) {
5369 s := "hash:"
5470 for i, a := range args {
5571 if i > 0 {
@@ -105,124 +121,176 @@ func loadOracle(tb testing.TB) (*oracleFile, map[string]Value, []string) {
105121 return &of, ctx, names
106122 }
107123
108-func TestOracle(t *testing.T) {
109- of, ctxValues, ctxNames := loadOracle(t)
110- if len(of.Cases) < 30000 {
111- t.Fatalf("oracle has only %d cases", len(of.Cases))
124+// oracleRun carries the state shared by the checks of one oracle case.
125+type oracleRun struct {
126+ t *testing.T
127+ ec EvalContext
128+ ctxNames []string
129+ failures int
130+ c oracleCase
131+}
132+
133+func (r *oracleRun) fail(format string, args ...any) {
134+ r.failures++
135+ if r.failures <= 40 {
136+ e := r.c.E
137+ if len(e) > 200 {
138+ e = e[:200] + "..."
139+ }
140+ r.t.Errorf("%q: %s", e, fmt.Sprintf(format, args...))
112141 }
113- ec := EvalContext{Contexts: ctxValues, Funcs: oracleFuncs}
114- failures := 0
115- fail := func(c oracleCase, format string, args ...any) {
116- failures++
117- if failures <= 40 {
118- e := c.E
119- if len(e) > 200 {
120- e = e[:200] + "..."
121- }
122- t.Errorf("%q: %s", e, fmt.Sprintf(format, args...))
142+}
143+
144+// checkLex compares the tokens and reports whether the case lexes.
145+func (r *oracleRun) checkLex() bool {
146+ c := r.c
147+ tokens, err := Lex(c.E)
148+ if c.LexErr != nil {
149+ switch {
150+ case err == nil:
151+ r.fail("lex: want error %q, got none", *c.LexErr)
152+ case err.Error() != *c.LexErr:
153+ r.fail("lex error: want %q, got %q", *c.LexErr, err.Error())
123154 }
155+ return false
124156 }
125- var counts struct{ lexErr, parseErr, evalErr, ok, empty int }
126- for _, c := range of.Cases {
127- tokens, err := Lex(c.E)
128- if c.LexErr != nil {
129- counts.lexErr++
130- if err == nil {
131- fail(c, "lex: want error %q, got none", *c.LexErr)
132- } else if err.Error() != *c.LexErr {
133- fail(c, "lex error: want %q, got %q", *c.LexErr, err.Error())
157+ if err != nil {
158+ r.fail("lex: unexpected error %v", err)
159+ return false
160+ }
161+ if len(tokens) != len(c.Tokens) {
162+ r.fail("lex: %d tokens, want %d", len(tokens), len(c.Tokens))
163+ return false
164+ }
165+ for i, tk := range tokens {
166+ w := c.Tokens[i]
167+ got := []any{float64(tk.Type), tk.Lexeme, float64(tk.Range.Start.Line), float64(tk.Range.Start.Column), float64(tk.Range.End.Line), float64(tk.Range.End.Column)}
168+ for j := range got {
169+ if got[j] != w[j] {
170+ r.fail("token %d field %d: got %v, want %v", i, j, got[j], w[j])
134171 }
135- continue
136172 }
137- if err != nil {
138- fail(c, "lex: unexpected error %v", err)
139- continue
140- }
141- if len(tokens) != len(c.Tokens) {
142- fail(c, "lex: %d tokens, want %d", len(tokens), len(c.Tokens))
143- continue
144- }
145- for i, tk := range tokens {
146- w := c.Tokens[i]
147- got := []any{float64(tk.Type), tk.Lexeme, float64(tk.Range.Start.Line), float64(tk.Range.Start.Column), float64(tk.Range.End.Line), float64(tk.Range.End.Column)}
148- for j := range got {
149- if got[j] != w[j] {
150- fail(c, "token %d field %d: got %v, want %v", i, j, got[j], w[j])
151- }
152- }
153- var gv any
154- switch tk.Type {
155- case TokenNumber:
156- f := float64(tk.Value.(Number))
157- if f != f {
158- gv = "NaN"
159- } else {
160- gv = jsNumberToString(f)
161- }
162- case TokenString:
163- gv = string(tk.Value.(String))
164- }
165- if gv != w[6] {
166- fail(c, "token %d value: got %v, want %v", i, gv, w[6])
173+ var gv any
174+ switch tk.Type {
175+ case TokenNumber:
176+ f := float64(tk.Value.(Number))
177+ if math.IsNaN(f) {
178+ gv = "NaN"
179+ } else {
180+ gv = jsNumberToString(f)
167181 }
182+ case TokenString:
183+ gv = string(tk.Value.(String))
184+ }
185+ if gv != w[6] {
186+ r.fail("token %d value: got %v, want %v", i, gv, w[6])
168187 }
188+ }
189+ return true
190+}
169191
170- node, err := Parse(c.E, ctxNames, oracleFuncInfos)
171- if c.ParseErr != nil {
172- counts.parseErr++
173- var pe *Error
174- switch {
175- case err == nil:
176- fail(c, "parse: want error %q, got none", *c.ParseErr)
177- case err.Error() != *c.ParseErr:
178- fail(c, "parse error: want %q, got %q", *c.ParseErr, err.Error())
179- case func() bool { pe, _ = err.(*Error); return pe == nil }():
180- fail(c, "parse error has type %T", err)
181- case c.Pos != nil && (pe.Pos.Line != c.Pos[0] || pe.Pos.Column != c.Pos[1]):
182- fail(c, "parse error position: got %v, want %v", pe.Pos, c.Pos)
183- }
184- continue
192+// checkParse compares the parse outcome and returns the tree when the case
193+// parses to something to evaluate.
194+func (r *oracleRun) checkParse() Node {
195+ c := r.c
196+ node, err := Parse(c.E, r.ctxNames, oracleFuncInfos)
197+ if c.ParseErr != nil {
198+ var pe *Error
199+ switch {
200+ case err == nil:
201+ r.fail("parse: want error %q, got none", *c.ParseErr)
202+ case err.Error() != *c.ParseErr:
203+ r.fail("parse error: want %q, got %q", *c.ParseErr, err.Error())
204+ case !errors.As(err, &pe):
205+ r.fail("parse error has type %T", err)
206+ case c.Pos != nil && (pe.Pos.Line != c.Pos[0] || pe.Pos.Column != c.Pos[1]):
207+ r.fail("parse error position: got %v, want %v", pe.Pos, c.Pos)
185208 }
186- if err != nil {
187- fail(c, "parse: unexpected error %v", err)
188- continue
209+ return nil
210+ }
211+ if err != nil {
212+ r.fail("parse: unexpected error %v", err)
213+ return nil
214+ }
215+ if c.Empty {
216+ if node != nil {
217+ r.fail("parse: want empty")
189218 }
190- if c.Empty {
191- counts.empty++
192- if node != nil {
193- fail(c, "parse: want empty")
194- }
195- continue
219+ return nil
220+ }
221+ return node
222+}
223+
224+func (r *oracleRun) checkEval(node Node) {
225+ c := r.c
226+ v, err := Evaluate(context.Background(), node, r.ec)
227+ if c.EvalErr != nil {
228+ switch {
229+ case err == nil:
230+ r.fail("eval: want error %q, got %s", *c.EvalErr, CoerceString(v))
231+ case err.Error() != *c.EvalErr:
232+ r.fail("eval error: want %q, got %q", *c.EvalErr, err.Error())
196233 }
234+ return
235+ }
236+ if err != nil {
237+ r.fail("eval: unexpected error %v", err)
238+ return
239+ }
240+ if k := kindName(v); k != c.R.K {
241+ r.fail("result kind: got %s, want %s", k, c.R.K)
242+ }
243+ if c.R.U != nil {
244+ if got := utf16Units(CoerceString(v)); !unitsEqual(got, c.R.U) {
245+ r.fail("result string units: got %04x, want %04x", got, c.R.U)
246+ }
247+ } else if s := CoerceString(v); s != c.R.S {
248+ r.fail("result string: got %q, want %q", s, c.R.S)
249+ }
250+ if j := ToJSON(v); j != c.R.J {
251+ r.fail("result JSON: got %q, want %q", j, c.R.J)
252+ }
253+}
254+
255+func TestOracle(t *testing.T) {
256+ of, ctxValues, ctxNames := loadOracle(t)
257+ if len(of.Cases) < 30000 {
258+ t.Fatalf("oracle has only %d cases", len(of.Cases))
259+ }
260+ ref := of.Reference
261+ if !strings.HasPrefix(ref.Integrity, "sha512-") {
262+ t.Errorf("oracle does not record the integrity of the reference package: %+v", ref)
263+ }
264+ t.Logf("reference: %s, integrity %s, generated with node %s (V8 %s, Unicode %s)", ref.Package, ref.Integrity, ref.Node, ref.V8, ref.Unicode)
197265
198- v, err := Evaluate(node, ec)
199- if c.EvalErr != nil {
266+ run := &oracleRun{t: t, ctxNames: ctxNames, ec: EvalContext{Contexts: ctxValues, Funcs: oracleFuncs, MaxResultBytes: -1}}
267+ var counts struct{ lexErr, parseErr, evalErr, ok, empty int }
268+ for _, c := range of.Cases {
269+ run.c = c
270+ switch {
271+ case c.LexErr != nil:
272+ counts.lexErr++
273+ case c.ParseErr != nil:
274+ counts.parseErr++
275+ case c.Empty:
276+ counts.empty++
277+ case c.EvalErr != nil:
200278 counts.evalErr++
201- if err == nil {
202- fail(c, "eval: want error %q, got %s", *c.EvalErr, CoerceString(v))
203- } else if err.Error() != *c.EvalErr {
204- fail(c, "eval error: want %q, got %q", *c.EvalErr, err.Error())
205- }
206- continue
279+ default:
280+ counts.ok++
207281 }
208- if err != nil {
209- fail(c, "eval: unexpected error %v", err)
282+ if !run.checkLex() {
210283 continue
211284 }
212- counts.ok++
213- if k := kindName(v); k != c.R.K {
214- fail(c, "result kind: got %s, want %s", k, c.R.K)
215- }
216- if s := CoerceString(v); s != c.R.S {
217- fail(c, "result string: got %q, want %q", s, c.R.S)
218- }
219- if j := ToJSON(v); j != c.R.J {
220- fail(c, "result JSON: got %q, want %q", j, c.R.J)
285+ node := run.checkParse()
286+ if node == nil {
287+ continue
221288 }
289+ run.checkEval(node)
222290 }
223291 t.Logf("%d cases: %d lex errors, %d parse errors, %d empty, %d eval errors, %d values; %d failures",
224- len(of.Cases), counts.lexErr, counts.parseErr, counts.empty, counts.evalErr, counts.ok, failures)
225- if failures > 0 {
226- t.Fatalf("%d oracle mismatches", failures)
292+ len(of.Cases), counts.lexErr, counts.parseErr, counts.empty, counts.evalErr, counts.ok, run.failures)
293+ if run.failures > 0 {
294+ t.Fatalf("%d oracle mismatches", run.failures)
227295 }
228296 }
@@ -4,9 +4,13 @@ package expr
4 4
5 import (5 import (
6 "compress/gzip"6 "compress/gzip"
7+ "context"
7 "encoding/json"8 "encoding/json"
9+ "errors"
8 "fmt"10 "fmt"
11+ "math"
9 "os"12 "os"
13+ "strings"
10 "testing"14 "testing"
11 )15 )
12 16
@@ -18,11 +22,20 @@ import (
18 // string coercions and toJSON output.22 // string coercions and toJSON output.
19 23
20 type oracleFile struct {24 type oracleFile struct {
21- Reference string `json:"reference"`25+ Reference oracleReference `json:"reference"`
22 Contexts map[string]json.RawMessage `json:"contexts"`26 Contexts map[string]json.RawMessage `json:"contexts"`
23 Cases []oracleCase `json:"cases"`27 Cases []oracleCase `json:"cases"`
24 }28 }
25 29
30+// oracleReference records where the oracle comes from.
31+type oracleReference struct {
32+ Package string `json:"package"`
33+ Integrity string `json:"integrity"`
34+ Node string `json:"node"`
35+ V8 string `json:"v8"`
36+ Unicode string `json:"unicode"`
37+}
38+
26 type oracleCase struct {39 type oracleCase struct {
27 E string `json:"e"`40 E string `json:"e"`
28 Tokens [][]any `json:"tokens"`41 Tokens [][]any `json:"tokens"`
@@ -38,6 +51,9 @@ type oracleResult struct {
38 K string `json:"k"`51 K string `json:"k"`
39 S string `json:"s"`52 S string `json:"s"`
40 J string `json:"j"`53 J string `json:"j"`
54+ // U holds the UTF-16 code units of the string form when it contains a
55+ // lone surrogate and S is therefore empty.
56+ U []uint16 `json:"u"`
41 }57 }
42 58
43 var oracleFuncInfos = []FuncInfo{59 var oracleFuncInfos = []FuncInfo{
@@ -47,9 +63,9 @@ var oracleFuncInfos = []FuncInfo{
47 }63 }
48 64
49 var oracleFuncs = map[string]Func{65 var oracleFuncs = map[string]Func{
50- "success": func([]Value) (Value, error) { return Boolean(true), nil },66+ "success": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
51- "always": func([]Value) (Value, error) { return Boolean(true), nil },67+ "always": func(context.Context, []Value) (Value, error) { return Boolean(true), nil },
52- "hashFiles": func(args []Value) (Value, error) {68+ "hashFiles": func(_ context.Context, args []Value) (Value, error) {
53 s := "hash:"69 s := "hash:"
54 for i, a := range args {70 for i, a := range args {
55 if i > 0 {71 if i > 0 {
@@ -105,124 +121,176 @@ func loadOracle(tb testing.TB) (*oracleFile, map[string]Value, []string) {
105 return &of, ctx, names121 return &of, ctx, names
106 }122 }
107 123
108-func TestOracle(t *testing.T) {124+// oracleRun carries the state shared by the checks of one oracle case.
109- of, ctxValues, ctxNames := loadOracle(t)125+type oracleRun struct {
110- if len(of.Cases) < 30000 {126+ t *testing.T
111- t.Fatalf("oracle has only %d cases", len(of.Cases))127+ ec EvalContext
128+ ctxNames []string
129+ failures int
130+ c oracleCase
131+}
132+
133+func (r *oracleRun) fail(format string, args ...any) {
134+ r.failures++
135+ if r.failures <= 40 {
136+ e := r.c.E
137+ if len(e) > 200 {
138+ e = e[:200] + "..."
139+ }
140+ r.t.Errorf("%q: %s", e, fmt.Sprintf(format, args...))
112 }141 }
113- ec := EvalContext{Contexts: ctxValues, Funcs: oracleFuncs}142+}
114- failures := 0143+
115- fail := func(c oracleCase, format string, args ...any) {144+// checkLex compares the tokens and reports whether the case lexes.
116- failures++145+func (r *oracleRun) checkLex() bool {
117- if failures <= 40 {146+ c := r.c
118- e := c.E147+ tokens, err := Lex(c.E)
119- if len(e) > 200 {148+ if c.LexErr != nil {
120- e = e[:200] + "..."149+ switch {
121- }150+ case err == nil:
122- t.Errorf("%q: %s", e, fmt.Sprintf(format, args...))151+ r.fail("lex: want error %q, got none", *c.LexErr)
152+ case err.Error() != *c.LexErr:
153+ r.fail("lex error: want %q, got %q", *c.LexErr, err.Error())
123 }154 }
155+ return false
124 }156 }
125- var counts struct{ lexErr, parseErr, evalErr, ok, empty int }157+ if err != nil {
126- for _, c := range of.Cases {158+ r.fail("lex: unexpected error %v", err)
127- tokens, err := Lex(c.E)159+ return false
128- if c.LexErr != nil {160+ }
129- counts.lexErr++161+ if len(tokens) != len(c.Tokens) {
130- if err == nil {162+ r.fail("lex: %d tokens, want %d", len(tokens), len(c.Tokens))
131- fail(c, "lex: want error %q, got none", *c.LexErr)163+ return false
132- } else if err.Error() != *c.LexErr {164+ }
133- fail(c, "lex error: want %q, got %q", *c.LexErr, err.Error())165+ for i, tk := range tokens {
166+ w := c.Tokens[i]
167+ got := []any{float64(tk.Type), tk.Lexeme, float64(tk.Range.Start.Line), float64(tk.Range.Start.Column), float64(tk.Range.End.Line), float64(tk.Range.End.Column)}
168+ for j := range got {
169+ if got[j] != w[j] {
170+ r.fail("token %d field %d: got %v, want %v", i, j, got[j], w[j])
134 }171 }
135- continue
136 }172 }
137- if err != nil {173+ var gv any
138- fail(c, "lex: unexpected error %v", err)174+ switch tk.Type {
139- continue175+ case TokenNumber:
140- }176+ f := float64(tk.Value.(Number))
141- if len(tokens) != len(c.Tokens) {177+ if math.IsNaN(f) {
142- fail(c, "lex: %d tokens, want %d", len(tokens), len(c.Tokens))178+ gv = "NaN"
143- continue179+ } else {
144- }180+ gv = jsNumberToString(f)
145- for i, tk := range tokens {
146- w := c.Tokens[i]
147- got := []any{float64(tk.Type), tk.Lexeme, float64(tk.Range.Start.Line), float64(tk.Range.Start.Column), float64(tk.Range.End.Line), float64(tk.Range.End.Column)}
148- for j := range got {
149- if got[j] != w[j] {
150- fail(c, "token %d field %d: got %v, want %v", i, j, got[j], w[j])
151- }
152- }
153- var gv any
154- switch tk.Type {
155- case TokenNumber:
156- f := float64(tk.Value.(Number))
157- if f != f {
158- gv = "NaN"
159- } else {
160- gv = jsNumberToString(f)
161- }
162- case TokenString:
163- gv = string(tk.Value.(String))
164- }
165- if gv != w[6] {
166- fail(c, "token %d value: got %v, want %v", i, gv, w[6])
167 }181 }
182+ case TokenString:
183+ gv = string(tk.Value.(String))
184+ }
185+ if gv != w[6] {
186+ r.fail("token %d value: got %v, want %v", i, gv, w[6])
168 }187 }
188+ }
189+ return true
190+}
169 191
170- node, err := Parse(c.E, ctxNames, oracleFuncInfos)192+// checkParse compares the parse outcome and returns the tree when the case
171- if c.ParseErr != nil {193+// parses to something to evaluate.
172- counts.parseErr++194+func (r *oracleRun) checkParse() Node {
173- var pe *Error195+ c := r.c
174- switch {196+ node, err := Parse(c.E, r.ctxNames, oracleFuncInfos)
175- case err == nil:197+ if c.ParseErr != nil {
176- fail(c, "parse: want error %q, got none", *c.ParseErr)198+ var pe *Error
177- case err.Error() != *c.ParseErr:199+ switch {
178- fail(c, "parse error: want %q, got %q", *c.ParseErr, err.Error())200+ case err == nil:
179- case func() bool { pe, _ = err.(*Error); return pe == nil }():201+ r.fail("parse: want error %q, got none", *c.ParseErr)
180- fail(c, "parse error has type %T", err)202+ case err.Error() != *c.ParseErr:
181- case c.Pos != nil && (pe.Pos.Line != c.Pos[0] || pe.Pos.Column != c.Pos[1]):203+ r.fail("parse error: want %q, got %q", *c.ParseErr, err.Error())
182- fail(c, "parse error position: got %v, want %v", pe.Pos, c.Pos)204+ case !errors.As(err, &pe):
183- }205+ r.fail("parse error has type %T", err)
184- continue206+ case c.Pos != nil && (pe.Pos.Line != c.Pos[0] || pe.Pos.Column != c.Pos[1]):
207+ r.fail("parse error position: got %v, want %v", pe.Pos, c.Pos)
185 }208 }
186- if err != nil {209+ return nil
187- fail(c, "parse: unexpected error %v", err)210+ }
188- continue211+ if err != nil {
212+ r.fail("parse: unexpected error %v", err)
213+ return nil
214+ }
215+ if c.Empty {
216+ if node != nil {
217+ r.fail("parse: want empty")
189 }218 }
190- if c.Empty {219+ return nil
191- counts.empty++220+ }
192- if node != nil {221+ return node
193- fail(c, "parse: want empty")222+}
194- }223+
195- continue224+func (r *oracleRun) checkEval(node Node) {
225+ c := r.c
226+ v, err := Evaluate(context.Background(), node, r.ec)
227+ if c.EvalErr != nil {
228+ switch {
229+ case err == nil:
230+ r.fail("eval: want error %q, got %s", *c.EvalErr, CoerceString(v))
231+ case err.Error() != *c.EvalErr:
232+ r.fail("eval error: want %q, got %q", *c.EvalErr, err.Error())
196 }233 }
234+ return
235+ }
236+ if err != nil {
237+ r.fail("eval: unexpected error %v", err)
238+ return
239+ }
240+ if k := kindName(v); k != c.R.K {
241+ r.fail("result kind: got %s, want %s", k, c.R.K)
242+ }
243+ if c.R.U != nil {
244+ if got := utf16Units(CoerceString(v)); !unitsEqual(got, c.R.U) {
245+ r.fail("result string units: got %04x, want %04x", got, c.R.U)
246+ }
247+ } else if s := CoerceString(v); s != c.R.S {
248+ r.fail("result string: got %q, want %q", s, c.R.S)
249+ }
250+ if j := ToJSON(v); j != c.R.J {
251+ r.fail("result JSON: got %q, want %q", j, c.R.J)
252+ }
253+}
254+
255+func TestOracle(t *testing.T) {
256+ of, ctxValues, ctxNames := loadOracle(t)
257+ if len(of.Cases) < 30000 {
258+ t.Fatalf("oracle has only %d cases", len(of.Cases))
259+ }
260+ ref := of.Reference
261+ if !strings.HasPrefix(ref.Integrity, "sha512-") {
262+ t.Errorf("oracle does not record the integrity of the reference package: %+v", ref)
263+ }
264+ t.Logf("reference: %s, integrity %s, generated with node %s (V8 %s, Unicode %s)", ref.Package, ref.Integrity, ref.Node, ref.V8, ref.Unicode)
197 265
198- v, err := Evaluate(node, ec)266+ run := &oracleRun{t: t, ctxNames: ctxNames, ec: EvalContext{Contexts: ctxValues, Funcs: oracleFuncs, MaxResultBytes: -1}}
199- if c.EvalErr != nil {267+ var counts struct{ lexErr, parseErr, evalErr, ok, empty int }
268+ for _, c := range of.Cases {
269+ run.c = c
270+ switch {
271+ case c.LexErr != nil:
272+ counts.lexErr++
273+ case c.ParseErr != nil:
274+ counts.parseErr++
275+ case c.Empty:
276+ counts.empty++
277+ case c.EvalErr != nil:
200 counts.evalErr++278 counts.evalErr++
201- if err == nil {279+ default:
202- fail(c, "eval: want error %q, got %s", *c.EvalErr, CoerceString(v))280+ counts.ok++
203- } else if err.Error() != *c.EvalErr {
204- fail(c, "eval error: want %q, got %q", *c.EvalErr, err.Error())
205- }
206- continue
207 }281 }
208- if err != nil {282+ if !run.checkLex() {
209- fail(c, "eval: unexpected error %v", err)
210 continue283 continue
211 }284 }
212- counts.ok++285+ node := run.checkParse()
213- if k := kindName(v); k != c.R.K {286+ if node == nil {
214- fail(c, "result kind: got %s, want %s", k, c.R.K)287+ continue
215- }
216- if s := CoerceString(v); s != c.R.S {
217- fail(c, "result string: got %q, want %q", s, c.R.S)
218- }
219- if j := ToJSON(v); j != c.R.J {
220- fail(c, "result JSON: got %q, want %q", j, c.R.J)
221 }288 }
289+ run.checkEval(node)
222 }290 }
223 t.Logf("%d cases: %d lex errors, %d parse errors, %d empty, %d eval errors, %d values; %d failures",291 t.Logf("%d cases: %d lex errors, %d parse errors, %d empty, %d eval errors, %d values; %d failures",
224- len(of.Cases), counts.lexErr, counts.parseErr, counts.empty, counts.evalErr, counts.ok, failures)292+ len(of.Cases), counts.lexErr, counts.parseErr, counts.empty, counts.evalErr, counts.ok, run.failures)
225- if failures > 0 {293+ if run.failures > 0 {
226- t.Fatalf("%d oracle mismatches", failures)294+ t.Fatalf("%d oracle mismatches", run.failures)
227 }295 }
228 }296 }
M expr/parser.go +16 -8
@@ -268,18 +268,26 @@ func (p *parser) validateFunction(ident Token, argCount int) error {
268268 return newTokenError(ErrorUnrecognizedFunction, ident)
269269 }
270270 }
271- if argCount < f.MinArgs {
272- return newTokenError(ErrorTooFewParameters, ident)
273- }
274- if f.MaxArgs >= 0 && argCount > f.MaxArgs {
275- return newTokenError(ErrorTooManyParameters, ident)
276- }
277- if name == "case" && argCount%2 == 0 {
278- return newTokenError(ErrorEvenParameters, ident)
271+ if t, bad := arityError(name, f, argCount); bad {
272+ return newTokenError(t, ident)
279273 }
280274 return nil
281275 }
282276
277+// arityError reports whether argCount is wrong for f, and how. name is the
278+// lower-cased function name: case also needs an odd number of arguments.
279+func arityError(name string, f FuncInfo, argCount int) (ErrorType, bool) {
280+ switch {
281+ case argCount < f.MinArgs:
282+ return ErrorTooFewParameters, true
283+ case f.MaxArgs >= 0 && argCount > f.MaxArgs:
284+ return ErrorTooManyParameters, true
285+ case name == "case" && argCount%2 == 0:
286+ return ErrorEvenParameters, true
287+ }
288+ return 0, false
289+}
290+
283291 func (p *parser) primary() (Node, error) {
284292 switch {
285293 case p.match(TokenFalse):
@@ -268,18 +268,26 @@ func (p *parser) validateFunction(ident Token, argCount int) error {
268 return newTokenError(ErrorUnrecognizedFunction, ident)268 return newTokenError(ErrorUnrecognizedFunction, ident)
269 }269 }
270 }270 }
271- if argCount < f.MinArgs {271+ if t, bad := arityError(name, f, argCount); bad {
272- return newTokenError(ErrorTooFewParameters, ident)272+ return newTokenError(t, ident)
273- }
274- if f.MaxArgs >= 0 && argCount > f.MaxArgs {
275- return newTokenError(ErrorTooManyParameters, ident)
276- }
277- if name == "case" && argCount%2 == 0 {
278- return newTokenError(ErrorEvenParameters, ident)
279 }273 }
280 return nil274 return nil
281 }275 }
282 276
277+// arityError reports whether argCount is wrong for f, and how. name is the
278+// lower-cased function name: case also needs an odd number of arguments.
279+func arityError(name string, f FuncInfo, argCount int) (ErrorType, bool) {
280+ switch {
281+ case argCount < f.MinArgs:
282+ return ErrorTooFewParameters, true
283+ case f.MaxArgs >= 0 && argCount > f.MaxArgs:
284+ return ErrorTooManyParameters, true
285+ case name == "case" && argCount%2 == 0:
286+ return ErrorEvenParameters, true
287+ }
288+ return 0, false
289+}
290+
283 func (p *parser) primary() (Node, error) {291 func (p *parser) primary() (Node, error) {
284 switch {292 switch {
285 case p.match(TokenFalse):293 case p.match(TokenFalse):
M expr/testdata/SOURCE +7 -0
@@ -2,3 +2,10 @@ oracle.json.gz and tojson/*.golden are outputs of @actions/expressions 0.3.61
22 (https://github.com/actions/languageservices, MIT, Copyright GitHub, Inc.) run
33 on expressions and JSON inputs written for this repository. They are generated
44 by ../gen/oracle.mjs and ../gen/golden.mjs. See ../../THIRD_PARTY.md.
5+
6+Reference package: @actions/expressions 0.3.61
7+ integrity (sha512 of actions-expressions-0.3.61.tgz, computed locally in
8+ the format of the npm registry "dist.integrity" field):
9+ sha512-Ig+tXELvTjlqxQAWBQnLNn2dPpNraLMdSnT+vDzDPmJtbNUhIAH/Z9kVT8pZ8UtJi9vUGs8hwHE8JFJmbOnLEA==
10+Generated with: node v25.2.1 (V8 14.1.146.11-node.14, Unicode 16.0)
11+The same values are in the "reference" field of oracle.json.gz.
@@ -2,3 +2,10 @@ oracle.json.gz and tojson/*.golden are outputs of @actions/expressions 0.3.61
2 (https://github.com/actions/languageservices, MIT, Copyright GitHub, Inc.) run2 (https://github.com/actions/languageservices, MIT, Copyright GitHub, Inc.) run
3 on expressions and JSON inputs written for this repository. They are generated3 on expressions and JSON inputs written for this repository. They are generated
4 by ../gen/oracle.mjs and ../gen/golden.mjs. See ../../THIRD_PARTY.md.4 by ../gen/oracle.mjs and ../gen/golden.mjs. See ../../THIRD_PARTY.md.
5+
6+Reference package: @actions/expressions 0.3.61
7+ integrity (sha512 of actions-expressions-0.3.61.tgz, computed locally in
8+ the format of the npm registry "dist.integrity" field):
9+ sha512-Ig+tXELvTjlqxQAWBQnLNn2dPpNraLMdSnT+vDzDPmJtbNUhIAH/Z9kVT8pZ8UtJi9vUGs8hwHE8JFJmbOnLEA==
10+Generated with: node v25.2.1 (V8 14.1.146.11-node.14, Unicode 16.0)
11+The same values are in the "reference" field of oracle.json.gz.
M expr/testdata/oracle.json.gz +0 -0 binary file: no text diff

binary file: no text diff

M expr/testutil_test.go +5 -2
@@ -2,7 +2,10 @@
22
33 package expr
44
5-import "testing"
5+import (
6+ "context"
7+ "testing"
8+)
69
710 // mustJSON parses JSON text into a Value or fails the test.
811 func mustJSON(tb testing.TB, s string) Value {
@@ -51,7 +54,7 @@ func evalString(tb testing.TB, src string) string {
5154 if err != nil {
5255 return "PARSE " + err.Error()
5356 }
54- v, err := Evaluate(node, EvalContext{Contexts: testContexts(tb)})
57+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: testContexts(tb)})
5558 if err != nil {
5659 return "ERR " + err.Error()
5760 }
@@ -2,7 +2,10 @@
2 2
3 package expr3 package expr
4 4
5-import "testing"5+import (
6+ "context"
7+ "testing"
8+)
6 9
7 // mustJSON parses JSON text into a Value or fails the test.10 // mustJSON parses JSON text into a Value or fails the test.
8 func mustJSON(tb testing.TB, s string) Value {11 func mustJSON(tb testing.TB, s string) Value {
@@ -51,7 +54,7 @@ func evalString(tb testing.TB, src string) string {
51 if err != nil {54 if err != nil {
52 return "PARSE " + err.Error()55 return "PARSE " + err.Error()
53 }56 }
54- v, err := Evaluate(node, EvalContext{Contexts: testContexts(tb)})57+ v, err := Evaluate(context.Background(), node, EvalContext{Contexts: testContexts(tb)})
55 if err != nil {58 if err != nil {
56 return "ERR " + err.Error()59 return "ERR " + err.Error()
57 }60 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close