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