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