You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

567 lines
22 KiB

  1. #region License
  2. // Copyright (c) 2007 James Newton-King
  3. //
  4. // Permission is hereby granted, free of charge, to any person
  5. // obtaining a copy of this software and associated documentation
  6. // files (the "Software"), to deal in the Software without
  7. // restriction, including without limitation the rights to use,
  8. // copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following
  11. // conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be
  14. // included in all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18. // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23. // OTHER DEALINGS IN THE SOFTWARE.
  24. #endregion
  25. using System;
  26. using System.IO;
  27. #if HAVE_ASYNC
  28. using System.Threading;
  29. using System.Threading.Tasks;
  30. #endif
  31. using System.Collections.Generic;
  32. using System.Diagnostics;
  33. #if NET20
  34. using Newtonsoft.Json.Utilities.LinqBridge;
  35. #else
  36. using System.Linq;
  37. #endif
  38. namespace Newtonsoft.Json.Utilities
  39. {
  40. internal static class BufferUtils
  41. {
  42. public static char[] RentBuffer(IArrayPool<char> bufferPool, int minSize)
  43. {
  44. if (bufferPool == null)
  45. {
  46. return new char[minSize];
  47. }
  48. char[] buffer = bufferPool.Rent(minSize);
  49. return buffer;
  50. }
  51. public static void ReturnBuffer(IArrayPool<char> bufferPool, char[] buffer)
  52. {
  53. bufferPool?.Return(buffer);
  54. }
  55. public static char[] EnsureBufferSize(IArrayPool<char> bufferPool, int size, char[] buffer)
  56. {
  57. if (bufferPool == null)
  58. {
  59. return new char[size];
  60. }
  61. if (buffer != null)
  62. {
  63. bufferPool.Return(buffer);
  64. }
  65. return bufferPool.Rent(size);
  66. }
  67. }
  68. internal static class JavaScriptUtils
  69. {
  70. internal static readonly bool[] SingleQuoteCharEscapeFlags = new bool[128];
  71. internal static readonly bool[] DoubleQuoteCharEscapeFlags = new bool[128];
  72. internal static readonly bool[] HtmlCharEscapeFlags = new bool[128];
  73. private const int UnicodeTextLength = 6;
  74. static JavaScriptUtils()
  75. {
  76. IList<char> escapeChars = new List<char>
  77. {
  78. '\n', '\r', '\t', '\\', '\f', '\b',
  79. };
  80. for (int i = 0; i < ' '; i++)
  81. {
  82. escapeChars.Add((char)i);
  83. }
  84. foreach (char escapeChar in escapeChars.Union(new[] { '\'' }))
  85. {
  86. SingleQuoteCharEscapeFlags[escapeChar] = true;
  87. }
  88. foreach (char escapeChar in escapeChars.Union(new[] { '"' }))
  89. {
  90. DoubleQuoteCharEscapeFlags[escapeChar] = true;
  91. }
  92. foreach (char escapeChar in escapeChars.Union(new[] { '"', '\'', '<', '>', '&' }))
  93. {
  94. HtmlCharEscapeFlags[escapeChar] = true;
  95. }
  96. }
  97. private const string EscapedUnicodeText = "!";
  98. public static bool[] GetCharEscapeFlags(StringEscapeHandling stringEscapeHandling, char quoteChar)
  99. {
  100. if (stringEscapeHandling == StringEscapeHandling.EscapeHtml)
  101. {
  102. return HtmlCharEscapeFlags;
  103. }
  104. if (quoteChar == '"')
  105. {
  106. return DoubleQuoteCharEscapeFlags;
  107. }
  108. return SingleQuoteCharEscapeFlags;
  109. }
  110. public static bool ShouldEscapeJavaScriptString(string s, bool[] charEscapeFlags)
  111. {
  112. if (s == null)
  113. {
  114. return false;
  115. }
  116. foreach (char c in s)
  117. {
  118. if (c >= charEscapeFlags.Length || charEscapeFlags[c])
  119. {
  120. return true;
  121. }
  122. }
  123. return false;
  124. }
  125. public static void WriteEscapedJavaScriptString(TextWriter writer, string s, char delimiter, bool appendDelimiters,
  126. bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling, IArrayPool<char> bufferPool, ref char[] writeBuffer)
  127. {
  128. // leading delimiter
  129. if (appendDelimiters)
  130. {
  131. writer.Write(delimiter);
  132. }
  133. if (!string.IsNullOrEmpty(s))
  134. {
  135. int lastWritePosition = FirstCharToEscape(s, charEscapeFlags, stringEscapeHandling);
  136. if (lastWritePosition == -1)
  137. {
  138. writer.Write(s);
  139. }
  140. else
  141. {
  142. if (lastWritePosition != 0)
  143. {
  144. if (writeBuffer == null || writeBuffer.Length < lastWritePosition)
  145. {
  146. writeBuffer = BufferUtils.EnsureBufferSize(bufferPool, lastWritePosition, writeBuffer);
  147. }
  148. // write unchanged chars at start of text.
  149. s.CopyTo(0, writeBuffer, 0, lastWritePosition);
  150. writer.Write(writeBuffer, 0, lastWritePosition);
  151. }
  152. int length;
  153. for (int i = lastWritePosition; i < s.Length; i++)
  154. {
  155. char c = s[i];
  156. if (c < charEscapeFlags.Length && !charEscapeFlags[c])
  157. {
  158. continue;
  159. }
  160. string escapedValue;
  161. switch (c)
  162. {
  163. case '\t':
  164. escapedValue = @"\t";
  165. break;
  166. case '\n':
  167. escapedValue = @"\n";
  168. break;
  169. case '\r':
  170. escapedValue = @"\r";
  171. break;
  172. case '\f':
  173. escapedValue = @"\f";
  174. break;
  175. case '\b':
  176. escapedValue = @"\b";
  177. break;
  178. case '\\':
  179. escapedValue = @"\\";
  180. break;
  181. case '\u0085': // Next Line
  182. escapedValue = @"\u0085";
  183. break;
  184. case '\u2028': // Line Separator
  185. escapedValue = @"\u2028";
  186. break;
  187. case '\u2029': // Paragraph Separator
  188. escapedValue = @"\u2029";
  189. break;
  190. default:
  191. if (c < charEscapeFlags.Length || stringEscapeHandling == StringEscapeHandling.EscapeNonAscii)
  192. {
  193. if (c == '\'' && stringEscapeHandling != StringEscapeHandling.EscapeHtml)
  194. {
  195. escapedValue = @"\'";
  196. }
  197. else if (c == '"' && stringEscapeHandling != StringEscapeHandling.EscapeHtml)
  198. {
  199. escapedValue = @"\""";
  200. }
  201. else
  202. {
  203. if (writeBuffer == null || writeBuffer.Length < UnicodeTextLength)
  204. {
  205. writeBuffer = BufferUtils.EnsureBufferSize(bufferPool, UnicodeTextLength, writeBuffer);
  206. }
  207. StringUtils.ToCharAsUnicode(c, writeBuffer);
  208. // slightly hacky but it saves multiple conditions in if test
  209. escapedValue = EscapedUnicodeText;
  210. }
  211. }
  212. else
  213. {
  214. escapedValue = null;
  215. }
  216. break;
  217. }
  218. if (escapedValue == null)
  219. {
  220. continue;
  221. }
  222. bool isEscapedUnicodeText = string.Equals(escapedValue, EscapedUnicodeText);
  223. if (i > lastWritePosition)
  224. {
  225. length = i - lastWritePosition + ((isEscapedUnicodeText) ? UnicodeTextLength : 0);
  226. int start = (isEscapedUnicodeText) ? UnicodeTextLength : 0;
  227. if (writeBuffer == null || writeBuffer.Length < length)
  228. {
  229. char[] newBuffer = BufferUtils.RentBuffer(bufferPool, length);
  230. // the unicode text is already in the buffer
  231. // copy it over when creating new buffer
  232. if (isEscapedUnicodeText)
  233. {
  234. Debug.Assert(writeBuffer != null, "Write buffer should never be null because it is set when the escaped unicode text is encountered.");
  235. Array.Copy(writeBuffer, newBuffer, UnicodeTextLength);
  236. }
  237. BufferUtils.ReturnBuffer(bufferPool, writeBuffer);
  238. writeBuffer = newBuffer;
  239. }
  240. s.CopyTo(lastWritePosition, writeBuffer, start, length - start);
  241. // write unchanged chars before writing escaped text
  242. writer.Write(writeBuffer, start, length - start);
  243. }
  244. lastWritePosition = i + 1;
  245. if (!isEscapedUnicodeText)
  246. {
  247. writer.Write(escapedValue);
  248. }
  249. else
  250. {
  251. writer.Write(writeBuffer, 0, UnicodeTextLength);
  252. }
  253. }
  254. Debug.Assert(lastWritePosition != 0);
  255. length = s.Length - lastWritePosition;
  256. if (length > 0)
  257. {
  258. if (writeBuffer == null || writeBuffer.Length < length)
  259. {
  260. writeBuffer = BufferUtils.EnsureBufferSize(bufferPool, length, writeBuffer);
  261. }
  262. s.CopyTo(lastWritePosition, writeBuffer, 0, length);
  263. // write remaining text
  264. writer.Write(writeBuffer, 0, length);
  265. }
  266. }
  267. }
  268. // trailing delimiter
  269. if (appendDelimiters)
  270. {
  271. writer.Write(delimiter);
  272. }
  273. }
  274. public static string ToEscapedJavaScriptString(string value, char delimiter, bool appendDelimiters, StringEscapeHandling stringEscapeHandling)
  275. {
  276. bool[] charEscapeFlags = GetCharEscapeFlags(stringEscapeHandling, delimiter);
  277. using (StringWriter w = StringUtils.CreateStringWriter(value?.Length ?? 16))
  278. {
  279. char[] buffer = null;
  280. WriteEscapedJavaScriptString(w, value, delimiter, appendDelimiters, charEscapeFlags, stringEscapeHandling, null, ref buffer);
  281. return w.ToString();
  282. }
  283. }
  284. private static int FirstCharToEscape(string s, bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling)
  285. {
  286. for (int i = 0; i != s.Length; i++)
  287. {
  288. char c = s[i];
  289. if (c < charEscapeFlags.Length)
  290. {
  291. if (charEscapeFlags[c])
  292. {
  293. return i;
  294. }
  295. }
  296. else if (stringEscapeHandling == StringEscapeHandling.EscapeNonAscii)
  297. {
  298. return i;
  299. }
  300. else
  301. {
  302. switch (c)
  303. {
  304. case '\u0085':
  305. case '\u2028':
  306. case '\u2029':
  307. return i;
  308. }
  309. }
  310. }
  311. return -1;
  312. }
  313. #if HAVE_ASYNC
  314. public static Task WriteEscapedJavaScriptStringAsync(TextWriter writer, string s, char delimiter, bool appendDelimiters, bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling, JsonTextWriter client, char[] writeBuffer, CancellationToken cancellationToken = default(CancellationToken))
  315. {
  316. if (cancellationToken.IsCancellationRequested)
  317. {
  318. return cancellationToken.FromCanceled();
  319. }
  320. if (appendDelimiters)
  321. {
  322. return WriteEscapedJavaScriptStringWithDelimitersAsync(writer, s, delimiter, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken);
  323. }
  324. if (string.IsNullOrEmpty(s))
  325. {
  326. return cancellationToken.CancelIfRequestedAsync() ?? AsyncUtils.CompletedTask;
  327. }
  328. return WriteEscapedJavaScriptStringWithoutDelimitersAsync(writer, s, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken);
  329. }
  330. private static Task WriteEscapedJavaScriptStringWithDelimitersAsync(TextWriter writer, string s, char delimiter,
  331. bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling, JsonTextWriter client, char[] writeBuffer, CancellationToken cancellationToken)
  332. {
  333. Task task = writer.WriteAsync(delimiter, cancellationToken);
  334. if (!task.IsCompletedSucessfully())
  335. {
  336. return WriteEscapedJavaScriptStringWithDelimitersAsync(task, writer, s, delimiter, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken);
  337. }
  338. if (!string.IsNullOrEmpty(s))
  339. {
  340. task = WriteEscapedJavaScriptStringWithoutDelimitersAsync(writer, s, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken);
  341. if (task.IsCompletedSucessfully())
  342. {
  343. return writer.WriteAsync(delimiter, cancellationToken);
  344. }
  345. }
  346. return WriteCharAsync(task, writer, delimiter, cancellationToken);
  347. }
  348. private static async Task WriteEscapedJavaScriptStringWithDelimitersAsync(Task task, TextWriter writer, string s, char delimiter,
  349. bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling, JsonTextWriter client, char[] writeBuffer, CancellationToken cancellationToken)
  350. {
  351. await task.ConfigureAwait(false);
  352. if (!string.IsNullOrEmpty(s))
  353. {
  354. await WriteEscapedJavaScriptStringWithoutDelimitersAsync(writer, s, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken).ConfigureAwait(false);
  355. }
  356. await writer.WriteAsync(delimiter).ConfigureAwait(false);
  357. }
  358. public static async Task WriteCharAsync(Task task, TextWriter writer, char c, CancellationToken cancellationToken)
  359. {
  360. await task.ConfigureAwait(false);
  361. await writer.WriteAsync(c, cancellationToken).ConfigureAwait(false);
  362. }
  363. private static Task WriteEscapedJavaScriptStringWithoutDelimitersAsync(
  364. TextWriter writer, string s, bool[] charEscapeFlags, StringEscapeHandling stringEscapeHandling,
  365. JsonTextWriter client, char[] writeBuffer, CancellationToken cancellationToken)
  366. {
  367. int i = FirstCharToEscape(s, charEscapeFlags, stringEscapeHandling);
  368. return i == -1
  369. ? writer.WriteAsync(s, cancellationToken)
  370. : WriteDefinitelyEscapedJavaScriptStringWithoutDelimitersAsync(writer, s, i, charEscapeFlags, stringEscapeHandling, client, writeBuffer, cancellationToken);
  371. }
  372. private static async Task WriteDefinitelyEscapedJavaScriptStringWithoutDelimitersAsync(
  373. TextWriter writer, string s, int lastWritePosition, bool[] charEscapeFlags,
  374. StringEscapeHandling stringEscapeHandling, JsonTextWriter client, char[] writeBuffer,
  375. CancellationToken cancellationToken)
  376. {
  377. if (writeBuffer == null || writeBuffer.Length < lastWritePosition)
  378. {
  379. writeBuffer = client.EnsureWriteBuffer(lastWritePosition, UnicodeTextLength);
  380. }
  381. if (lastWritePosition != 0)
  382. {
  383. s.CopyTo(0, writeBuffer, 0, lastWritePosition);
  384. // write unchanged chars at start of text.
  385. await writer.WriteAsync(writeBuffer, 0, lastWritePosition, cancellationToken).ConfigureAwait(false);
  386. }
  387. int length;
  388. bool isEscapedUnicodeText = false;
  389. string escapedValue = null;
  390. for (int i = lastWritePosition; i < s.Length; i++)
  391. {
  392. char c = s[i];
  393. if (c < charEscapeFlags.Length && !charEscapeFlags[c])
  394. {
  395. continue;
  396. }
  397. switch (c)
  398. {
  399. case '\t':
  400. escapedValue = @"\t";
  401. break;
  402. case '\n':
  403. escapedValue = @"\n";
  404. break;
  405. case '\r':
  406. escapedValue = @"\r";
  407. break;
  408. case '\f':
  409. escapedValue = @"\f";
  410. break;
  411. case '\b':
  412. escapedValue = @"\b";
  413. break;
  414. case '\\':
  415. escapedValue = @"\\";
  416. break;
  417. case '\u0085': // Next Line
  418. escapedValue = @"\u0085";
  419. break;
  420. case '\u2028': // Line Separator
  421. escapedValue = @"\u2028";
  422. break;
  423. case '\u2029': // Paragraph Separator
  424. escapedValue = @"\u2029";
  425. break;
  426. default:
  427. if (c < charEscapeFlags.Length || stringEscapeHandling == StringEscapeHandling.EscapeNonAscii)
  428. {
  429. if (c == '\'' && stringEscapeHandling != StringEscapeHandling.EscapeHtml)
  430. {
  431. escapedValue = @"\'";
  432. }
  433. else if (c == '"' && stringEscapeHandling != StringEscapeHandling.EscapeHtml)
  434. {
  435. escapedValue = @"\""";
  436. }
  437. else
  438. {
  439. if (writeBuffer.Length < UnicodeTextLength)
  440. {
  441. writeBuffer = client.EnsureWriteBuffer(UnicodeTextLength, 0);
  442. }
  443. StringUtils.ToCharAsUnicode(c, writeBuffer);
  444. isEscapedUnicodeText = true;
  445. }
  446. }
  447. else
  448. {
  449. continue;
  450. }
  451. break;
  452. }
  453. if (i > lastWritePosition)
  454. {
  455. length = i - lastWritePosition + (isEscapedUnicodeText ? UnicodeTextLength : 0);
  456. int start = isEscapedUnicodeText ? UnicodeTextLength : 0;
  457. if (writeBuffer.Length < length)
  458. {
  459. writeBuffer = client.EnsureWriteBuffer(length, UnicodeTextLength);
  460. }
  461. s.CopyTo(lastWritePosition, writeBuffer, start, length - start);
  462. // write unchanged chars before writing escaped text
  463. await writer.WriteAsync(writeBuffer, start, length - start, cancellationToken).ConfigureAwait(false);
  464. }
  465. lastWritePosition = i + 1;
  466. if (!isEscapedUnicodeText)
  467. {
  468. await writer.WriteAsync(escapedValue, cancellationToken).ConfigureAwait(false);
  469. }
  470. else
  471. {
  472. await writer.WriteAsync(writeBuffer, 0, UnicodeTextLength, cancellationToken).ConfigureAwait(false);
  473. isEscapedUnicodeText = false;
  474. }
  475. }
  476. length = s.Length - lastWritePosition;
  477. if (length != 0)
  478. {
  479. if (writeBuffer.Length < length)
  480. {
  481. writeBuffer = client.EnsureWriteBuffer(length, 0);
  482. }
  483. s.CopyTo(lastWritePosition, writeBuffer, 0, length);
  484. // write remaining text
  485. await writer.WriteAsync(writeBuffer, 0, length, cancellationToken).ConfigureAwait(false);
  486. }
  487. }
  488. #endif
  489. }
  490. }