summaryrefslogtreecommitdiffstats
path: root/meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch
diff options
context:
space:
mode:
Diffstat (limited to 'meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch')
-rw-r--r--meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch585
1 files changed, 585 insertions, 0 deletions
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch b/meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch
new file mode 100644
index 0000000000..fc38929648
--- /dev/null
+++ b/meta/recipes-devtools/go/go-1.14/CVE-2023-24538_5.patch
@@ -0,0 +1,585 @@
1From e0e6bca6ddc0e6d9fa3a5b644af9b446924fbf83 Mon Sep 17 00:00:00 2001
2From: Russ Cox <rsc@golang.org>
3Date: Thu, 20 May 2021 12:46:33 -0400
4Subject: [PATCH 5/6] html/template, text/template: implement break and
5 continue for range loops
6
7Break and continue for range loops was accepted as a proposal in June 2017.
8It was implemented in CL 66410 (Oct 2017)
9but then rolled back in CL 92155 (Feb 2018)
10because html/template changes had not been implemented.
11
12This CL reimplements break and continue in text/template
13and then adds support for them in html/template as well.
14
15Fixes #20531.
16
17Change-Id: I05330482a976f1c078b4b49c2287bd9031bb7616
18Reviewed-on: https://go-review.googlesource.com/c/go/+/321491
19Trust: Russ Cox <rsc@golang.org>
20Run-TryBot: Russ Cox <rsc@golang.org>
21TryBot-Result: Go Bot <gobot@golang.org>
22Reviewed-by: Rob Pike <r@golang.org>
23
24Dependency Patch #5
25
26Upstream-Status: Backport from https://github.com/golang/go/commit/d0dd26a88c019d54f22463daae81e785f5867565
27CVE: CVE-2023-24538
28Signed-off-by: Shubham Kulkarni <skulkarni@mvista.com>
29---
30 src/html/template/context.go | 4 ++
31 src/html/template/escape.go | 71 ++++++++++++++++++++++++++++++++++-
32 src/html/template/escape_test.go | 24 ++++++++++++
33 src/text/template/doc.go | 8 ++++
34 src/text/template/exec.go | 24 +++++++++++-
35 src/text/template/exec_test.go | 2 +
36 src/text/template/parse/lex.go | 13 ++++++-
37 src/text/template/parse/lex_test.go | 2 +
38 src/text/template/parse/node.go | 36 ++++++++++++++++++
39 src/text/template/parse/parse.go | 42 ++++++++++++++++++++-
40 src/text/template/parse/parse_test.go | 8 ++++
41 11 files changed, 230 insertions(+), 4 deletions(-)
42
43diff --git a/src/html/template/context.go b/src/html/template/context.go
44index f7d4849..aaa7d08 100644
45--- a/src/html/template/context.go
46+++ b/src/html/template/context.go
47@@ -6,6 +6,7 @@ package template
48
49 import (
50 "fmt"
51+ "text/template/parse"
52 )
53
54 // context describes the state an HTML parser must be in when it reaches the
55@@ -22,6 +23,7 @@ type context struct {
56 jsCtx jsCtx
57 attr attr
58 element element
59+ n parse.Node // for range break/continue
60 err *Error
61 }
62
63@@ -141,6 +143,8 @@ const (
64 // stateError is an infectious error state outside any valid
65 // HTML/CSS/JS construct.
66 stateError
67+ // stateDead marks unreachable code after a {{break}} or {{continue}}.
68+ stateDead
69 )
70
71 // isComment is true for any state that contains content meant for template
72diff --git a/src/html/template/escape.go b/src/html/template/escape.go
73index 8739735..6dea79c 100644
74--- a/src/html/template/escape.go
75+++ b/src/html/template/escape.go
76@@ -97,6 +97,15 @@ type escaper struct {
77 actionNodeEdits map[*parse.ActionNode][]string
78 templateNodeEdits map[*parse.TemplateNode]string
79 textNodeEdits map[*parse.TextNode][]byte
80+ // rangeContext holds context about the current range loop.
81+ rangeContext *rangeContext
82+}
83+
84+// rangeContext holds information about the current range loop.
85+type rangeContext struct {
86+ outer *rangeContext // outer loop
87+ breaks []context // context at each break action
88+ continues []context // context at each continue action
89 }
90
91 // makeEscaper creates a blank escaper for the given set.
92@@ -109,6 +118,7 @@ func makeEscaper(n *nameSpace) escaper {
93 map[*parse.ActionNode][]string{},
94 map[*parse.TemplateNode]string{},
95 map[*parse.TextNode][]byte{},
96+ nil,
97 }
98 }
99
100@@ -124,8 +134,16 @@ func (e *escaper) escape(c context, n parse.Node) context {
101 switch n := n.(type) {
102 case *parse.ActionNode:
103 return e.escapeAction(c, n)
104+ case *parse.BreakNode:
105+ c.n = n
106+ e.rangeContext.breaks = append(e.rangeContext.breaks, c)
107+ return context{state: stateDead}
108 case *parse.CommentNode:
109 return c
110+ case *parse.ContinueNode:
111+ c.n = n
112+ e.rangeContext.continues = append(e.rangeContext.breaks, c)
113+ return context{state: stateDead}
114 case *parse.IfNode:
115 return e.escapeBranch(c, &n.BranchNode, "if")
116 case *parse.ListNode:
117@@ -427,6 +445,12 @@ func join(a, b context, node parse.Node, nodeName string) context {
118 if b.state == stateError {
119 return b
120 }
121+ if a.state == stateDead {
122+ return b
123+ }
124+ if b.state == stateDead {
125+ return a
126+ }
127 if a.eq(b) {
128 return a
129 }
130@@ -466,14 +490,27 @@ func join(a, b context, node parse.Node, nodeName string) context {
131
132 // escapeBranch escapes a branch template node: "if", "range" and "with".
133 func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context {
134+ if nodeName == "range" {
135+ e.rangeContext = &rangeContext{outer: e.rangeContext}
136+ }
137 c0 := e.escapeList(c, n.List)
138- if nodeName == "range" && c0.state != stateError {
139+ if nodeName == "range" {
140+ if c0.state != stateError {
141+ c0 = joinRange(c0, e.rangeContext)
142+ }
143+ e.rangeContext = e.rangeContext.outer
144+ if c0.state == stateError {
145+ return c0
146+ }
147+
148 // The "true" branch of a "range" node can execute multiple times.
149 // We check that executing n.List once results in the same context
150 // as executing n.List twice.
151+ e.rangeContext = &rangeContext{outer: e.rangeContext}
152 c1, _ := e.escapeListConditionally(c0, n.List, nil)
153 c0 = join(c0, c1, n, nodeName)
154 if c0.state == stateError {
155+ e.rangeContext = e.rangeContext.outer
156 // Make clear that this is a problem on loop re-entry
157 // since developers tend to overlook that branch when
158 // debugging templates.
159@@ -481,11 +518,39 @@ func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string)
160 c0.err.Description = "on range loop re-entry: " + c0.err.Description
161 return c0
162 }
163+ c0 = joinRange(c0, e.rangeContext)
164+ e.rangeContext = e.rangeContext.outer
165+ if c0.state == stateError {
166+ return c0
167+ }
168 }
169 c1 := e.escapeList(c, n.ElseList)
170 return join(c0, c1, n, nodeName)
171 }
172
173+func joinRange(c0 context, rc *rangeContext) context {
174+ // Merge contexts at break and continue statements into overall body context.
175+ // In theory we could treat breaks differently from continues, but for now it is
176+ // enough to treat them both as going back to the start of the loop (which may then stop).
177+ for _, c := range rc.breaks {
178+ c0 = join(c0, c, c.n, "range")
179+ if c0.state == stateError {
180+ c0.err.Line = c.n.(*parse.BreakNode).Line
181+ c0.err.Description = "at range loop break: " + c0.err.Description
182+ return c0
183+ }
184+ }
185+ for _, c := range rc.continues {
186+ c0 = join(c0, c, c.n, "range")
187+ if c0.state == stateError {
188+ c0.err.Line = c.n.(*parse.ContinueNode).Line
189+ c0.err.Description = "at range loop continue: " + c0.err.Description
190+ return c0
191+ }
192+ }
193+ return c0
194+}
195+
196 // escapeList escapes a list template node.
197 func (e *escaper) escapeList(c context, n *parse.ListNode) context {
198 if n == nil {
199@@ -493,6 +558,9 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context {
200 }
201 for _, m := range n.Nodes {
202 c = e.escape(c, m)
203+ if c.state == stateDead {
204+ break
205+ }
206 }
207 return c
208 }
209@@ -503,6 +571,7 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context {
210 // which is the same as whether e was updated.
211 func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) {
212 e1 := makeEscaper(e.ns)
213+ e1.rangeContext = e.rangeContext
214 // Make type inferences available to f.
215 for k, v := range e.output {
216 e1.output[k] = v
217diff --git a/src/html/template/escape_test.go b/src/html/template/escape_test.go
218index c709660..fa2b84a 100644
219--- a/src/html/template/escape_test.go
220+++ b/src/html/template/escape_test.go
221@@ -920,6 +920,22 @@ func TestErrors(t *testing.T) {
222 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>",
223 "",
224 },
225+ {
226+ "{{range .Items}}<a{{if .X}}{{end}}>{{end}}",
227+ "",
228+ },
229+ {
230+ "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}",
231+ "",
232+ },
233+ {
234+ "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}",
235+ "",
236+ },
237+ {
238+ "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
239+ "",
240+ },
241 // Error cases.
242 {
243 "{{if .Cond}}<a{{end}}",
244@@ -956,6 +972,14 @@ func TestErrors(t *testing.T) {
245 "z:2:8: on range loop re-entry: {{range}} branches",
246 },
247 {
248+ "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}",
249+ "z:1:29: at range loop break: {{range}} branches end in different contexts",
250+ },
251+ {
252+ "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
253+ "z:1:29: at range loop continue: {{range}} branches end in different contexts",
254+ },
255+ {
256 "<a b=1 c={{.H}}",
257 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
258 },
259diff --git a/src/text/template/doc.go b/src/text/template/doc.go
260index 7b30294..0228b15 100644
261--- a/src/text/template/doc.go
262+++ b/src/text/template/doc.go
263@@ -112,6 +112,14 @@ data, defined in detail in the corresponding sections that follow.
264 T0 is executed; otherwise, dot is set to the successive elements
265 of the array, slice, or map and T1 is executed.
266
267+ {{break}}
268+ The innermost {{range pipeline}} loop is ended early, stopping the
269+ current iteration and bypassing all remaining iterations.
270+
271+ {{continue}}
272+ The current iteration of the innermost {{range pipeline}} loop is
273+ stopped, and the loop starts the next iteration.
274+
275 {{template "name"}}
276 The template with the specified name is executed with nil data.
277
278diff --git a/src/text/template/exec.go b/src/text/template/exec.go
279index 7ac5175..6cb140a 100644
280--- a/src/text/template/exec.go
281+++ b/src/text/template/exec.go
282@@ -5,6 +5,7 @@
283 package template
284
285 import (
286+ "errors"
287 "fmt"
288 "internal/fmtsort"
289 "io"
290@@ -244,6 +245,12 @@ func (t *Template) DefinedTemplates() string {
291 return b.String()
292 }
293
294+// Sentinel errors for use with panic to signal early exits from range loops.
295+var (
296+ walkBreak = errors.New("break")
297+ walkContinue = errors.New("continue")
298+)
299+
300 // Walk functions step through the major pieces of the template structure,
301 // generating output as they go.
302 func (s *state) walk(dot reflect.Value, node parse.Node) {
303@@ -256,7 +263,11 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
304 if len(node.Pipe.Decl) == 0 {
305 s.printValue(node, val)
306 }
307+ case *parse.BreakNode:
308+ panic(walkBreak)
309 case *parse.CommentNode:
310+ case *parse.ContinueNode:
311+ panic(walkContinue)
312 case *parse.IfNode:
313 s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
314 case *parse.ListNode:
315@@ -335,6 +346,11 @@ func isTrue(val reflect.Value) (truth, ok bool) {
316
317 func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
318 s.at(r)
319+ defer func() {
320+ if r := recover(); r != nil && r != walkBreak {
321+ panic(r)
322+ }
323+ }()
324 defer s.pop(s.mark())
325 val, _ := indirect(s.evalPipeline(dot, r.Pipe))
326 // mark top of stack before any variables in the body are pushed.
327@@ -348,8 +364,14 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
328 if len(r.Pipe.Decl) > 1 {
329 s.setTopVar(2, index)
330 }
331+ defer s.pop(mark)
332+ defer func() {
333+ // Consume panic(walkContinue)
334+ if r := recover(); r != nil && r != walkContinue {
335+ panic(r)
336+ }
337+ }()
338 s.walk(elem, r.List)
339- s.pop(mark)
340 }
341 switch val.Kind() {
342 case reflect.Array, reflect.Slice:
343diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go
344index 3309b33..a639f44 100644
345--- a/src/text/template/exec_test.go
346+++ b/src/text/template/exec_test.go
347@@ -563,6 +563,8 @@ var execTests = []execTest{
348 {"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true},
349 {"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true},
350 {"range empty else", "{{range .SIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
351+ {"range []int break else", "{{range .SI}}-{{.}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-3-", tVal, true},
352+ {"range []int continue else", "{{range .SI}}-{{.}}-{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true},
353 {"range []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true},
354 {"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true},
355 {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true},
356diff --git a/src/text/template/parse/lex.go b/src/text/template/parse/lex.go
357index 6784071..95e3377 100644
358--- a/src/text/template/parse/lex.go
359+++ b/src/text/template/parse/lex.go
360@@ -62,6 +62,8 @@ const (
361 // Keywords appear after all the rest.
362 itemKeyword // used only to delimit the keywords
363 itemBlock // block keyword
364+ itemBreak // break keyword
365+ itemContinue // continue keyword
366 itemDot // the cursor, spelled '.'
367 itemDefine // define keyword
368 itemElse // else keyword
369@@ -76,6 +78,8 @@ const (
370 var key = map[string]itemType{
371 ".": itemDot,
372 "block": itemBlock,
373+ "break": itemBreak,
374+ "continue": itemContinue,
375 "define": itemDefine,
376 "else": itemElse,
377 "end": itemEnd,
378@@ -119,6 +123,8 @@ type lexer struct {
379 parenDepth int // nesting depth of ( ) exprs
380 line int // 1+number of newlines seen
381 startLine int // start line of this item
382+ breakOK bool // break keyword allowed
383+ continueOK bool // continue keyword allowed
384 }
385
386 // next returns the next rune in the input.
387@@ -461,7 +467,12 @@ Loop:
388 }
389 switch {
390 case key[word] > itemKeyword:
391- l.emit(key[word])
392+ item := key[word]
393+ if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK {
394+ l.emit(itemIdentifier)
395+ } else {
396+ l.emit(item)
397+ }
398 case word[0] == '.':
399 l.emit(itemField)
400 case word == "true", word == "false":
401diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go
402index 6510eed..df6aabf 100644
403--- a/src/text/template/parse/lex_test.go
404+++ b/src/text/template/parse/lex_test.go
405@@ -35,6 +35,8 @@ var itemName = map[itemType]string{
406 // keywords
407 itemDot: ".",
408 itemBlock: "block",
409+ itemBreak: "break",
410+ itemContinue: "continue",
411 itemDefine: "define",
412 itemElse: "else",
413 itemIf: "if",
414diff --git a/src/text/template/parse/node.go b/src/text/template/parse/node.go
415index a9dad5e..c398da0 100644
416--- a/src/text/template/parse/node.go
417+++ b/src/text/template/parse/node.go
418@@ -71,6 +71,8 @@ const (
419 NodeVariable // A $ variable.
420 NodeWith // A with action.
421 NodeComment // A comment.
422+ NodeBreak // A break action.
423+ NodeContinue // A continue action.
424 )
425
426 // Nodes.
427@@ -907,6 +909,40 @@ func (i *IfNode) Copy() Node {
428 return i.tr.newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList())
429 }
430
431+// BreakNode represents a {{break}} action.
432+type BreakNode struct {
433+ tr *Tree
434+ NodeType
435+ Pos
436+ Line int
437+}
438+
439+func (t *Tree) newBreak(pos Pos, line int) *BreakNode {
440+ return &BreakNode{tr: t, NodeType: NodeBreak, Pos: pos, Line: line}
441+}
442+
443+func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) }
444+func (b *BreakNode) String() string { return "{{break}}" }
445+func (b *BreakNode) tree() *Tree { return b.tr }
446+func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") }
447+
448+// ContinueNode represents a {{continue}} action.
449+type ContinueNode struct {
450+ tr *Tree
451+ NodeType
452+ Pos
453+ Line int
454+}
455+
456+func (t *Tree) newContinue(pos Pos, line int) *ContinueNode {
457+ return &ContinueNode{tr: t, NodeType: NodeContinue, Pos: pos, Line: line}
458+}
459+
460+func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) }
461+func (c *ContinueNode) String() string { return "{{continue}}" }
462+func (c *ContinueNode) tree() *Tree { return c.tr }
463+func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") }
464+
465 // RangeNode represents a {{range}} action and its commands.
466 type RangeNode struct {
467 BranchNode
468diff --git a/src/text/template/parse/parse.go b/src/text/template/parse/parse.go
469index 5e6e512..7f78b56 100644
470--- a/src/text/template/parse/parse.go
471+++ b/src/text/template/parse/parse.go
472@@ -31,6 +31,7 @@ type Tree struct {
473 vars []string // variables defined at the moment.
474 treeSet map[string]*Tree
475 actionLine int // line of left delim starting action
476+ rangeDepth int
477 mode Mode
478 }
479
480@@ -223,6 +224,8 @@ func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet ma
481 t.vars = []string{"$"}
482 t.funcs = funcs
483 t.treeSet = treeSet
484+ lex.breakOK = !t.hasFunction("break")
485+ lex.continueOK = !t.hasFunction("continue")
486 }
487
488 // stopParse terminates parsing.
489@@ -385,6 +388,10 @@ func (t *Tree) action() (n Node) {
490 switch token := t.nextNonSpace(); token.typ {
491 case itemBlock:
492 return t.blockControl()
493+ case itemBreak:
494+ return t.breakControl(token.pos, token.line)
495+ case itemContinue:
496+ return t.continueControl(token.pos, token.line)
497 case itemElse:
498 return t.elseControl()
499 case itemEnd:
500@@ -404,6 +411,32 @@ func (t *Tree) action() (n Node) {
501 return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim))
502 }
503
504+// Break:
505+// {{break}}
506+// Break keyword is past.
507+func (t *Tree) breakControl(pos Pos, line int) Node {
508+ if token := t.next(); token.typ != itemRightDelim {
509+ t.unexpected(token, "in {{break}}")
510+ }
511+ if t.rangeDepth == 0 {
512+ t.errorf("{{break}} outside {{range}}")
513+ }
514+ return t.newBreak(pos, line)
515+}
516+
517+// Continue:
518+// {{continue}}
519+// Continue keyword is past.
520+func (t *Tree) continueControl(pos Pos, line int) Node {
521+ if token := t.next(); token.typ != itemRightDelim {
522+ t.unexpected(token, "in {{continue}}")
523+ }
524+ if t.rangeDepth == 0 {
525+ t.errorf("{{continue}} outside {{range}}")
526+ }
527+ return t.newContinue(pos, line)
528+}
529+
530 // Pipeline:
531 // declarations? command ('|' command)*
532 func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
533@@ -479,8 +512,14 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
534 func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
535 defer t.popVars(len(t.vars))
536 pipe = t.pipeline(context, itemRightDelim)
537+ if context == "range" {
538+ t.rangeDepth++
539+ }
540 var next Node
541 list, next = t.itemList()
542+ if context == "range" {
543+ t.rangeDepth--
544+ }
545 switch next.Type() {
546 case nodeEnd: //done
547 case nodeElse:
548@@ -522,7 +561,8 @@ func (t *Tree) ifControl() Node {
549 // {{range pipeline}} itemList {{else}} itemList {{end}}
550 // Range keyword is past.
551 func (t *Tree) rangeControl() Node {
552- return t.newRange(t.parseControl(false, "range"))
553+ r := t.newRange(t.parseControl(false, "range"))
554+ return r
555 }
556
557 // With:
558diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go
559index 220f984..ba45636 100644
560--- a/src/text/template/parse/parse_test.go
561+++ b/src/text/template/parse/parse_test.go
562@@ -230,6 +230,10 @@ var parseTests = []parseTest{
563 `{{range $x := .SI}}{{.}}{{end}}`},
564 {"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
565 `{{range $x, $y := .SI}}{{.}}{{end}}`},
566+ {"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
567+ `{{range .SI}}{{.}}{{break}}{{end}}`},
568+ {"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
569+ `{{range .SI}}{{.}}{{continue}}{{end}}`},
570 {"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
571 `{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
572 {"template", "{{template `x`}}", noError,
573@@ -279,6 +283,10 @@ var parseTests = []parseTest{
574 {"adjacent args", "{{printf 3`x`}}", hasError, ""},
575 {"adjacent args with .", "{{printf `x`.}}", hasError, ""},
576 {"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
577+ {"break outside range", "{{range .}}{{end}} {{break}}", hasError, ""},
578+ {"continue outside range", "{{range .}}{{end}} {{continue}}", hasError, ""},
579+ {"break in range else", "{{range .}}{{else}}{{break}}{{end}}", hasError, ""},
580+ {"continue in range else", "{{range .}}{{else}}{{continue}}{{end}}", hasError, ""},
581 // Other kinds of assignments and operators aren't available yet.
582 {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
583 {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
584--
5852.7.4