Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Uint8Array to/from base64 #1911

Merged
merged 7 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.0" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.45.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="Test262Harness" Version="1.0.0" />
<PackageVersion Include="xunit" Version="2.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" PrivateAssets="all" />
Expand Down
1 change: 0 additions & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"regexp-v-flag",
"source-phase-imports",
"tail-call-optimization",
"uint8array-base64",
"Temporal",
"u180e"
],
Expand Down
338 changes: 338 additions & 0 deletions Jint/Extensions/WebEncoders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
// modified from
// https://github.com/dotnet/aspnetcore/blob/fd060ce8c36ffe195b9e9a69a1bbd8fb53cc6d7c/src/Shared/WebEncoders/WebEncoders.cs

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#if NETCOREAPP
using System.Buffers;
#endif
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace Jint.Extensions;

/// <summary>
/// Contains utility APIs to assist with common encoding and decoding operations.
/// </summary>
[SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper")]
[SuppressMessage("Maintainability", "CA1512:Use ArgumentOutOfRangeException throw helper")]
internal static class WebEncoders
{
private static readonly byte[] EmptyBytes = [];

/// <summary>
/// Decodes a base64url-encoded string.
/// </summary>
/// <param name="input">The base64url-encoded input to decode.</param>
/// <returns>The base64url-decoded form of the input.</returns>
/// <remarks>
/// The input must not contain any whitespace or padding characters.
/// Throws <see cref="FormatException"/> if the input is malformed.
/// </remarks>
public static byte[] Base64UrlDecode(ReadOnlySpan<char> input)
{
// Special-case empty input
if (input.Length == 0)
{
return EmptyBytes;
}

// Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form.
var buffer = new char[GetArraySizeRequiredToDecode(input.Length)];

return Base64UrlDecode(input, buffer);
}

/// <summary>
/// Decodes a base64url-encoded <paramref name="input"/> into a <c>byte[]</c>.
/// </summary>
public static byte[] Base64UrlDecode(ReadOnlySpan<char> input, char[] buffer)
{
if (input.Length == 0)
{
return EmptyBytes;
}

// Assumption: input is base64url encoded without padding and contains no whitespace.

var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(input.Length);
var arraySizeRequired = checked(input.Length + paddingCharsToAdd);
Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4.");

// Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'.
var i = 0;
for (var j = 0; i < input.Length; i++, j++)
{
var ch = input[j];
if (ch == '-')
{
buffer[i] = '+';
}
else if (ch == '_')
{
buffer[i] = '/';
}
else
{
buffer[i] = ch;
}
}

// Add the padding characters back.
for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--)
{
buffer[i] = '=';
}

// Decode.
// If the caller provided invalid base64 chars, they'll be caught here.
return Convert.FromBase64CharArray(buffer, 0, arraySizeRequired);
}

private static int GetArraySizeRequiredToDecode(int count)
{
if (count == 0)
{
return 0;
}

var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);

return checked(count + numPaddingCharsToAdd);
}

/// <summary>
/// Encodes <paramref name="input"/> using base64url encoding.
/// </summary>
/// <param name="input">The binary input to encode.</param>
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
public static string Base64UrlEncode(byte[] input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}

return Base64UrlEncode(input, offset: 0, count: input.Length);
}

/// <summary>
/// Encodes <paramref name="input"/> using base64url encoding.
/// </summary>
/// <param name="input">The binary input to encode.</param>
/// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
/// <param name="count">The number of bytes from <paramref name="input"/> to encode.</param>
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
public static string Base64UrlEncode(byte[] input, int offset, int count)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}

#if NETCOREAPP
return Base64UrlEncode(input.AsSpan(offset, count));
#else
// Special-case empty input
if (count == 0)
{
return string.Empty;
}

var buffer = new char[GetArraySizeRequiredToEncode(count)];
var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count);

return new string(buffer, startIndex: 0, length: numBase64Chars);
#endif
}

/// <summary>
/// Encodes <paramref name="input"/> using base64url encoding.
/// </summary>
/// <param name="input">The binary input to encode.</param>
/// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
/// <param name="output">
/// Buffer to receive the base64url-encoded form of <paramref name="input"/>. Array must be large enough to
/// hold <paramref name="outputOffset"/> characters and the full base64-encoded form of
/// <paramref name="input"/>, including padding characters.
/// </param>
/// <param name="outputOffset">
/// The offset into <paramref name="output"/> at which to begin writing the base64url-encoded form of
/// <paramref name="input"/>.
/// </param>
/// <param name="count">The number of <c>byte</c>s from <paramref name="input"/> to encode.</param>
/// <returns>
/// The number of characters written to <paramref name="output"/>, less any padding characters.
/// </returns>
public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}

if (outputOffset < 0)
{
throw new ArgumentOutOfRangeException(nameof(outputOffset));
}

var arraySizeRequired = GetArraySizeRequiredToEncode(count);
if (output.Length - outputOffset < arraySizeRequired)
{
throw new ArgumentException("invalid", nameof(count));
}

#if NETCOREAPP
return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset));
#else
// Special-case empty input.
if (count == 0)
{
return 0;
}

// Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.

// Start with default Base64 encoding.
var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset);

// Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
for (var i = outputOffset; i - outputOffset < numBase64Chars; i++)
{
var ch = output[i];
if (ch == '+')
{
output[i] = '-';
}
else if (ch == '/')
{
output[i] = '_';
}
else if (ch == '=')
{
// We've reached a padding character; truncate the remainder.
return i - outputOffset;
}
}

return numBase64Chars;
#endif
}

/// <summary>
/// Get the minimum output <c>char[]</c> size required for encoding <paramref name="count"/>
/// <see cref="byte"/>s with the <see cref="Base64UrlEncode(byte[], int, char[], int, int)"/> method.
/// </summary>
/// <param name="count">The number of characters to encode.</param>
/// <returns>
/// The minimum output <c>char[]</c> size required for encoding <paramref name="count"/> <see cref="byte"/>s.
/// </returns>
public static int GetArraySizeRequiredToEncode(int count)
{
var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
return checked(numWholeOrPartialInputBlocks * 4);
}

#if NETCOREAPP
/// <summary>
/// Encodes <paramref name="input"/> using base64url encoding.
/// </summary>
/// <param name="input">The binary input to encode.</param>
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
public static string Base64UrlEncode(ReadOnlySpan<byte> input)
{
if (input.IsEmpty)
{
return string.Empty;
}

int bufferSize = GetArraySizeRequiredToEncode(input.Length);

char[]? bufferToReturnToPool = null;
Span<char> buffer = bufferSize <= 128
? stackalloc char[bufferSize]
: bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize);

var numBase64Chars = Base64UrlEncode(input, buffer);
var base64Url = new string(buffer.Slice(0, numBase64Chars));

if (bufferToReturnToPool != null)
{
ArrayPool<char>.Shared.Return(bufferToReturnToPool);
}

return base64Url;
}

private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output)
{
Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length));

if (input.IsEmpty)
{
return 0;
}

// Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.

Convert.TryToBase64Chars(input, output, out int charsWritten);

// Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
for (var i = 0; i < charsWritten; i++)
{
var ch = output[i];
if (ch == '+')
{
output[i] = '-';
}
else if (ch == '/')
{
output[i] = '_';
}
else if (ch == '=')
{
// We've reached a padding character; truncate the remainder.
return i;
}
}

return charsWritten;
}
#endif

private static int GetNumBase64PaddingCharsInString(string str)
{
// Assumption: input contains a well-formed base64 string with no whitespace.

// base64 guaranteed have 0 - 2 padding characters.
if (str[str.Length - 1] == '=')
{
if (str[str.Length - 2] == '=')
{
return 2;
}
return 1;
}
return 0;
}

private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
{
switch (inputLength % 4)
{
case 0:
return 0;
case 2:
return 2;
case 3:
return 1;
default:
throw new FormatException("invalid length");
}
}
}
2 changes: 1 addition & 1 deletion Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal ArrayBufferConstructor(
_prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
}

private ArrayBufferPrototype PrototypeObject { get; }
internal ArrayBufferPrototype PrototypeObject { get; }

protected override void Initialize()
{
Expand Down
12 changes: 2 additions & 10 deletions Jint/Native/Global/GlobalObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,25 +283,17 @@ private static JsValue IsFinite(JsValue thisObject, JsValue[] arguments)
private static bool IsValidHexaChar(char c) => Uri.IsHexDigit(c);

/// <summary>
/// http://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.2
/// https://tc39.es/ecma262/#sec-encodeuri-uri
/// </summary>
/// <param name="thisObject"></param>
/// <param name="arguments"></param>
/// <returns></returns>
private JsValue EncodeUri(JsValue thisObject, JsValue[] arguments)
{
var uriString = TypeConverter.ToString(arguments.At(0));

return Encode(uriString, UnescapedUriSet);
}


/// <summary>
/// http://www.ecma-international.org/ecma-262/5.1/#sec-15.1.3.4
/// https://tc39.es/ecma262/#sec-encodeuricomponent-uricomponent
/// </summary>
/// <param name="thisObject"></param>
/// <param name="arguments"></param>
/// <returns></returns>
private JsValue EncodeUriComponent(JsValue thisObject, JsValue[] arguments)
{
var uriString = TypeConverter.ToString(arguments.At(0));
Expand Down
Loading