github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/tpl/internal/go_templates/htmltemplate/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 //go:build go1.13 && !windows 6 // +build go1.13,!windows 7 8 package template 9 10 import ( 11 "math" 12 "strings" 13 "testing" 14 ) 15 16 func TestNextJsCtx(t *testing.T) { 17 tests := []struct { 18 jsCtx jsCtx 19 s string 20 }{ 21 // Statement terminators precede regexps. 22 {jsCtxRegexp, ";"}, 23 // This is not airtight. 24 // ({ valueOf: function () { return 1 } } / 2) 25 // is valid JavaScript but in practice, devs do not do this. 26 // A block followed by a statement starting with a RegExp is 27 // much more common: 28 // while (x) {...} /foo/.test(x) || panic() 29 {jsCtxRegexp, "}"}, 30 // But member, call, grouping, and array expression terminators 31 // precede div ops. 32 {jsCtxDivOp, ")"}, 33 {jsCtxDivOp, "]"}, 34 // At the start of a primary expression, array, or expression 35 // statement, expect a regexp. 36 {jsCtxRegexp, "("}, 37 {jsCtxRegexp, "["}, 38 {jsCtxRegexp, "{"}, 39 // Assignment operators precede regexps as do all exclusively 40 // prefix and binary operators. 41 {jsCtxRegexp, "="}, 42 {jsCtxRegexp, "+="}, 43 {jsCtxRegexp, "*="}, 44 {jsCtxRegexp, "*"}, 45 {jsCtxRegexp, "!"}, 46 // Whether the + or - is infix or prefix, it cannot precede a 47 // div op. 48 {jsCtxRegexp, "+"}, 49 {jsCtxRegexp, "-"}, 50 // An incr/decr op precedes a div operator. 51 // This is not airtight. In (g = ++/h/i) a regexp follows a 52 // pre-increment operator, but in practice devs do not try to 53 // increment or decrement regular expressions. 54 // (g++/h/i) where ++ is a postfix operator on g is much more 55 // common. 56 {jsCtxDivOp, "--"}, 57 {jsCtxDivOp, "++"}, 58 {jsCtxDivOp, "x--"}, 59 // When we have many dashes or pluses, then they are grouped 60 // left to right. 61 {jsCtxRegexp, "x---"}, // A postfix -- then a -. 62 // return followed by a slash returns the regexp literal or the 63 // slash starts a regexp literal in an expression statement that 64 // is dead code. 65 {jsCtxRegexp, "return"}, 66 {jsCtxRegexp, "return "}, 67 {jsCtxRegexp, "return\t"}, 68 {jsCtxRegexp, "return\n"}, 69 {jsCtxRegexp, "return\u2028"}, 70 // Identifiers can be divided and cannot validly be preceded by 71 // a regular expressions. Semicolon insertion cannot happen 72 // between an identifier and a regular expression on a new line 73 // because the one token lookahead for semicolon insertion has 74 // to conclude that it could be a div binary op and treat it as 75 // such. 76 {jsCtxDivOp, "x"}, 77 {jsCtxDivOp, "x "}, 78 {jsCtxDivOp, "x\t"}, 79 {jsCtxDivOp, "x\n"}, 80 {jsCtxDivOp, "x\u2028"}, 81 {jsCtxDivOp, "preturn"}, 82 // Numbers precede div ops. 83 {jsCtxDivOp, "0"}, 84 // Dots that are part of a number are div preceders. 85 {jsCtxDivOp, "0."}, 86 } 87 88 for _, test := range tests { 89 if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx { 90 t.Errorf("want %s got %q", test.jsCtx, test.s) 91 } 92 if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx { 93 t.Errorf("want %s got %q", test.jsCtx, test.s) 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 "`abcdefghijklmno" + 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 }