exp/template: add a JavaScript escaper.
R=r
CC=golang-dev
https://golang.org/cl/4671048
diff --git a/src/pkg/exp/template/exec_test.go b/src/pkg/exp/template/exec_test.go
index 5be82dd..74c92e5 100644
--- a/src/pkg/exp/template/exec_test.go
+++ b/src/pkg/exp/template/exec_test.go
@@ -158,6 +158,8 @@
"<script>alert("XSS");</script>", nil, true},
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"<script>alert("XSS");</script>", 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 @@
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)
+ }
+ }
+}
diff --git a/src/pkg/exp/template/funcs.go b/src/pkg/exp/template/funcs.go
index 44770c7..c42f3b2 100644
--- a/src/pkg/exp/template/funcs.go
+++ b/src/pkg/exp/template/funcs.go
@@ -6,10 +6,12 @@
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 @@
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 @@
// HTML escaping.
var (
- escQuot = []byte(""") // shorter than """
- escApos = []byte("'") // shorter than "'"
- escAmp = []byte("&")
- escLt = []byte("<")
- escGt = []byte(">")
+ htmlQuot = []byte(""") // shorter than """
+ htmlApos = []byte("'") // shorter than "'"
+ htmlAmp = []byte("&")
+ htmlLt = []byte("<")
+ htmlGt = []byte(">")
)
// 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 @@
}
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)
+}