]> Cypherpunks repositories - gostls13.git/commitdiff
exp/template: add a JavaScript escaper.
authorDavid Symonds <dsymonds@golang.org>
Wed, 6 Jul 2011 06:51:49 +0000 (16:51 +1000)
committerDavid Symonds <dsymonds@golang.org>
Wed, 6 Jul 2011 06:51:49 +0000 (16:51 +1000)
R=r
CC=golang-dev
https://golang.org/cl/4671048

src/pkg/exp/template/exec_test.go
src/pkg/exp/template/funcs.go

index 5be82dd6eff0be807a02615920fb0bad6e1e229d..74c92e5f6914ef756f6ac2b73ed6f75a4325881d 100644 (file)
@@ -158,6 +158,8 @@ var execTests = []execTest{
                "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
        {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
                "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
+       // JS.
+       {"js", `{{js .}}`, `It\'d be nice.`, `It'd be nice.`, true},
        // Booleans
        {"not", "{{not true}} {{not false}}", "false true", nil, true},
        {"and", "{{and 0 0}} {{and 1 0}} {{and 0 1}} {{and 1 1}}", "false false false true", nil, true},
@@ -248,3 +250,21 @@ func TestExecuteError(t *testing.T) {
                t.Errorf("expected os.EPERM; got %s", err)
        }
 }
+
+func TestJSEscaping(t *testing.T) {
+       testCases := []struct {
+               in, exp string
+       }{
+               {`a`, `a`},
+               {`'foo`, `\'foo`},
+               {`Go "jump" \`, `Go \"jump\" \\`},
+               {`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
+               {"unprintable \uFDFF", `unprintable \uFDFF`},
+       }
+       for _, tc := range testCases {
+               s := JSEscapeString(tc.in)
+               if s != tc.exp {
+                       t.Errorf("JS escaping [%s] got [%s] want [%s]", tc.in, s, tc.exp)
+               }
+       }
+}
index 44770c7044e007c687738b9db37ed1ac14798660..c42f3b2509bec249870cdd13a599a303338f8519 100644 (file)
@@ -6,10 +6,12 @@ package template
 
 import (
        "bytes"
-       "io"
        "fmt"
+       "io"
        "reflect"
        "strings"
+       "unicode"
+       "utf8"
 )
 
 // FuncMap is the type of the map defining the mapping from names to functions.
@@ -20,6 +22,7 @@ type FuncMap map[string]interface{}
 var funcs = map[string]reflect.Value{
        "printf": reflect.ValueOf(fmt.Sprintf),
        "html":   reflect.ValueOf(HTMLEscaper),
+       "js":     reflect.ValueOf(JSEscaper),
        "and":    reflect.ValueOf(and),
        "or":     reflect.ValueOf(or),
        "not":    reflect.ValueOf(not),
@@ -98,34 +101,34 @@ func not(arg interface{}) (truth bool) {
 // HTML escaping.
 
 var (
-       escQuot = []byte("&#34;") // shorter than "&quot;"
-       escApos = []byte("&#39;") // shorter than "&apos;"
-       escAmp  = []byte("&amp;")
-       escLt   = []byte("&lt;")
-       escGt   = []byte("&gt;")
+       htmlQuot = []byte("&#34;") // shorter than "&quot;"
+       htmlApos = []byte("&#39;") // shorter than "&apos;"
+       htmlAmp  = []byte("&amp;")
+       htmlLt   = []byte("&lt;")
+       htmlGt   = []byte("&gt;")
 )
 
 // HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
 func HTMLEscape(w io.Writer, b []byte) {
        last := 0
        for i, c := range b {
-               var esc []byte
+               var html []byte
                switch c {
                case '"':
-                       esc = escQuot
+                       html = htmlQuot
                case '\'':
-                       esc = escApos
+                       html = htmlApos
                case '&':
-                       esc = escAmp
+                       html = htmlAmp
                case '<':
-                       esc = escLt
+                       html = htmlLt
                case '>':
-                       esc = escGt
+                       html = htmlGt
                default:
                        continue
                }
                w.Write(b[last:i])
-               w.Write(esc)
+               w.Write(html)
                last = i + 1
        }
        w.Write(b[last:])
@@ -155,3 +158,92 @@ func HTMLEscaper(args ...interface{}) string {
        }
        return HTMLEscapeString(s)
 }
+
+// JavaScript escaping.
+
+var (
+       jsLowUni = []byte(`\u00`)
+       hex      = []byte("0123456789ABCDEF")
+
+       jsBackslash = []byte(`\\`)
+       jsApos      = []byte(`\'`)
+       jsQuot      = []byte(`\"`)
+)
+
+
+// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
+func JSEscape(w io.Writer, b []byte) {
+       last := 0
+       for i := 0; i < len(b); i++ {
+               c := b[i]
+
+               if ' ' <= c && c < utf8.RuneSelf && c != '\\' && c != '"' && c != '\'' {
+                       // fast path: nothing to do
+                       continue
+               }
+               w.Write(b[last:i])
+
+               if c < utf8.RuneSelf {
+                       // Quotes and slashes get quoted.
+                       // Control characters get written as \u00XX.
+                       switch c {
+                       case '\\':
+                               w.Write(jsBackslash)
+                       case '\'':
+                               w.Write(jsApos)
+                       case '"':
+                               w.Write(jsQuot)
+                       default:
+                               w.Write(jsLowUni)
+                               t, b := c>>4, c&0x0f
+                               w.Write(hex[t : t+1])
+                               w.Write(hex[b : b+1])
+                       }
+               } else {
+                       // Unicode rune.
+                       rune, size := utf8.DecodeRune(b[i:])
+                       if unicode.IsPrint(rune) {
+                               w.Write(b[i : i+size])
+                       } else {
+                               // TODO(dsymonds): Do this without fmt?
+                               fmt.Fprintf(w, "\\u%04X", rune)
+                       }
+                       i += size - 1
+               }
+               last = i + 1
+       }
+       w.Write(b[last:])
+}
+
+// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s.
+func JSEscapeString(s string) string {
+       // Avoid allocation if we can.
+       if strings.IndexFunc(s, jsIsSpecial) < 0 {
+               return s
+       }
+       var b bytes.Buffer
+       JSEscape(&b, []byte(s))
+       return b.String()
+}
+
+func jsIsSpecial(rune int) bool {
+       switch rune {
+       case '\\', '\'', '"':
+               return true
+       }
+       return rune < ' ' || utf8.RuneSelf <= rune
+}
+
+// JSEscaper returns the escaped JavaScript equivalent of the textual
+// representation of its arguments.
+func JSEscaper(args ...interface{}) string {
+       ok := false
+       var s string
+       if len(args) == 1 {
+               s, ok = args[0].(string)
+       }
+       if !ok {
+               s = fmt.Sprint(args...)
+       }
+       return JSEscapeString(s)
+}