github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/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 // Some JS interpreters treat NBSP as a normal space, so 84 // we must too in order to properly escape things. 85 {jsCtxRegexp, "=\u00A0"}, 86 } 87 88 for _, test := range tests { 89 if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx { 90 t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) 91 } 92 if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx { 93 t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) 94 } 95 } 96 97 if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp { 98 t.Error("Blank tokens") 99 } 100 101 if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp { 102 t.Error("Blank tokens") 103 } 104 } 105 106 func TestJSValEscaper(t *testing.T) { 107 tests := []struct { 108 x any 109 js string 110 }{ 111 {int(42), " 42 "}, 112 {uint(42), " 42 "}, 113 {int16(42), " 42 "}, 114 {uint16(42), " 42 "}, 115 {int32(-42), " -42 "}, 116 {uint32(42), " 42 "}, 117 {int16(-42), " -42 "}, 118 {uint16(42), " 42 "}, 119 {int64(-42), " -42 "}, 120 {uint64(42), " 42 "}, 121 {uint64(1) << 53, " 9007199254740992 "}, 122 // ulp(1 << 53) > 1 so this loses precision in JS 123 // but it is still a representable integer literal. 124 {uint64(1)<<53 + 1, " 9007199254740993 "}, 125 {float32(1.0), " 1 "}, 126 {float32(-1.0), " -1 "}, 127 {float32(0.5), " 0.5 "}, 128 {float32(-0.5), " -0.5 "}, 129 {float32(1.0) / float32(256), " 0.00390625 "}, 130 {float32(0), " 0 "}, 131 {math.Copysign(0, -1), " -0 "}, 132 {float64(1.0), " 1 "}, 133 {float64(-1.0), " -1 "}, 134 {float64(0.5), " 0.5 "}, 135 {float64(-0.5), " -0.5 "}, 136 {float64(0), " 0 "}, 137 {math.Copysign(0, -1), " -0 "}, 138 {"", `""`}, 139 {"foo", `"foo"`}, 140 // Newlines. 141 {"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`}, 142 // "\v" == "v" on IE 6 so use "\u000b" instead. 143 {"\t\x0b", `"\t\u000b"`}, 144 {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`}, 145 {[]any{}, "[]"}, 146 {[]any{42, "foo", nil}, `[42,"foo",null]`}, 147 {[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`}, 148 {"<!--", `"\u003c!--"`}, 149 {"-->", `"--\u003e"`}, 150 {"<![CDATA[", `"\u003c![CDATA["`}, 151 {"]]>", `"]]\u003e"`}, 152 {"</script", `"\u003c/script"`}, 153 {"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E" 154 {nil, " null "}, 155 } 156 157 for _, test := range tests { 158 if js := jsValEscaper(test.x); js != test.js { 159 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js) 160 } 161 // Make sure that escaping corner cases are not broken 162 // by nesting. 163 a := []any{test.x} 164 want := "[" + strings.TrimSpace(test.js) + "]" 165 if js := jsValEscaper(a); js != want { 166 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js) 167 } 168 } 169 } 170 171 func TestJSStrEscaper(t *testing.T) { 172 tests := []struct { 173 x any 174 esc string 175 }{ 176 {"", ``}, 177 {"foo", `foo`}, 178 {"\u0000", `\u0000`}, 179 {"\t", `\t`}, 180 {"\n", `\n`}, 181 {"\r", `\r`}, 182 {"\u2028", `\u2028`}, 183 {"\u2029", `\u2029`}, 184 {"\\", `\\`}, 185 {"\\n", `\\n`}, 186 {"foo\r\nbar", `foo\r\nbar`}, 187 // Preserve attribute boundaries. 188 {`"`, `\u0022`}, 189 {`'`, `\u0027`}, 190 // Allow embedding in HTML without further escaping. 191 {`&`, `\u0026amp;`}, 192 // Prevent breaking out of text node and element boundaries. 193 {"</script>", `\u003c\/script\u003e`}, 194 {"<![CDATA[", `\u003c![CDATA[`}, 195 {"]]>", `]]\u003e`}, 196 // https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span 197 // "The text in style, script, title, and textarea elements 198 // must not have an escaping text span start that is not 199 // followed by an escaping text span end." 200 // Furthermore, spoofing an escaping text span end could lead 201 // to different interpretation of a </script> sequence otherwise 202 // masked by the escaping text span, and spoofing a start could 203 // allow regular text content to be interpreted as script 204 // allowing script execution via a combination of a JS string 205 // injection followed by an HTML text injection. 206 {"<!--", `\u003c!--`}, 207 {"-->", `--\u003e`}, 208 // From https://code.google.com/p/doctype/wiki/ArticleUtf7 209 {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-", 210 `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`, 211 }, 212 // Invalid UTF-8 sequence 213 {"foo\xA0bar", "foo\xA0bar"}, 214 // Invalid unicode scalar value. 215 {"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"}, 216 } 217 218 for _, test := range tests { 219 esc := jsStrEscaper(test.x) 220 if esc != test.esc { 221 t.Errorf("%q: want %q got %q", test.x, test.esc, esc) 222 } 223 } 224 } 225 226 func TestJSRegexpEscaper(t *testing.T) { 227 tests := []struct { 228 x any 229 esc string 230 }{ 231 {"", `(?:)`}, 232 {"foo", `foo`}, 233 {"\u0000", `\u0000`}, 234 {"\t", `\t`}, 235 {"\n", `\n`}, 236 {"\r", `\r`}, 237 {"\u2028", `\u2028`}, 238 {"\u2029", `\u2029`}, 239 {"\\", `\\`}, 240 {"\\n", `\\n`}, 241 {"foo\r\nbar", `foo\r\nbar`}, 242 // Preserve attribute boundaries. 243 {`"`, `\u0022`}, 244 {`'`, `\u0027`}, 245 // Allow embedding in HTML without further escaping. 246 {`&`, `\u0026amp;`}, 247 // Prevent breaking out of text node and element boundaries. 248 {"</script>", `\u003c\/script\u003e`}, 249 {"<![CDATA[", `\u003c!\[CDATA\[`}, 250 {"]]>", `\]\]\u003e`}, 251 // Escaping text spans. 252 {"<!--", `\u003c!\-\-`}, 253 {"-->", `\-\-\u003e`}, 254 {"*", `\*`}, 255 {"+", `\u002b`}, 256 {"?", `\?`}, 257 {"[](){}", `\[\]\(\)\{\}`}, 258 {"$foo|x.y", `\$foo\|x\.y`}, 259 {"x^y", `x\^y`}, 260 } 261 262 for _, test := range tests { 263 esc := jsRegexpEscaper(test.x) 264 if esc != test.esc { 265 t.Errorf("%q: want %q got %q", test.x, test.esc, esc) 266 } 267 } 268 } 269 270 func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { 271 input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 272 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 273 ` !"#$%&'()*+,-./` + 274 `0123456789:;<=>?` + 275 `@ABCDEFGHIJKLMNO` + 276 `PQRSTUVWXYZ[\]^_` + 277 "`abcdefghijklmno" + 278 "pqrstuvwxyz{|}~\x7f" + 279 "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") 280 281 tests := []struct { 282 name string 283 escaper func(...any) string 284 escaped string 285 }{ 286 { 287 "jsStrEscaper", 288 jsStrEscaper, 289 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` + 290 `\u0008\t\n\u000b\f\r\u000e\u000f` + 291 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` + 292 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` + 293 ` !\u0022#$%\u0026\u0027()*\u002b,-.\/` + 294 `0123456789:;\u003c=\u003e?` + 295 `@ABCDEFGHIJKLMNO` + 296 `PQRSTUVWXYZ[\\]^_` + 297 "\\u0060abcdefghijklmno" + 298 "pqrstuvwxyz{|}~\u007f" + 299 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", 300 }, 301 { 302 "jsRegexpEscaper", 303 jsRegexpEscaper, 304 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` + 305 `\u0008\t\n\u000b\f\r\u000e\u000f` + 306 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` + 307 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` + 308 ` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` + 309 `0123456789:;\u003c=\u003e\?` + 310 `@ABCDEFGHIJKLMNO` + 311 `PQRSTUVWXYZ\[\\\]\^_` + 312 "`abcdefghijklmno" + 313 `pqrstuvwxyz\{\|\}~` + "\u007f" + 314 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", 315 }, 316 } 317 318 for _, test := range tests { 319 if s := test.escaper(input); s != test.escaped { 320 t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) 321 continue 322 } 323 324 // Escape it rune by rune to make sure that any 325 // fast-path checking does not break escaping. 326 var buf strings.Builder 327 for _, c := range input { 328 buf.WriteString(test.escaper(string(c))) 329 } 330 331 if s := buf.String(); s != test.escaped { 332 t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) 333 continue 334 } 335 } 336 } 337 338 func TestIsJsMimeType(t *testing.T) { 339 tests := []struct { 340 in string 341 out bool 342 }{ 343 {"application/javascript;version=1.8", true}, 344 {"application/javascript;version=1.8;foo=bar", true}, 345 {"application/javascript/version=1.8", false}, 346 {"text/javascript", true}, 347 {"application/json", true}, 348 {"application/ld+json", true}, 349 {"module", true}, 350 } 351 352 for _, test := range tests { 353 if isJSType(test.in) != test.out { 354 t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out) 355 } 356 } 357 } 358 359 func BenchmarkJSValEscaperWithNum(b *testing.B) { 360 for i := 0; i < b.N; i++ { 361 jsValEscaper(3.141592654) 362 } 363 } 364 365 func BenchmarkJSValEscaperWithStr(b *testing.B) { 366 for i := 0; i < b.N; i++ { 367 jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 368 } 369 } 370 371 func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) { 372 for i := 0; i < b.N; i++ { 373 jsValEscaper("The quick, brown fox jumps over the lazy dog") 374 } 375 } 376 377 func BenchmarkJSValEscaperWithObj(b *testing.B) { 378 o := struct { 379 S string 380 N int 381 }{ 382 "The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028", 383 42, 384 } 385 for i := 0; i < b.N; i++ { 386 jsValEscaper(o) 387 } 388 } 389 390 func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) { 391 o := struct { 392 S string 393 N int 394 }{ 395 "The quick, brown fox jumps over the lazy dog", 396 42, 397 } 398 for i := 0; i < b.N; i++ { 399 jsValEscaper(o) 400 } 401 } 402 403 func BenchmarkJSStrEscaperNoSpecials(b *testing.B) { 404 for i := 0; i < b.N; i++ { 405 jsStrEscaper("The quick, brown fox jumps over the lazy dog.") 406 } 407 } 408 409 func BenchmarkJSStrEscaper(b *testing.B) { 410 for i := 0; i < b.N; i++ { 411 jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 412 } 413 } 414 415 func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) { 416 for i := 0; i < b.N; i++ { 417 jsRegexpEscaper("The quick, brown fox jumps over the lazy dog") 418 } 419 } 420 421 func BenchmarkJSRegexpEscaper(b *testing.B) { 422 for i := 0; i < b.N; i++ { 423 jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 424 } 425 }