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 }