js_test.go (12113B)
1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 //go:build go1.13 && !windows
6 // +build go1.13,!windows
7
8 package template
9
10 import (
11 "bytes"
12 "math"
13 "strings"
14 "testing"
15 )
16
17 func TestNextJsCtx(t *testing.T) {
18 tests := []struct {
19 jsCtx jsCtx
20 s string
21 }{
22 // Statement terminators precede regexps.
23 {jsCtxRegexp, ";"},
24 // This is not airtight.
25 // ({ valueOf: function () { return 1 } } / 2)
26 // is valid JavaScript but in practice, devs do not do this.
27 // A block followed by a statement starting with a RegExp is
28 // much more common:
29 // while (x) {...} /foo/.test(x) || panic()
30 {jsCtxRegexp, "}"},
31 // But member, call, grouping, and array expression terminators
32 // precede div ops.
33 {jsCtxDivOp, ")"},
34 {jsCtxDivOp, "]"},
35 // At the start of a primary expression, array, or expression
36 // statement, expect a regexp.
37 {jsCtxRegexp, "("},
38 {jsCtxRegexp, "["},
39 {jsCtxRegexp, "{"},
40 // Assignment operators precede regexps as do all exclusively
41 // prefix and binary operators.
42 {jsCtxRegexp, "="},
43 {jsCtxRegexp, "+="},
44 {jsCtxRegexp, "*="},
45 {jsCtxRegexp, "*"},
46 {jsCtxRegexp, "!"},
47 // Whether the + or - is infix or prefix, it cannot precede a
48 // div op.
49 {jsCtxRegexp, "+"},
50 {jsCtxRegexp, "-"},
51 // An incr/decr op precedes a div operator.
52 // This is not airtight. In (g = ++/h/i) a regexp follows a
53 // pre-increment operator, but in practice devs do not try to
54 // increment or decrement regular expressions.
55 // (g++/h/i) where ++ is a postfix operator on g is much more
56 // common.
57 {jsCtxDivOp, "--"},
58 {jsCtxDivOp, "++"},
59 {jsCtxDivOp, "x--"},
60 // When we have many dashes or pluses, then they are grouped
61 // left to right.
62 {jsCtxRegexp, "x---"}, // A postfix -- then a -.
63 // return followed by a slash returns the regexp literal or the
64 // slash starts a regexp literal in an expression statement that
65 // is dead code.
66 {jsCtxRegexp, "return"},
67 {jsCtxRegexp, "return "},
68 {jsCtxRegexp, "return\t"},
69 {jsCtxRegexp, "return\n"},
70 {jsCtxRegexp, "return\u2028"},
71 // Identifiers can be divided and cannot validly be preceded by
72 // a regular expressions. Semicolon insertion cannot happen
73 // between an identifier and a regular expression on a new line
74 // because the one token lookahead for semicolon insertion has
75 // to conclude that it could be a div binary op and treat it as
76 // such.
77 {jsCtxDivOp, "x"},
78 {jsCtxDivOp, "x "},
79 {jsCtxDivOp, "x\t"},
80 {jsCtxDivOp, "x\n"},
81 {jsCtxDivOp, "x\u2028"},
82 {jsCtxDivOp, "preturn"},
83 // Numbers precede div ops.
84 {jsCtxDivOp, "0"},
85 // Dots that are part of a number are div preceders.
86 {jsCtxDivOp, "0."},
87 }
88
89 for _, test := range tests {
90 if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx {
91 t.Errorf("want %s got %q", test.jsCtx, test.s)
92 }
93 if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx {
94 t.Errorf("want %s got %q", test.jsCtx, test.s)
95 }
96 }
97
98 if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp {
99 t.Error("Blank tokens")
100 }
101
102 if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp {
103 t.Error("Blank tokens")
104 }
105 }
106
107 func TestJSValEscaper(t *testing.T) {
108 tests := []struct {
109 x any
110 js string
111 }{
112 {int(42), " 42 "},
113 {uint(42), " 42 "},
114 {int16(42), " 42 "},
115 {uint16(42), " 42 "},
116 {int32(-42), " -42 "},
117 {uint32(42), " 42 "},
118 {int16(-42), " -42 "},
119 {uint16(42), " 42 "},
120 {int64(-42), " -42 "},
121 {uint64(42), " 42 "},
122 {uint64(1) << 53, " 9007199254740992 "},
123 // ulp(1 << 53) > 1 so this loses precision in JS
124 // but it is still a representable integer literal.
125 {uint64(1)<<53 + 1, " 9007199254740993 "},
126 {float32(1.0), " 1 "},
127 {float32(-1.0), " -1 "},
128 {float32(0.5), " 0.5 "},
129 {float32(-0.5), " -0.5 "},
130 {float32(1.0) / float32(256), " 0.00390625 "},
131 {float32(0), " 0 "},
132 {math.Copysign(0, -1), " -0 "},
133 {float64(1.0), " 1 "},
134 {float64(-1.0), " -1 "},
135 {float64(0.5), " 0.5 "},
136 {float64(-0.5), " -0.5 "},
137 {float64(0), " 0 "},
138 {math.Copysign(0, -1), " -0 "},
139 {"", `""`},
140 {"foo", `"foo"`},
141 // Newlines.
142 {"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
143 // "\v" == "v" on IE 6 so use "\u000b" instead.
144 {"\t\x0b", `"\t\u000b"`},
145 {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
146 {[]any{}, "[]"},
147 {[]any{42, "foo", nil}, `[42,"foo",null]`},
148 {[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
149 {"<!--", `"\u003c!--"`},
150 {"-->", `"--\u003e"`},
151 {"<![CDATA[", `"\u003c![CDATA["`},
152 {"]]>", `"]]\u003e"`},
153 {"</script", `"\u003c/script"`},
154 {"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E"
155 {nil, " null "},
156 }
157
158 for _, test := range tests {
159 if js := jsValEscaper(test.x); js != test.js {
160 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js)
161 }
162 // Make sure that escaping corner cases are not broken
163 // by nesting.
164 a := []any{test.x}
165 want := "[" + strings.TrimSpace(test.js) + "]"
166 if js := jsValEscaper(a); js != want {
167 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js)
168 }
169 }
170 }
171
172 func TestJSStrEscaper(t *testing.T) {
173 tests := []struct {
174 x any
175 esc string
176 }{
177 {"", ``},
178 {"foo", `foo`},
179 {"\u0000", `\u0000`},
180 {"\t", `\t`},
181 {"\n", `\n`},
182 {"\r", `\r`},
183 {"\u2028", `\u2028`},
184 {"\u2029", `\u2029`},
185 {"\\", `\\`},
186 {"\\n", `\\n`},
187 {"foo\r\nbar", `foo\r\nbar`},
188 // Preserve attribute boundaries.
189 {`"`, `\u0022`},
190 {`'`, `\u0027`},
191 // Allow embedding in HTML without further escaping.
192 {`&`, `\u0026amp;`},
193 // Prevent breaking out of text node and element boundaries.
194 {"</script>", `\u003c\/script\u003e`},
195 {"<![CDATA[", `\u003c![CDATA[`},
196 {"]]>", `]]\u003e`},
197 // https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
198 // "The text in style, script, title, and textarea elements
199 // must not have an escaping text span start that is not
200 // followed by an escaping text span end."
201 // Furthermore, spoofing an escaping text span end could lead
202 // to different interpretation of a </script> sequence otherwise
203 // masked by the escaping text span, and spoofing a start could
204 // allow regular text content to be interpreted as script
205 // allowing script execution via a combination of a JS string
206 // injection followed by an HTML text injection.
207 {"<!--", `\u003c!--`},
208 {"-->", `--\u003e`},
209 // From https://code.google.com/p/doctype/wiki/ArticleUtf7
210 {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
211 `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
212 },
213 // Invalid UTF-8 sequence
214 {"foo\xA0bar", "foo\xA0bar"},
215 // Invalid unicode scalar value.
216 {"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"},
217 }
218
219 for _, test := range tests {
220 esc := jsStrEscaper(test.x)
221 if esc != test.esc {
222 t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
223 }
224 }
225 }
226
227 func TestJSRegexpEscaper(t *testing.T) {
228 tests := []struct {
229 x any
230 esc string
231 }{
232 {"", `(?:)`},
233 {"foo", `foo`},
234 {"\u0000", `\u0000`},
235 {"\t", `\t`},
236 {"\n", `\n`},
237 {"\r", `\r`},
238 {"\u2028", `\u2028`},
239 {"\u2029", `\u2029`},
240 {"\\", `\\`},
241 {"\\n", `\\n`},
242 {"foo\r\nbar", `foo\r\nbar`},
243 // Preserve attribute boundaries.
244 {`"`, `\u0022`},
245 {`'`, `\u0027`},
246 // Allow embedding in HTML without further escaping.
247 {`&`, `\u0026amp;`},
248 // Prevent breaking out of text node and element boundaries.
249 {"</script>", `\u003c\/script\u003e`},
250 {"<![CDATA[", `\u003c!\[CDATA\[`},
251 {"]]>", `\]\]\u003e`},
252 // Escaping text spans.
253 {"<!--", `\u003c!\-\-`},
254 {"-->", `\-\-\u003e`},
255 {"*", `\*`},
256 {"+", `\u002b`},
257 {"?", `\?`},
258 {"[](){}", `\[\]\(\)\{\}`},
259 {"$foo|x.y", `\$foo\|x\.y`},
260 {"x^y", `x\^y`},
261 }
262
263 for _, test := range tests {
264 esc := jsRegexpEscaper(test.x)
265 if esc != test.esc {
266 t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
267 }
268 }
269 }
270
271 func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
272 input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
273 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
274 ` !"#$%&'()*+,-./` +
275 `0123456789:;<=>?` +
276 `@ABCDEFGHIJKLMNO` +
277 `PQRSTUVWXYZ[\]^_` +
278 "`abcdefghijklmno" +
279 "pqrstuvwxyz{|}~\x7f" +
280 "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
281
282 tests := []struct {
283 name string
284 escaper func(...any) string
285 escaped string
286 }{
287 {
288 "jsStrEscaper",
289 jsStrEscaper,
290 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
291 `\u0008\t\n\u000b\f\r\u000e\u000f` +
292 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
293 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
294 ` !\u0022#$%\u0026\u0027()*\u002b,-.\/` +
295 `0123456789:;\u003c=\u003e?` +
296 `@ABCDEFGHIJKLMNO` +
297 `PQRSTUVWXYZ[\\]^_` +
298 "`abcdefghijklmno" +
299 "pqrstuvwxyz{|}~\u007f" +
300 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
301 },
302 {
303 "jsRegexpEscaper",
304 jsRegexpEscaper,
305 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
306 `\u0008\t\n\u000b\f\r\u000e\u000f` +
307 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
308 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
309 ` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` +
310 `0123456789:;\u003c=\u003e\?` +
311 `@ABCDEFGHIJKLMNO` +
312 `PQRSTUVWXYZ\[\\\]\^_` +
313 "`abcdefghijklmno" +
314 `pqrstuvwxyz\{\|\}~` + "\u007f" +
315 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
316 },
317 }
318
319 for _, test := range tests {
320 if s := test.escaper(input); s != test.escaped {
321 t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
322 continue
323 }
324
325 // Escape it rune by rune to make sure that any
326 // fast-path checking does not break escaping.
327 var buf bytes.Buffer
328 for _, c := range input {
329 buf.WriteString(test.escaper(string(c)))
330 }
331
332 if s := buf.String(); s != test.escaped {
333 t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
334 continue
335 }
336 }
337 }
338
339 func TestIsJsMimeType(t *testing.T) {
340 tests := []struct {
341 in string
342 out bool
343 }{
344 {"application/javascript;version=1.8", true},
345 {"application/javascript;version=1.8;foo=bar", true},
346 {"application/javascript/version=1.8", false},
347 {"text/javascript", true},
348 {"application/json", true},
349 {"application/ld+json", true},
350 {"module", true},
351 }
352
353 for _, test := range tests {
354 if isJSType(test.in) != test.out {
355 t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out)
356 }
357 }
358 }
359
360 func BenchmarkJSValEscaperWithNum(b *testing.B) {
361 for i := 0; i < b.N; i++ {
362 jsValEscaper(3.141592654)
363 }
364 }
365
366 func BenchmarkJSValEscaperWithStr(b *testing.B) {
367 for i := 0; i < b.N; i++ {
368 jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
369 }
370 }
371
372 func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) {
373 for i := 0; i < b.N; i++ {
374 jsValEscaper("The quick, brown fox jumps over the lazy dog")
375 }
376 }
377
378 func BenchmarkJSValEscaperWithObj(b *testing.B) {
379 o := struct {
380 S string
381 N int
382 }{
383 "The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028",
384 42,
385 }
386 for i := 0; i < b.N; i++ {
387 jsValEscaper(o)
388 }
389 }
390
391 func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) {
392 o := struct {
393 S string
394 N int
395 }{
396 "The quick, brown fox jumps over the lazy dog",
397 42,
398 }
399 for i := 0; i < b.N; i++ {
400 jsValEscaper(o)
401 }
402 }
403
404 func BenchmarkJSStrEscaperNoSpecials(b *testing.B) {
405 for i := 0; i < b.N; i++ {
406 jsStrEscaper("The quick, brown fox jumps over the lazy dog.")
407 }
408 }
409
410 func BenchmarkJSStrEscaper(b *testing.B) {
411 for i := 0; i < b.N; i++ {
412 jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
413 }
414 }
415
416 func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) {
417 for i := 0; i < b.N; i++ {
418 jsRegexpEscaper("The quick, brown fox jumps over the lazy dog")
419 }
420 }
421
422 func BenchmarkJSRegexpEscaper(b *testing.B) {
423 for i := 0; i < b.N; i++ {
424 jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
425 }
426 }