Skip to content

Commit

Permalink
html/template: track brace depth for each nested expression
Browse files Browse the repository at this point in the history
We need to track the brace depth for each individual nested expression,
since a string interpolation expression may be nested inside of an
object.

e.g. `${ {1:`${}`}}` has brace depths [1, 0] when inside of the inner
${} expression. When we exit the inner expression, we need to reset to
the previous brace depth (1) so that we know that the following } closes
the object, but not the outer expression.

Note that if you write a broken expression (i.e. `${ { }`) escaping will
clearly not work as expected (or depending on your interpretation, since
it is broken, it will work as expected). Since the JS parser doesn't
catch syntax errors, it's up to the user to write a valid template.

Updates golang#61619

Change-Id: I4c33723d12aff49facdcb1134d9ca82b7a0dffc4
Reviewed-on: https://go-review.googlesource.com/c/go/+/532995
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
rolandshoemaker authored and yunginnanet committed Oct 20, 2023
1 parent 6647cd2 commit f567316
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 30 deletions.
23 changes: 13 additions & 10 deletions src/html/template/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ import (
// https://www.w3.org/TR/html5/syntax.html#the-end
// where the context element is null.
type context struct {
state state
delim delim
urlPart urlPart
jsCtx jsCtx
jsTmplExprDepth int
jsBraceDepth int
attr attr
element element
n parse.Node // for range break/continue
err *Error
state state
delim delim
urlPart urlPart
jsCtx jsCtx
// jsBraceDepth contains the current depth, for each JS template literal
// string interpolation expression, of braces we've seen. This is used to
// determine if the next } will close a JS template literal string
// interpolation expression or not.
jsBraceDepth []int
attr attr
element element
n parse.Node // for range break/continue
err *Error
}

func (c context) String() string {
Expand Down
14 changes: 13 additions & 1 deletion src/html/template/escape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1797,13 +1797,25 @@ func TestEscapeText(t *testing.T) {
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>`${ { `` }`",
"<script>`${ { `` }",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ { }`",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${",
context{state: stateJS, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ `}",
context{state: stateJSTmplLit, element: elementScript},
},
}

for _, test := range tests {
Expand Down
30 changes: 11 additions & 19 deletions src/html/template/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,29 +323,23 @@ func tJS(c context, s []byte) (context, int) {
case '{':
// We only care about tracking brace depth if we are inside of a
// template literal.
if c.jsTmplExprDepth == 0 {
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
c.jsBraceDepth++
c.jsBraceDepth[len(c.jsBraceDepth)-1]++
case '}':
if c.jsTmplExprDepth == 0 {
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
for j := 0; j <= i; j++ {
switch s[j] {
case '\\':
j++
case '{':
c.jsBraceDepth++
case '}':
c.jsBraceDepth--
}
}
if c.jsBraceDepth >= 0 {
// There are no cases where a brace can be escaped in the JS context
// that are not syntax errors, it seems. Because of this we can just
// count "\}" as "}" and move on, the script is already broken as
// fully fledged parsers will just fail anyway.
c.jsBraceDepth[len(c.jsBraceDepth)-1]--
if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
return c, i + 1
}
c.jsTmplExprDepth--
c.jsBraceDepth = 0
c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
c.state = stateJSTmplLit
default:
panic("unreachable")
Expand All @@ -354,7 +348,6 @@ func tJS(c context, s []byte) (context, int) {
}

func tJSTmpl(c context, s []byte) (context, int) {
c.jsBraceDepth = 0
var k int
for {
i := k + bytes.IndexAny(s[k:], "`\\$")
Expand All @@ -372,8 +365,7 @@ func tJSTmpl(c context, s []byte) (context, int) {
}
case '$':
if len(s) >= i+2 && s[i+1] == '{' {
c.jsTmplExprDepth++
c.jsBraceDepth = 0
c.jsBraceDepth = append(c.jsBraceDepth, 0)
c.state = stateJS
return c, i + 2
}
Expand Down

0 comments on commit f567316

Please sign in to comment.