]> Cypherpunks repositories - gostls13.git/commitdiff
html/template: track brace depth for each nested expression
authorRoland Shoemaker <roland@golang.org>
Thu, 5 Oct 2023 13:16:18 +0000 (06:16 -0700)
committerRoland Shoemaker <roland@golang.org>
Mon, 16 Oct 2023 03:29:27 +0000 (03:29 +0000)
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 #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>

src/html/template/context.go
src/html/template/escape_test.go
src/html/template/transition.go

index 63d5c31b018be3f5e9ee97334246219eff649088..b78f0f7325ed66545278250aabd421bd82e78920 100644 (file)
@@ -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 {
index 91fbfb9a3cdb6ca3b69affbe19a0e790b40ad118..497ead8716ffedd2c98546f606805149c1e9f8ba 100644 (file)
@@ -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 {
index 4aa2920986a87bfea1bb7042ffbc44feb5f5006f..d5a05f66da406da9f4eb3ebfc36499115c7e42a1 100644 (file)
@@ -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")
@@ -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:], "`\\$")
@@ -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
                        }