Просмотр исходного кода

Implement Uint8Array to/from base64 (#1911)

Marko Lahma 1 год назад
Родитель
Сommit
94bd26f530

+ 1 - 1
Directory.Packages.props

@@ -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" />

+ 0 - 1
Jint.Tests.Test262/Test262Harness.settings.json

@@ -18,7 +18,6 @@
     "regexp-v-flag",
     "source-phase-imports",
     "tail-call-optimization",
-    "uint8array-base64",
     "Temporal",
     "u180e"
   ],

+ 338 - 0
Jint/Extensions/WebEncoders.cs

@@ -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");
+        }
+    }
+}

+ 1 - 1
Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs

@@ -28,7 +28,7 @@ public sealed class ArrayBufferConstructor : Constructor
         _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
     }
 
-    private ArrayBufferPrototype PrototypeObject { get; }
+    internal ArrayBufferPrototype PrototypeObject { get; }
 
     protected override void Initialize()
     {

+ 2 - 10
Jint/Native/Global/GlobalObject.cs

@@ -283,25 +283,17 @@ namespace Jint.Native.Global
         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));

+ 1 - 0
Jint/Native/JsArrayBuffer.cs

@@ -28,6 +28,7 @@ public class JsArrayBuffer : ObjectInstance
             ExceptionHelper.ThrowRangeError(engine.Realm, "arrayBufferMaxByteLength cannot be larger than int32.MaxValue");
         }
 
+        _prototype = engine.Intrinsics.ArrayBuffer.PrototypeObject;
         _arrayBufferData = data;
         _arrayBufferMaxByteLength = (int?) arrayBufferMaxByteLength;
     }

+ 14 - 4
Jint/Native/JsString.cs

@@ -128,15 +128,25 @@ public class JsString : JsValue, IEquatable<JsString>, IEquatable<string>
             return a.Equals(b);
         }
 
-        if (a is null)
+        return b is null;
+    }
+
+    public static bool operator !=(JsString a, JsValue b)
+    {
+        return !(a == b);
+    }
+
+    public static bool operator ==(JsString? a, string? b)
+    {
+        if (a is not null)
         {
-            return b is null;
+            return a.Equals(b);
         }
 
-        return b is not null && a.Equals(b);
+        return b is null;
     }
 
-    public static bool operator !=(JsString a, JsValue b)
+    public static bool operator !=(JsString? a, string? b)
     {
         return !(a == b);
     }

+ 154 - 173
Jint/Native/TypedArray/TypedArrayConstructor.Types.cs

@@ -1,225 +1,206 @@
 using Jint.Runtime;
 
-namespace Jint.Native.TypedArray
+namespace Jint.Native.TypedArray;
+
+public sealed class Int8ArrayConstructor : TypedArrayConstructor
 {
-    public sealed class Int8ArrayConstructor : TypedArrayConstructor
+    internal Int8ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int8)
     {
-        internal Int8ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int8)
-        {
-        }
-
-        public JsTypedArray Construct(sbyte[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
     }
 
-    public sealed class Uint8ArrayConstructor : TypedArrayConstructor
+    public JsTypedArray Construct(ReadOnlySpan<sbyte> values)
     {
-        internal Uint8ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint8)
-        {
-        }
-
-        public JsTypedArray Construct(byte[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Uint8ClampedArrayConstructor : TypedArrayConstructor
+public sealed class Uint8ClampedArrayConstructor : TypedArrayConstructor
+{
+    internal Uint8ClampedArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint8C)
     {
-        internal Uint8ClampedArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint8C)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(byte[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<byte> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Int16ArrayConstructor : TypedArrayConstructor
+public sealed class Int16ArrayConstructor : TypedArrayConstructor
+{
+    internal Int16ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int16)
     {
-        internal Int16ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int16)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(short[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<short> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Uint16ArrayConstructor : TypedArrayConstructor
+public sealed class Uint16ArrayConstructor : TypedArrayConstructor
+{
+    internal Uint16ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint16)
     {
-        internal Uint16ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint16)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(ushort[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<ushort> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Int32ArrayConstructor : TypedArrayConstructor
+public sealed class Int32ArrayConstructor : TypedArrayConstructor
+{
+    internal Int32ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int32)
     {
-        internal Int32ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Int32)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(int[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<int> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Uint32ArrayConstructor : TypedArrayConstructor
+public sealed class Uint32ArrayConstructor : TypedArrayConstructor
+{
+    internal Uint32ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint32)
     {
-        internal Uint32ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint32)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(uint[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<uint> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Float16ArrayConstructor : TypedArrayConstructor
+public sealed class Float16ArrayConstructor : TypedArrayConstructor
+{
+    internal Float16ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float16)
     {
-        internal Float16ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float16)
-        {
-        }
+    }
 
 #if SUPPORTS_HALF
-        public JsTypedArray Construct(Half[] values)
+    public JsTypedArray Construct(ReadOnlySpan<Half> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        for (var i = 0; i < values.Length; ++i)
         {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            for (var i = 0; i < values.Length; ++i)
-            {
-                array.DoIntegerIndexedElementSet(i, values[i]);
-            }
-            return array;
+            array.DoIntegerIndexedElementSet(i, values[i]);
         }
-#endif
+        return array;
     }
+#endif
+}
 
-    public sealed class Float32ArrayConstructor : TypedArrayConstructor
+public sealed class Float32ArrayConstructor : TypedArrayConstructor
+{
+    internal Float32ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float32)
     {
-        internal Float32ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float32)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(float[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<float> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class Float64ArrayConstructor : TypedArrayConstructor
+public sealed class Float64ArrayConstructor : TypedArrayConstructor
+{
+    internal Float64ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float64)
     {
-        internal Float64ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Float64)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(double[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<double> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class BigInt64ArrayConstructor : TypedArrayConstructor
+public sealed class BigInt64ArrayConstructor : TypedArrayConstructor
+{
+    internal BigInt64ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.BigInt64)
     {
-        internal BigInt64ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.BigInt64)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(long[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<long> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
+}
 
-    public sealed class BigUint64ArrayConstructor : TypedArrayConstructor
+public sealed class BigUint64ArrayConstructor : TypedArrayConstructor
+{
+    internal BigUint64ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.BigUint64)
     {
-        internal BigUint64ArrayConstructor(
-            Engine engine,
-            Realm realm,
-            IntrinsicTypedArrayConstructor functionPrototype,
-            IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.BigUint64)
-        {
-        }
+    }
 
-        public JsTypedArray Construct(ulong[] values)
-        {
-            var array = (JsTypedArray) base.Construct([values.Length], this);
-            FillTypedArrayInstance(array, values);
-            return array;
-        }
+    public JsTypedArray Construct(ReadOnlySpan<ulong> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
     }
 }

+ 360 - 0
Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs

@@ -0,0 +1,360 @@
+using System.Buffers;
+using System.Globalization;
+using Jint.Collections;
+using Jint.Extensions;
+using Jint.Native.Object;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
+
+namespace Jint.Native.TypedArray;
+
+public sealed class Uint8ArrayConstructor : TypedArrayConstructor
+{
+    internal Uint8ArrayConstructor(
+        Engine engine,
+        Realm realm,
+        IntrinsicTypedArrayConstructor functionPrototype,
+        IntrinsicTypedArrayPrototype objectPrototype) : base(engine, realm, functionPrototype, objectPrototype, TypedArrayElementType.Uint8)
+    {
+    }
+
+    protected override void Initialize()
+    {
+        const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+        var properties = new PropertyDictionary(1, checkExistingKeys: false) { ["BYTES_PER_ELEMENT"] = new(new PropertyDescriptor(JsNumber.PositiveOne, PropertyFlag.AllForbidden)), ["fromBase64"] = new(new ClrFunction(Engine, "fromBase64", FromBase64, 1, PropertyFlag.Configurable), PropertyFlags), ["fromHex"] = new(new ClrFunction(Engine, "fromHex", FromHex, 1, PropertyFlag.Configurable), PropertyFlags), };
+        SetProperties(properties);
+    }
+
+    public JsTypedArray Construct(ReadOnlySpan<byte> values)
+    {
+        var array = (JsTypedArray) base.Construct([values.Length], this);
+        FillTypedArrayInstance(array, values);
+        return array;
+    }
+
+    private JsTypedArray FromBase64(JsValue thisObject, JsValue[] arguments)
+    {
+        var s = arguments.At(0);
+
+        if (!s.IsString())
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "fromBase64 must be called with a string");
+        }
+
+        var opts = GetOptionsObject(_engine, arguments.At(1));
+        var alphabet = GetAndValidateAlphabetOption(_engine, opts);
+        var lastChunkHandling = GetAndValidateLastChunkHandling(_engine, opts);
+
+        var result = FromBase64(_engine, s.ToString(), alphabet.ToString(), lastChunkHandling.ToString());
+        if (result.Error is not null)
+        {
+            throw result.Error;
+        }
+
+        var ta = _realm.Intrinsics.Uint8Array.Construct(new JsArrayBuffer(_engine, result.Bytes));
+        return ta;
+    }
+
+    internal static JsString GetAndValidateLastChunkHandling(Engine engine, ObjectInstance opts)
+    {
+        var lastChunkHandling = opts.Get("lastChunkHandling");
+        if (lastChunkHandling.IsUndefined())
+        {
+            lastChunkHandling = "loose";
+        }
+
+        if (lastChunkHandling is not JsString s || (s != "loose" && s != "strict" && s != "stop-before-partial"))
+        {
+            ExceptionHelper.ThrowTypeError(engine.Realm, "lastChunkHandling must be either 'loose', 'strict' or 'stop-before-partial'");
+            return default;
+        }
+
+        return s;
+    }
+
+    internal static JsString GetAndValidateAlphabetOption(Engine engine, ObjectInstance opts)
+    {
+        var alphabet = opts.Get("alphabet");
+        if (alphabet.IsUndefined())
+        {
+            alphabet = "base64";
+        }
+
+        if (alphabet is not JsString s || (s != "base64" && s != "base64url"))
+        {
+            ExceptionHelper.ThrowTypeError(engine.Realm, "alphabet must be either 'base64' or 'base64url'");
+            return default;
+        }
+
+        return s;
+    }
+
+    internal readonly record struct FromEncodingResult(byte[] Bytes, JavaScriptException? Error, int Read);
+
+    private static readonly SearchValues<char> Base64Alphabet = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
+
+    internal static FromEncodingResult FromBase64(Engine engine, string input, string alphabet, string lastChunkHandling, uint maxLength = uint.MaxValue)
+    {
+        if (maxLength == 0)
+        {
+            return new FromEncodingResult([], Error: null, 0);
+        }
+
+        var read = 0;
+        var bytes = new List<byte>();
+        var chunk = new char[4];
+        var chunkLength = 0;
+        var index = 0;
+        var length = input.Length;
+
+        var stopBeforePartial = string.Equals(lastChunkHandling, "stop-before-partial", StringComparison.Ordinal);
+        var loose = string.Equals(lastChunkHandling, "loose", StringComparison.Ordinal);
+        var base64Url = string.Equals(alphabet, "base64url", StringComparison.Ordinal);
+        var throwOnExtraBits = string.Equals(lastChunkHandling, "strict", StringComparison.Ordinal);
+
+        while (true)
+        {
+            index = SkipAsciiWhitespace(input, index);
+            if (index == length)
+            {
+                if (chunkLength > 0)
+                {
+                    if (stopBeforePartial)
+                    {
+                        return new FromEncodingResult(bytes.ToArray(), Error: null, read);
+                    }
+
+                    if (loose)
+                    {
+                        if (chunkLength == 1)
+                        {
+                            return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 chunk length."), read);
+                        }
+
+                        DecodeBase64Chunk(engine, bytes, chunk, chunkLength, throwOnExtraBits: false);
+                    }
+                    else // strict
+                    {
+                        return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 chunk length in strict mode."), read);
+                    }
+                }
+
+                return new FromEncodingResult(bytes.ToArray(), Error: null, length);
+            }
+
+            char currentChar = input[index];
+            index++;
+
+            if (currentChar == '=')
+            {
+                if (chunkLength < 2)
+                {
+                    return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid '=' placement in base64 string."), read);
+                }
+
+                index = SkipAsciiWhitespace(input, index);
+                if (chunkLength == 2)
+                {
+                    if (index == length)
+                    {
+                        if (stopBeforePartial)
+                        {
+                            return new FromEncodingResult(bytes.ToArray(), Error: null, read);
+                        }
+
+                        return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 string termination."), read);
+                    }
+
+                    currentChar = input[index];
+                    if (currentChar != '=')
+                    {
+                        return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Expected '=' in base64 string."), read);
+                    }
+
+                    index = SkipAsciiWhitespace(input, index + 1);
+                }
+
+                if (index < length)
+                {
+                    return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Extra characters after base64 string."), read);
+                }
+
+                DecodeBase64Chunk(engine, bytes, chunk, chunkLength, throwOnExtraBits);
+                return new FromEncodingResult(bytes.ToArray(), Error: null, length);
+            }
+
+            if (base64Url)
+            {
+                if (currentChar is '+' or '/')
+                {
+                    return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid character in base64url string."), read);
+                }
+
+                if (currentChar == '-')
+                {
+                    currentChar = '+';
+                }
+
+                if (currentChar == '_')
+                {
+                    currentChar = '/';
+                }
+            }
+
+            if (!Base64Alphabet.Contains(currentChar))
+            {
+                return new FromEncodingResult([], ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 character."), read);
+            }
+
+            ulong remaining = maxLength - (ulong) bytes.Count;
+            if ((remaining == 1 && chunkLength == 2) || (remaining == 2 && chunkLength == 3))
+            {
+                return new FromEncodingResult(bytes.ToArray(), Error: null, read);
+            }
+
+            chunk[chunkLength] = currentChar;
+            chunkLength++;
+
+            if (chunkLength == 4)
+            {
+                DecodeBase64Chunk(engine, bytes, chunk, chunkLength, throwOnExtraBits: false);
+                chunkLength = 0;
+                read = index;
+                if (bytes.Count == maxLength)
+                {
+                    return new FromEncodingResult(bytes.ToArray(), Error: null, read);
+                }
+            }
+        }
+    }
+
+    private static int SkipAsciiWhitespace(string input, int index)
+    {
+        while (index < input.Length)
+        {
+            var c = input[index];
+            if (c != 0x0009 && c != 0x000A && c != 0x000C && c != 0x000D && c != 0x0020)
+            {
+                return index;
+            }
+
+            index++;
+        }
+
+        return index;
+    }
+
+    private static void DecodeBase64Chunk(
+        Engine engine,
+        List<byte> into,
+        char[] chunk,
+        int chunkLength,
+        bool throwOnExtraBits = false)
+    {
+        if (chunkLength == 2)
+        {
+            chunk[2] = 'A';
+            chunk[3] = 'A';
+        }
+        else if (chunkLength == 3)
+        {
+            chunk[3] = 'A';
+        }
+
+        var bytes = WebEncoders.Base64UrlDecode(chunk);
+
+        if (chunkLength == 2)
+        {
+            if (throwOnExtraBits && bytes[1] != 0)
+            {
+                ExceptionHelper.ThrowSyntaxError(engine.Realm, "Invalid padding in base64 chunk.");
+            }
+            into.Add(bytes[0]);
+            return;
+        }
+
+        if (chunkLength == 3)
+        {
+            if (throwOnExtraBits && bytes[2] != 0)
+            {
+                ExceptionHelper.ThrowSyntaxError(engine.Realm, "Invalid padding in base64 chunk.");
+            }
+            into.Add(bytes[0]);
+            into.Add(bytes[1]);
+            return;
+        }
+
+        into.AddRange(bytes);
+    }
+
+    private JsTypedArray FromHex(JsValue thisObject, JsValue[] arguments)
+    {
+        var s = arguments.At(0);
+
+        if (!s.IsString())
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "fromHex must be called with a string");
+        }
+
+        var result = FromHex(_engine, s.ToString());
+        if (result.Error is not null)
+        {
+            throw result.Error;
+        }
+
+        var ta = _realm.Intrinsics.Uint8Array.Construct(new JsArrayBuffer(_engine, result.Bytes));
+        ta._viewedArrayBuffer._arrayBufferData = result.Bytes;
+        return ta;
+    }
+
+    private static readonly SearchValues<char> HexAlphabet = SearchValues.Create("0123456789abcdefABCDEF");
+
+    internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLength = int.MaxValue)
+    {
+        var length = s.Length;
+        var bytes = new byte[System.Math.Min(maxLength, length / 2)];
+        var read = 0;
+
+        if (length % 2 != 0)
+        {
+            return new FromEncodingResult(bytes, ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid hex string"), read);
+        }
+
+        var byteIndex = 0;
+        while (read < length && byteIndex < maxLength)
+        {
+            var hexits = s.AsSpan(read, 2);
+            if (!HexAlphabet.Contains(hexits[0]) || !HexAlphabet.Contains(hexits[1]))
+            {
+                return new FromEncodingResult(bytes, ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid hex value"), read);
+            }
+
+#if SUPPORTS_SPAN_PARSE
+            var b = byte.Parse(hexits, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+#else
+            var b = byte.Parse(hexits.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+#endif
+            bytes[byteIndex++] = b;
+            read += 2;
+        }
+
+        return new FromEncodingResult(bytes, Error: null, read);
+    }
+
+    internal static ObjectInstance GetOptionsObject(Engine engine, JsValue options)
+    {
+        if (options.IsUndefined())
+        {
+            return new JsObject(engine);
+        }
+
+        if (options.IsObject())
+        {
+            return options.AsObject();
+        }
+
+        ExceptionHelper.ThrowTypeError(engine.Realm, "Invalid options");
+        return default;
+    }
+}

+ 10 - 8
Jint/Native/TypedArray/TypedArrayConstructor.cs

@@ -24,14 +24,17 @@ namespace Jint.Native.TypedArray
             TypedArrayElementType type) : base(engine, realm, new JsString(type.GetTypedArrayName()))
         {
             _arrayElementType = type;
-
             _prototype = functionPrototype;
-            PrototypeObject = new TypedArrayPrototype(engine, objectPrototype, this, type);
+
+            PrototypeObject = type == TypedArrayElementType.Uint8
+                ? new Uint8ArrayPrototype(engine, objectPrototype, this)
+                : new TypedArrayPrototype(engine, objectPrototype, this, type);
+
             _length = new PropertyDescriptor(JsNumber.PositiveThree, PropertyFlag.Configurable);
             _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
         }
 
-        private TypedArrayPrototype PrototypeObject { get; }
+        private Prototype PrototypeObject { get; }
 
         protected override void Initialize()
         {
@@ -56,7 +59,6 @@ namespace Jint.Native.TypedArray
                 ExceptionHelper.ThrowTypeError(_realm);
             }
 
-
             var numberOfArgs = arguments.Length;
             if (numberOfArgs == 0)
             {
@@ -263,7 +265,7 @@ namespace Jint.Native.TypedArray
         /// <summary>
         /// https://tc39.es/ecma262/#sec-allocatetypedarray
         /// </summary>
-        private JsTypedArray AllocateTypedArray(JsValue newTarget, uint length = 0)
+        internal JsTypedArray AllocateTypedArray(JsValue newTarget, uint length = 0)
         {
             Func<Intrinsics, ObjectInstance> defaultProto = _arrayElementType switch
             {
@@ -296,7 +298,7 @@ namespace Jint.Native.TypedArray
             return obj;
         }
 
-        internal static void FillTypedArrayInstance<T>(JsTypedArray target, T[] values)
+        internal static void FillTypedArrayInstance<T>(JsTypedArray target, ReadOnlySpan<T>values)
         {
             for (var i = 0; i < values.Length; ++i)
             {
@@ -304,7 +306,7 @@ namespace Jint.Native.TypedArray
             }
         }
 
-        internal static void FillTypedArrayInstance(JsTypedArray target, ulong[] values)
+        internal static void FillTypedArrayInstance(JsTypedArray target, ReadOnlySpan<ulong> values)
         {
             for (var i = 0; i < values.Length; ++i)
             {
@@ -312,7 +314,7 @@ namespace Jint.Native.TypedArray
             }
         }
 
-        internal static void FillTypedArrayInstance(JsTypedArray target, long[] values)
+        internal static void FillTypedArrayInstance(JsTypedArray target, ReadOnlySpan<long> values)
         {
             for (var i = 0; i < values.Length; ++i)
             {

+ 1 - 1
Jint/Native/TypedArray/TypedArrayPrototype.cs

@@ -24,7 +24,7 @@ internal sealed class TypedArrayPrototype : Prototype
 
     protected override void Initialize()
     {
-        var properties = new PropertyDictionary(2, false)
+        var properties = new PropertyDictionary(2, checkExistingKeys: false)
         {
             ["BYTES_PER_ELEMENT"] = new(JsNumber.Create(_arrayElementType.GetElementSize()), PropertyFlag.AllForbidden),
             ["constructor"] = new(_constructor, PropertyFlag.NonEnumerable),

+ 188 - 0
Jint/Native/TypedArray/Uint8ArrayPrototype.cs

@@ -0,0 +1,188 @@
+using System.Text;
+using Jint.Collections;
+using Jint.Extensions;
+using Jint.Native.ArrayBuffer;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
+
+namespace Jint.Native.TypedArray;
+
+internal sealed class Uint8ArrayPrototype : Prototype
+{
+    private readonly TypedArrayConstructor _constructor;
+
+    public Uint8ArrayPrototype(
+        Engine engine,
+        IntrinsicTypedArrayPrototype objectPrototype,
+        TypedArrayConstructor constructor)
+        : base(engine, engine.Realm)
+    {
+        _prototype = objectPrototype;
+        _constructor = constructor;
+
+    }
+
+    protected override void Initialize()
+    {
+        const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+        var properties = new PropertyDictionary(6, checkExistingKeys: false)
+        {
+            ["BYTES_PER_ELEMENT"] = new(JsNumber.PositiveOne, PropertyFlag.AllForbidden),
+            ["constructor"] = new(_constructor, PropertyFlag.NonEnumerable),
+            ["setFromBase64"] = new(new ClrFunction(Engine, "setFromBase64", SetFromBase64, 1, PropertyFlag.Configurable), PropertyFlags),
+            ["setFromHex"] = new(new ClrFunction(Engine, "setFromHex", SetFromHex, 1, PropertyFlag.Configurable), PropertyFlags),
+            ["toBase64"] = new(new ClrFunction(Engine, "toBase64", ToBase64, 0, PropertyFlag.Configurable), PropertyFlags),
+            ["toHex"] = new(new ClrFunction(Engine, "toHex", ToHex, 0, PropertyFlag.Configurable), PropertyFlags),
+        };
+        SetProperties(properties);
+    }
+
+    private JsObject SetFromBase64(JsValue thisObject, JsValue[] arguments)
+    {
+        var into = ValidateUint8Array(thisObject);
+        var s = arguments.At(0);
+
+        if (!s.IsString())
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "setFromBase64 must be called with a string");
+        }
+
+        var opts = Uint8ArrayConstructor.GetOptionsObject(_engine, arguments.At(1));
+        var alphabet = Uint8ArrayConstructor.GetAndValidateAlphabetOption(_engine, opts);
+        var lastChunkHandling = Uint8ArrayConstructor.GetAndValidateLastChunkHandling(_engine, opts);
+
+        var taRecord = IntrinsicTypedArrayPrototype.MakeTypedArrayWithBufferWitnessRecord(into, ArrayBufferOrder.SeqCst);
+        if (taRecord.IsTypedArrayOutOfBounds)
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "TypedArray is out of bounds");
+        }
+
+        var byteLength = taRecord.TypedArrayLength;
+        var result = Uint8ArrayConstructor.FromBase64(_engine, s.ToString(), alphabet.ToString(), lastChunkHandling.ToString(), byteLength);
+        if (result.Error is not null)
+        {
+            throw result.Error;
+        }
+
+        SetUint8ArrayBytes(into, result.Bytes);
+
+        var resultObject = OrdinaryObjectCreate(_engine, _engine.Intrinsics.Object);
+        resultObject.CreateDataPropertyOrThrow("read", result.Read);
+        resultObject.CreateDataPropertyOrThrow("written", result.Bytes.Length);
+        return resultObject;
+    }
+
+    private static void SetUint8ArrayBytes(JsTypedArray into, byte[] bytes)
+    {
+        var offset = into._byteOffset;
+        var len = bytes.Length;
+        var index = 0;
+        while (index < len)
+        {
+            var b = bytes[index];
+            var byteIndexInBuffer = index + offset;
+            into._viewedArrayBuffer.SetValueInBuffer(byteIndexInBuffer, TypedArrayElementType.Uint8, b, isTypedArray: true, ArrayBufferOrder.Unordered);
+            index++;
+        }
+    }
+
+    private JsObject SetFromHex(JsValue thisObject, JsValue[] arguments)
+    {
+        var into = ValidateUint8Array(thisObject);
+        var s = arguments.At(0);
+
+        if (!s.IsString())
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "setFromHex must be called with a string");
+        }
+
+        var taRecord = IntrinsicTypedArrayPrototype.MakeTypedArrayWithBufferWitnessRecord(into, ArrayBufferOrder.SeqCst);
+        if (taRecord.IsTypedArrayOutOfBounds)
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "TypedArray is out of bounds");
+        }
+
+        var byteLength = taRecord.TypedArrayLength;
+        var result = Uint8ArrayConstructor.FromHex(_engine, s.ToString(), byteLength);
+        if (result.Error is not null)
+        {
+            throw result.Error;
+        }
+
+        SetUint8ArrayBytes(into, result.Bytes);
+
+        var resultObject = OrdinaryObjectCreate(_engine, _engine.Intrinsics.Object);
+        resultObject.CreateDataPropertyOrThrow("read", result.Read);
+        resultObject.CreateDataPropertyOrThrow("written", result.Bytes.Length);
+        return resultObject;
+    }
+
+    private JsValue ToBase64(JsValue thisObject, JsValue[] arguments)
+    {
+       var o = ValidateUint8Array(thisObject);
+
+        var opts = Uint8ArrayConstructor.GetOptionsObject(_engine, arguments.At(0));
+        var alphabet = Uint8ArrayConstructor.GetAndValidateAlphabetOption(_engine, opts);
+
+        var omitPadding = TypeConverter.ToBoolean(opts.Get("omitPadding"));
+#if NETCOREAPP
+        var toEncode = GetUint8ArrayBytes(o);
+#else
+        var toEncode = GetUint8ArrayBytes(o).ToArray();
+#endif
+
+        string outAscii;
+        if (alphabet == "base64")
+        {
+            outAscii = Convert.ToBase64String(toEncode);
+        }
+        else
+        {
+            outAscii = WebEncoders.Base64UrlEncode(toEncode);
+        }
+
+        return outAscii;
+    }
+
+    private JsValue ToHex(JsValue thisObject, JsValue[] arguments)
+    {
+        var o = ValidateUint8Array(thisObject);
+        var toEncode = GetUint8ArrayBytes(o);
+
+        using var outString = new ValueStringBuilder();
+        foreach (var b in toEncode)
+        {
+            var b1 = (byte)(b >> 4);
+            outString.Append((char)(b1 > 9 ? b1 - 10 + 'a' : b1 + '0'));
+
+            var b2 = (byte)(b & 0x0F);
+            outString.Append((char)(b2 > 9 ? b2 - 10 + 'a' : b2 + '0'));
+        }
+
+        return outString.ToString();
+    }
+
+    private ReadOnlySpan<byte> GetUint8ArrayBytes(JsTypedArray ta)
+    {
+        var buffer = ta._viewedArrayBuffer;
+        var taRecord = IntrinsicTypedArrayPrototype.MakeTypedArrayWithBufferWitnessRecord(ta, ArrayBufferOrder.SeqCst);
+        if (taRecord.IsTypedArrayOutOfBounds)
+        {
+            ExceptionHelper.ThrowTypeError(_realm, "TypedArray is out of bounds");
+        }
+
+        return buffer._arrayBufferData!.AsSpan(0, (int) taRecord.TypedArrayLength);
+    }
+
+    private JsTypedArray ValidateUint8Array(JsValue ta)
+    {
+        if (ta is not JsTypedArray { _arrayElementType: TypedArrayElementType.Uint8 } typedArray)
+        {
+            ExceptionHelper.ThrowTypeError(_engine.Realm, "Not a Uint8Array");
+            return default;
+        }
+
+        return typedArray;
+    }
+}

+ 8 - 3
Jint/Runtime/ExceptionHelper.cs

@@ -18,19 +18,24 @@ namespace Jint.Runtime
         [DoesNotReturn]
         public static void ThrowSyntaxError(Realm realm, string? message = null)
         {
-            throw new JavaScriptException(realm.Intrinsics.SyntaxError, message);
+            throw CreateSyntaxError(realm, message);
         }
 
         [DoesNotReturn]
         public static void ThrowSyntaxError(Realm realm, string message, in SourceLocation location)
         {
-            throw new JavaScriptException(realm.Intrinsics.SyntaxError, message).SetJavaScriptLocation(location);
+            throw CreateSyntaxError(realm, message).SetJavaScriptLocation(location);
+        }
+
+        public static JavaScriptException CreateSyntaxError(Realm realm, string? message)
+        {
+            return new JavaScriptException(realm.Intrinsics.SyntaxError, message);
         }
 
         [DoesNotReturn]
         public static void ThrowArgumentException(string? message = null)
         {
-            ThrowArgumentException(message, null);
+            ThrowArgumentException(message, paramName: null);
         }
 
         [DoesNotReturn]

+ 1 - 0
README.md

@@ -130,6 +130,7 @@ and many more.
 - ✔ Resizable and growable ArrayBuffers
 - ✔ Set methods (`intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`)
 - ✔ ShadowRealm
+- ✔ Uint8Array to/from base64
 
 #### Other