WebEncoders.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // modified from
  2. // https://github.com/dotnet/aspnetcore/blob/fd060ce8c36ffe195b9e9a69a1bbd8fb53cc6d7c/src/Shared/WebEncoders/WebEncoders.cs
  3. // Copyright (c) .NET Foundation. All rights reserved.
  4. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  5. #if NETCOREAPP
  6. using System.Buffers;
  7. #endif
  8. using System.Diagnostics;
  9. using System.Diagnostics.CodeAnalysis;
  10. namespace Jint.Extensions;
  11. /// <summary>
  12. /// Contains utility APIs to assist with common encoding and decoding operations.
  13. /// </summary>
  14. [SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper")]
  15. [SuppressMessage("Maintainability", "CA1512:Use ArgumentOutOfRangeException throw helper")]
  16. internal static class WebEncoders
  17. {
  18. private static readonly byte[] EmptyBytes = [];
  19. /// <summary>
  20. /// Decodes a base64url-encoded string.
  21. /// </summary>
  22. /// <param name="input">The base64url-encoded input to decode.</param>
  23. /// <returns>The base64url-decoded form of the input.</returns>
  24. /// <remarks>
  25. /// The input must not contain any whitespace or padding characters.
  26. /// Throws <see cref="FormatException"/> if the input is malformed.
  27. /// </remarks>
  28. public static byte[] Base64UrlDecode(ReadOnlySpan<char> input)
  29. {
  30. // Special-case empty input
  31. if (input.Length == 0)
  32. {
  33. return EmptyBytes;
  34. }
  35. // Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form.
  36. var buffer = new char[GetArraySizeRequiredToDecode(input.Length)];
  37. return Base64UrlDecode(input, buffer);
  38. }
  39. /// <summary>
  40. /// Decodes a base64url-encoded <paramref name="input"/> into a <c>byte[]</c>.
  41. /// </summary>
  42. public static byte[] Base64UrlDecode(ReadOnlySpan<char> input, char[] buffer)
  43. {
  44. if (input.Length == 0)
  45. {
  46. return EmptyBytes;
  47. }
  48. // Assumption: input is base64url encoded without padding and contains no whitespace.
  49. var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(input.Length);
  50. var arraySizeRequired = checked(input.Length + paddingCharsToAdd);
  51. Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4.");
  52. // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'.
  53. var i = 0;
  54. for (var j = 0; i < input.Length; i++, j++)
  55. {
  56. var ch = input[j];
  57. if (ch == '-')
  58. {
  59. buffer[i] = '+';
  60. }
  61. else if (ch == '_')
  62. {
  63. buffer[i] = '/';
  64. }
  65. else
  66. {
  67. buffer[i] = ch;
  68. }
  69. }
  70. // Add the padding characters back.
  71. for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--)
  72. {
  73. buffer[i] = '=';
  74. }
  75. // Decode.
  76. // If the caller provided invalid base64 chars, they'll be caught here.
  77. return Convert.FromBase64CharArray(buffer, 0, arraySizeRequired);
  78. }
  79. private static int GetArraySizeRequiredToDecode(int count)
  80. {
  81. if (count == 0)
  82. {
  83. return 0;
  84. }
  85. var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
  86. return checked(count + numPaddingCharsToAdd);
  87. }
  88. /// <summary>
  89. /// Encodes <paramref name="input"/> using base64url encoding.
  90. /// </summary>
  91. /// <param name="input">The binary input to encode.</param>
  92. /// <param name="omitPadding"></param>
  93. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  94. public static string Base64UrlEncode(byte[] input, bool omitPadding)
  95. {
  96. if (input == null)
  97. {
  98. throw new ArgumentNullException(nameof(input));
  99. }
  100. return Base64UrlEncode(input, offset: 0, count: input.Length, omitPadding);
  101. }
  102. /// <summary>
  103. /// Encodes <paramref name="input"/> using base64url encoding.
  104. /// </summary>
  105. /// <param name="input">The binary input to encode.</param>
  106. /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
  107. /// <param name="count">The number of bytes from <paramref name="input"/> to encode.</param>
  108. /// <param name="omitPadding"></param>
  109. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  110. public static string Base64UrlEncode(byte[] input, int offset, int count, bool omitPadding)
  111. {
  112. if (input == null)
  113. {
  114. throw new ArgumentNullException(nameof(input));
  115. }
  116. #if NETCOREAPP
  117. return Base64UrlEncode(input.AsSpan(offset, count), omitPadding);
  118. #else
  119. // Special-case empty input
  120. if (count == 0)
  121. {
  122. return string.Empty;
  123. }
  124. var buffer = new char[GetArraySizeRequiredToEncode(count)];
  125. var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count, omitPadding);
  126. return new string(buffer, startIndex: 0, length: numBase64Chars);
  127. #endif
  128. }
  129. /// <summary>
  130. /// Encodes <paramref name="input"/> using base64url encoding.
  131. /// </summary>
  132. /// <param name="input">The binary input to encode.</param>
  133. /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
  134. /// <param name="output">
  135. /// Buffer to receive the base64url-encoded form of <paramref name="input"/>. Array must be large enough to
  136. /// hold <paramref name="outputOffset"/> characters and the full base64-encoded form of
  137. /// <paramref name="input"/>, including padding characters.
  138. /// </param>
  139. /// <param name="outputOffset">
  140. /// The offset into <paramref name="output"/> at which to begin writing the base64url-encoded form of
  141. /// <paramref name="input"/>.
  142. /// </param>
  143. /// <param name="count">The number of <c>byte</c>s from <paramref name="input"/> to encode.</param>
  144. /// <param name="omitPadding"></param>
  145. /// <returns>
  146. /// The number of characters written to <paramref name="output"/>, less any padding characters.
  147. /// </returns>
  148. public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count, bool omitPadding)
  149. {
  150. if (input == null)
  151. {
  152. throw new ArgumentNullException(nameof(input));
  153. }
  154. if (output == null)
  155. {
  156. throw new ArgumentNullException(nameof(output));
  157. }
  158. if (outputOffset < 0)
  159. {
  160. throw new ArgumentOutOfRangeException(nameof(outputOffset));
  161. }
  162. var arraySizeRequired = GetArraySizeRequiredToEncode(count);
  163. if (output.Length - outputOffset < arraySizeRequired)
  164. {
  165. throw new ArgumentException("invalid", nameof(count));
  166. }
  167. #if NETCOREAPP
  168. return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset), omitPadding);
  169. #else
  170. // Special-case empty input.
  171. if (count == 0)
  172. {
  173. return 0;
  174. }
  175. // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
  176. // Start with default Base64 encoding.
  177. var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset);
  178. // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
  179. for (var i = outputOffset; i - outputOffset < numBase64Chars; i++)
  180. {
  181. var ch = output[i];
  182. if (ch == '+')
  183. {
  184. output[i] = '-';
  185. }
  186. else if (ch == '/')
  187. {
  188. output[i] = '_';
  189. }
  190. else if (omitPadding && ch == '=')
  191. {
  192. // We've reached a padding character; truncate the remainder.
  193. return i - outputOffset;
  194. }
  195. }
  196. return numBase64Chars;
  197. #endif
  198. }
  199. /// <summary>
  200. /// Get the minimum output <c>char[]</c> size required for encoding <paramref name="count"/>
  201. /// <see cref="byte"/>s with the <see cref="Base64UrlEncode(byte[], int, char[], int, int, bool)"/> method.
  202. /// </summary>
  203. /// <param name="count">The number of characters to encode.</param>
  204. /// <returns>
  205. /// The minimum output <c>char[]</c> size required for encoding <paramref name="count"/> <see cref="byte"/>s.
  206. /// </returns>
  207. public static int GetArraySizeRequiredToEncode(int count)
  208. {
  209. var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
  210. return checked(numWholeOrPartialInputBlocks * 4);
  211. }
  212. #if NETCOREAPP
  213. /// <summary>
  214. /// Encodes <paramref name="input"/> using base64url encoding.
  215. /// </summary>
  216. /// <param name="input">The binary input to encode.</param>
  217. /// <param name="omitPadding"></param>
  218. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  219. public static string Base64UrlEncode(ReadOnlySpan<byte> input, bool omitPadding)
  220. {
  221. if (input.IsEmpty)
  222. {
  223. return string.Empty;
  224. }
  225. int bufferSize = GetArraySizeRequiredToEncode(input.Length);
  226. char[]? bufferToReturnToPool = null;
  227. Span<char> buffer = bufferSize <= 128
  228. ? stackalloc char[bufferSize]
  229. : bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize);
  230. var numBase64Chars = Base64UrlEncode(input, buffer, omitPadding);
  231. var base64Url = new string(buffer.Slice(0, numBase64Chars));
  232. if (bufferToReturnToPool != null)
  233. {
  234. ArrayPool<char>.Shared.Return(bufferToReturnToPool);
  235. }
  236. return base64Url;
  237. }
  238. private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output, bool omitPadding)
  239. {
  240. Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length));
  241. if (input.IsEmpty)
  242. {
  243. return 0;
  244. }
  245. // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
  246. Convert.TryToBase64Chars(input, output, out int charsWritten);
  247. // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
  248. for (var i = 0; i < charsWritten; i++)
  249. {
  250. var ch = output[i];
  251. if (ch == '+')
  252. {
  253. output[i] = '-';
  254. }
  255. else if (ch == '/')
  256. {
  257. output[i] = '_';
  258. }
  259. else if (omitPadding && ch == '=')
  260. {
  261. // We've reached a padding character; truncate the remainder.
  262. return i;
  263. }
  264. }
  265. return charsWritten;
  266. }
  267. #endif
  268. private static int GetNumBase64PaddingCharsInString(string str)
  269. {
  270. // Assumption: input contains a well-formed base64 string with no whitespace.
  271. // base64 guaranteed have 0 - 2 padding characters.
  272. if (str[str.Length - 1] == '=')
  273. {
  274. if (str[str.Length - 2] == '=')
  275. {
  276. return 2;
  277. }
  278. return 1;
  279. }
  280. return 0;
  281. }
  282. private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
  283. {
  284. switch (inputLength % 4)
  285. {
  286. case 0:
  287. return 0;
  288. case 2:
  289. return 2;
  290. case 3:
  291. return 1;
  292. default:
  293. throw new FormatException("invalid length");
  294. }
  295. }
  296. }