js_test.go 12 KB

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