WebEncoders.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  93. public static string Base64UrlEncode(byte[] input)
  94. {
  95. if (input == null)
  96. {
  97. throw new ArgumentNullException(nameof(input));
  98. }
  99. return Base64UrlEncode(input, offset: 0, count: input.Length);
  100. }
  101. /// <summary>
  102. /// Encodes <paramref name="input"/> using base64url encoding.
  103. /// </summary>
  104. /// <param name="input">The binary input to encode.</param>
  105. /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
  106. /// <param name="count">The number of bytes from <paramref name="input"/> to encode.</param>
  107. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  108. public static string Base64UrlEncode(byte[] input, int offset, int count)
  109. {
  110. if (input == null)
  111. {
  112. throw new ArgumentNullException(nameof(input));
  113. }
  114. #if NETCOREAPP
  115. return Base64UrlEncode(input.AsSpan(offset, count));
  116. #else
  117. // Special-case empty input
  118. if (count == 0)
  119. {
  120. return string.Empty;
  121. }
  122. var buffer = new char[GetArraySizeRequiredToEncode(count)];
  123. var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count);
  124. return new string(buffer, startIndex: 0, length: numBase64Chars);
  125. #endif
  126. }
  127. /// <summary>
  128. /// Encodes <paramref name="input"/> using base64url encoding.
  129. /// </summary>
  130. /// <param name="input">The binary input to encode.</param>
  131. /// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
  132. /// <param name="output">
  133. /// Buffer to receive the base64url-encoded form of <paramref name="input"/>. Array must be large enough to
  134. /// hold <paramref name="outputOffset"/> characters and the full base64-encoded form of
  135. /// <paramref name="input"/>, including padding characters.
  136. /// </param>
  137. /// <param name="outputOffset">
  138. /// The offset into <paramref name="output"/> at which to begin writing the base64url-encoded form of
  139. /// <paramref name="input"/>.
  140. /// </param>
  141. /// <param name="count">The number of <c>byte</c>s from <paramref name="input"/> to encode.</param>
  142. /// <returns>
  143. /// The number of characters written to <paramref name="output"/>, less any padding characters.
  144. /// </returns>
  145. public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count)
  146. {
  147. if (input == null)
  148. {
  149. throw new ArgumentNullException(nameof(input));
  150. }
  151. if (output == null)
  152. {
  153. throw new ArgumentNullException(nameof(output));
  154. }
  155. if (outputOffset < 0)
  156. {
  157. throw new ArgumentOutOfRangeException(nameof(outputOffset));
  158. }
  159. var arraySizeRequired = GetArraySizeRequiredToEncode(count);
  160. if (output.Length - outputOffset < arraySizeRequired)
  161. {
  162. throw new ArgumentException("invalid", nameof(count));
  163. }
  164. #if NETCOREAPP
  165. return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset));
  166. #else
  167. // Special-case empty input.
  168. if (count == 0)
  169. {
  170. return 0;
  171. }
  172. // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
  173. // Start with default Base64 encoding.
  174. var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset);
  175. // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
  176. for (var i = outputOffset; i - outputOffset < numBase64Chars; i++)
  177. {
  178. var ch = output[i];
  179. if (ch == '+')
  180. {
  181. output[i] = '-';
  182. }
  183. else if (ch == '/')
  184. {
  185. output[i] = '_';
  186. }
  187. else if (ch == '=')
  188. {
  189. // We've reached a padding character; truncate the remainder.
  190. return i - outputOffset;
  191. }
  192. }
  193. return numBase64Chars;
  194. #endif
  195. }
  196. /// <summary>
  197. /// Get the minimum output <c>char[]</c> size required for encoding <paramref name="count"/>
  198. /// <see cref="byte"/>s with the <see cref="Base64UrlEncode(byte[], int, char[], int, int)"/> method.
  199. /// </summary>
  200. /// <param name="count">The number of characters to encode.</param>
  201. /// <returns>
  202. /// The minimum output <c>char[]</c> size required for encoding <paramref name="count"/> <see cref="byte"/>s.
  203. /// </returns>
  204. public static int GetArraySizeRequiredToEncode(int count)
  205. {
  206. var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
  207. return checked(numWholeOrPartialInputBlocks * 4);
  208. }
  209. #if NETCOREAPP
  210. /// <summary>
  211. /// Encodes <paramref name="input"/> using base64url encoding.
  212. /// </summary>
  213. /// <param name="input">The binary input to encode.</param>
  214. /// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
  215. public static string Base64UrlEncode(ReadOnlySpan<byte> input)
  216. {
  217. if (input.IsEmpty)
  218. {
  219. return string.Empty;
  220. }
  221. int bufferSize = GetArraySizeRequiredToEncode(input.Length);
  222. char[]? bufferToReturnToPool = null;
  223. Span<char> buffer = bufferSize <= 128
  224. ? stackalloc char[bufferSize]
  225. : bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize);
  226. var numBase64Chars = Base64UrlEncode(input, buffer);
  227. var base64Url = new string(buffer.Slice(0, numBase64Chars));
  228. if (bufferToReturnToPool != null)
  229. {
  230. ArrayPool<char>.Shared.Return(bufferToReturnToPool);
  231. }
  232. return base64Url;
  233. }
  234. private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output)
  235. {
  236. Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length));
  237. if (input.IsEmpty)
  238. {
  239. return 0;
  240. }
  241. // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
  242. Convert.TryToBase64Chars(input, output, out int charsWritten);
  243. // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
  244. for (var i = 0; i < charsWritten; i++)
  245. {
  246. var ch = output[i];
  247. if (ch == '+')
  248. {
  249. output[i] = '-';
  250. }
  251. else if (ch == '/')
  252. {
  253. output[i] = '_';
  254. }
  255. else if (ch == '=')
  256. {
  257. // We've reached a padding character; truncate the remainder.
  258. return i;
  259. }
  260. }
  261. return charsWritten;
  262. }
  263. #endif
  264. private static int GetNumBase64PaddingCharsInString(string str)
  265. {
  266. // Assumption: input contains a well-formed base64 string with no whitespace.
  267. // base64 guaranteed have 0 - 2 padding characters.
  268. if (str[str.Length - 1] == '=')
  269. {
  270. if (str[str.Length - 2] == '=')
  271. {
  272. return 2;
  273. }
  274. return 1;
  275. }
  276. return 0;
  277. }
  278. private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
  279. {
  280. switch (inputLength % 4)
  281. {
  282. case 0:
  283. return 0;
  284. case 2:
  285. return 2;
  286. case 3:
  287. return 1;
  288. default:
  289. throw new FormatException("invalid length");
  290. }
  291. }
  292. }