From 763212a9e12d7257a4d95c4b325e8573a70e4f6c Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 24 Apr 2024 09:13:52 -0700 Subject: [PATCH 01/38] Base64Url encoding, validation impelementation --- .../System.Memory/ref/System.Memory.cs | 18 + .../tests/Base64/Base64ValidationUnitTests.cs | 2 +- .../tests/Base64Url/Base64UrlEncoderTests.cs | 302 ++++++++++++++++ .../Base64Url/Base64UrlValidationUnitTests.cs | 339 ++++++++++++++++++ .../tests/System.Memory.Tests.csproj | 2 + .../System.Private.CoreLib.Shared.projitems | 3 + .../src/System/Buffers/Text/Base64.cs | 18 +- .../src/System/Buffers/Text/Base64Decoder.cs | 2 +- .../src/System/Buffers/Text/Base64Encoder.cs | 177 ++++----- .../Text/Base64Url/Base64UrlDecoder.cs | 42 +++ .../Text/Base64Url/Base64UrlEncoder.cs | 326 +++++++++++++++++ .../Text/Base64Url/Base64UrlValidator.cs | 83 +++++ .../System/Buffers/Text/Base64Validator.cs | 30 +- 13 files changed, 1252 insertions(+), 92 deletions(-) create mode 100644 src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs create mode 100644 src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 8aac4764a1498a..8f6a5ac768e808 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -639,6 +639,24 @@ public static void WriteUIntPtrLittleEndian(System.Span destination, nuint } namespace System.Buffers.Text { + public static class Base64Url + { + public static OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } + public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } + public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } + public static string EncodeToString(System.ReadOnlySpan source) { throw null; } + public static byte[] EncodeToUtf8(System.ReadOnlySpan source) { throw null; } + public static int EncodeToUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus EncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static int GetEncodedLength(int bytesLength) { throw null; } + public static bool IsValid(System.ReadOnlySpan base64UrlText) { throw null; } + public static bool IsValid(System.ReadOnlySpan base64UrlText, out int decodedLength) { throw null; } + public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText) { throw null; } + public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText, out int decodedLength) { throw null; } + public static bool TryEncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } + public static bool TryEncodeToUtf8InPlace(System.Span buffer, int dataLength, out int bytesWritten) { throw null; } + } public static partial class Utf8Formatter { public static bool TryFormat(bool value, System.Span destination, out int bytesWritten, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } diff --git a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs index c7f164ad9b7f5f..ba08c79e53f226 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs @@ -267,7 +267,7 @@ public void InvalidSizeBytes(string utf8WithByteToBeIgnored) [InlineData("Y")] public void InvalidSizeChars(string utf8WithByteToBeIgnored) { - byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored; Assert.False(Base64.IsValid(utf8BytesWithByteToBeIgnored)); Assert.False(Base64.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs new file mode 100644 index 00000000000000..d3403d3ddac51b --- /dev/null +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs @@ -0,0 +1,302 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.SpanTests; +using System.Text; +using Xunit; + +namespace System.Buffers.Text.Tests +{ + public class Base64UrlEncoderTests + { + [Fact] + public void BasicEncodingAndDecoding() + { + var bytes = new byte[byte.MaxValue + 1]; + for (int i = 0; i < byte.MaxValue + 1; i++) + { + bytes[i] = (byte)i; + } + + for (int value = 0; value < 256; value++) + { + Span sourceBytes = bytes.AsSpan(0, value + 1); + Span encodedBytes = new byte[Base64Url.GetEncodedLength(sourceBytes.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(sourceBytes, encodedBytes, out int consumed, out int encodedBytesCount)); + Assert.Equal(sourceBytes.Length, consumed); + Assert.Equal(encodedBytes.Length, encodedBytesCount); + + string encodedText = Encoding.ASCII.GetString(encodedBytes.ToArray()); + string expectedText = Convert.ToBase64String(bytes, 0, value + 1).Replace('+', '-').Replace('/', '_').TrimEnd('='); + Assert.Equal(expectedText, encodedText); + + /*Assert.Equal(0, encodedBytes.Length % 4); + Span decodedBytes = new byte[Base64.GetMaxDecodedFromUtf8Length(encodedBytes.Length)]; + Assert.Equal(OperationStatus.Done, Base64.DecodeFromUtf8(encodedBytes, decodedBytes, out consumed, out int decodedByteCount)); + Assert.Equal(encodedBytes.Length, consumed); + Assert.Equal(sourceBytes.Length, decodedByteCount); + Assert.True(sourceBytes.SequenceEqual(decodedBytes.Slice(0, decodedByteCount)));*/ + } + } + + [Fact] + public void BasicEncoding() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes = rnd.Next(100, 1000 * 1000); + Span source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + + Span encodedBytes = new byte[Base64Url.GetEncodedLength(source.Length)]; + OperationStatus result = Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount); + Assert.Equal(OperationStatus.Done, result); + Assert.Equal(source.Length, consumed); + Assert.Equal(encodedBytes.Length, encodedBytesCount); + Assert.True(VerifyEncodingCorrectness(source.Length, encodedBytes.Length, source, encodedBytes)); + } + } + + private static bool VerifyEncodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span encodedBytes) + { + string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed).ToArray()).Replace('+', '-').Replace('/', '_').TrimEnd('='); + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + return expectedText.Equals(encodedText); + } + + [Fact] + public void BasicEncodingWithFinalBlockFalse() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes = rnd.Next(100, 1000 * 1000); + Span source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + + Span encodedBytes = new byte[Base64Url.GetEncodedLength(source.Length)]; + int expectedConsumed = source.Length / 3 * 3; // only consume closest multiple of three since isFinalBlock is false + int expectedWritten = source.Length / 3 * 4; + + // The constant random seed guarantees that both states are tested. + OperationStatus expectedStatus = numBytes % 3 == 0 ? OperationStatus.Done : OperationStatus.NeedMoreData; + Assert.Equal(expectedStatus, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, encodedBytesCount); + Assert.True(VerifyEncodingCorrectness(expectedConsumed, expectedWritten, source, encodedBytes)); + } + } + + [Theory] + [InlineData(1, "AQ")] + [InlineData(2, "AQI")] + [InlineData(3, "AQID")] + [InlineData(4, "AQIDBA")] + [InlineData(5, "AQIDBAU")] + [InlineData(6, "AQIDBAUG")] + [InlineData(7, "AQIDBAUGBw")] + public void BasicEncodingWithFinalBlockTrueKnownInput(int numBytes, string expectedText) + { + int expectedConsumed = numBytes; + int expectedWritten = expectedText.Length; + + Span source = new byte[numBytes]; + for (int i = 0; i < numBytes; i++) + { + source[i] = (byte)(i + 1); + } + Span encodedBytes = new byte[Base64Url.GetEncodedLength(source.Length)]; + + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, encodedBytesCount); + + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + Assert.Equal(expectedText, encodedText); + } + + [Theory] + [InlineData(1, "", 0, 0)] + [InlineData(2, "", 0, 0)] + [InlineData(3, "AQID", 3, 4)] + [InlineData(4, "AQID", 3, 4)] + [InlineData(5, "AQID", 3, 4)] + [InlineData(6, "AQIDBAUG", 6, 8)] + [InlineData(7, "AQIDBAUG", 6, 8)] + public void BasicEncodingWithFinalBlockFalseKnownInput(int numBytes, string expectedText, int expectedConsumed, int expectedWritten) + { + Span source = new byte[numBytes]; + for (int i = 0; i < numBytes; i++) + { + source[i] = (byte)(i + 1); + } + Span encodedBytes = new byte[Base64Url.GetEncodedLength(source.Length)]; + + OperationStatus expectedStatus = numBytes % 3 == 0 ? OperationStatus.Done : OperationStatus.NeedMoreData; + Assert.Equal(expectedStatus, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, encodedBytesCount); + + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + Assert.Equal(expectedText, encodedText); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeEmptySpan(bool isFinalBlock) + { + Span source = Span.Empty; + Span encodedBytes = new byte[Base64Url.GetEncodedLength(source.Length)]; + + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount, isFinalBlock)); + Assert.Equal(0, consumed); + Assert.Equal(0, encodedBytesCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodingOutputTooSmall(bool isFinalBlock) + { + for (int numBytes = 4; numBytes < 20; numBytes++) + { + Span source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + + Span encodedBytes = new byte[4]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int written, isFinalBlock)); + int expectedConsumed = 3; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(encodedBytes.Length, written); + Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodingOutputTooSmallRetry(bool isFinalBlock) + { + Span source = new byte[750]; + Base64TestHelper.InitializeBytes(source); + + int outputSize = 320; + int requiredSize = Base64Url.GetEncodedLength(source.Length); + + Span encodedBytes = new byte[outputSize]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int written, isFinalBlock)); + int expectedConsumed = encodedBytes.Length / 4 * 3; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(encodedBytes.Length, written); + Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + + encodedBytes = new byte[requiredSize - outputSize]; + source = source.Slice(consumed); + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(source, encodedBytes, out consumed, out written, isFinalBlock)); + expectedConsumed = encodedBytes.Length / 4 * 3; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(encodedBytes.Length, written); + Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [OuterLoop] + public void EncodeTooLargeSpan(bool isFinalBlock) + { + if (!Environment.Is64BitProcess) + return; + + bool allocatedFirst = false; + bool allocatedSecond = false; + IntPtr memBlockFirst = IntPtr.Zero; + IntPtr memBlockSecond = IntPtr.Zero; + + // int.MaxValue - (int.MaxValue % 4) => 2147483644, largest multiple of 4 less than int.MaxValue + // CLR default limit of 2 gigabytes (GB). + // 1610612734, larger than MaximumEncodeLength, requires output buffer of size 2147483648 (which is > int.MaxValue) + const int sourceCount = (int.MaxValue >> 2) * 3 + 1; + const int encodedCount = 2000000000; + + try + { + allocatedFirst = AllocationHelper.TryAllocNative((IntPtr)sourceCount, out memBlockFirst); + allocatedSecond = AllocationHelper.TryAllocNative((IntPtr)encodedCount, out memBlockSecond); + if (allocatedFirst && allocatedSecond) + { + unsafe + { + var source = new Span(memBlockFirst.ToPointer(), sourceCount); + var encodedBytes = new Span(memBlockSecond.ToPointer(), encodedCount); + + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount, isFinalBlock)); + Assert.Equal((encodedBytes.Length >> 2) * 3, consumed); // encoding 1500000000 bytes fits into buffer of 2000000000 bytes + Assert.Equal(encodedBytes.Length, encodedBytesCount); + } + } + } + finally + { + if (allocatedFirst) + AllocationHelper.ReleaseNative(ref memBlockFirst); + if (allocatedSecond) + AllocationHelper.ReleaseNative(ref memBlockSecond); + } + } + + [Fact] + public void GetEncodedLength() + { + // (int.MaxValue - 4)/(4/3) => 1610612733, otherwise integer overflow + int[] input = { 0, 1, 2, 3, 4, 5, 6, 1610612728, 1610612729, 1610612730, 1610612731, 1610612732, 1610612733 }; + int[] expected = { 0, 2, 3, 4, 6, 7, 8, 2147483638, 2147483639, 2147483640, 2147483642, 2147483643, 2147483644 }; + for (int i = 0; i < input.Length; i++) + { + Assert.Equal(expected[i], Base64Url.GetEncodedLength(input[i])); + } + + // integer overflow + Assert.Throws(() => Base64Url.GetEncodedLength(1610612734)); + Assert.Throws(() => Base64Url.GetEncodedLength(int.MaxValue)); + + // negative input + Assert.Throws(() => Base64Url.GetEncodedLength(-1)); + Assert.Throws(() => Base64Url.GetEncodedLength(int.MinValue)); + } + + [Fact] + public void TryEncodeInPlace() + { + const int numberOfBytes = 15; + Span testBytes = new byte[numberOfBytes / 3 * 4]; // slack since encoding inflates the data + Base64TestHelper.InitializeBytes(testBytes); + + for (int numberOfBytesToTest = 0; numberOfBytesToTest <= numberOfBytes; numberOfBytesToTest++) + { + Span sliced = testBytes.Slice(0, numberOfBytesToTest); + Span dest = new byte[Base64Url.GetEncodedLength(numberOfBytesToTest)]; + //var expectedText = Convert.ToBase64String(testBytes.Slice(0, numberOfBytesToTest).ToArray()).Replace('+', '-').Replace('/', '_').TrimEnd('='); + var status = Base64Url.EncodeToUtf8(sliced, dest, out _, out int _); + Assert.Equal(OperationStatus.Done, status); + var expectedText = Encoding.ASCII.GetString(dest.ToArray()); + Assert.True(Base64Url.TryEncodeToUtf8InPlace(testBytes, numberOfBytesToTest, out int bytesWritten)); + Assert.Equal(Base64Url.GetEncodedLength(numberOfBytesToTest), bytesWritten); + + var encodedText = Encoding.ASCII.GetString(testBytes.Slice(0, bytesWritten).ToArray()); + Assert.Equal(expectedText, encodedText); + } + } + + [Fact] + public void TryEncodeInPlaceOutputTooSmall() + { + byte[] testBytes = { 1, 2, 3 }; + + Assert.False(Base64Url.TryEncodeToUtf8InPlace(testBytes, testBytes.Length, out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + } +} diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs new file mode 100644 index 00000000000000..384b9f60f9767c --- /dev/null +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -0,0 +1,339 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Xunit; + +namespace System.Buffers.Text.Tests +{ + public class Base64UrlValidationUnitTests : Base64TestBase + { + [Fact] + public void BasicValidationBytes() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 != 0); // ensure we have a valid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeDecodableBytes(source, numBytes); + + Assert.True(Base64Url.IsValid(source)); + Assert.True(Base64Url.IsValid(source, out int decodedLength)); + Assert.True(decodedLength > 0); + } + } + + [Fact] + public void BasicValidationChars() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 != 0); // ensure we have a valid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeDecodableBytes(source, numBytes); + Span chars = source + .ToArray() + .Select(Convert.ToChar) + .ToArray() + .AsSpan(); + + Assert.True(Base64Url.IsValid(chars)); + Assert.True(Base64Url.IsValid(chars, out int decodedLength)); + Assert.True(decodedLength > 0); + } + } + + [Fact] + public void BasicValidationInvalidInputLengthBytes() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 0); // ensure we have a invalid length + + Span source = new byte[numBytes]; + + Assert.False(Base64Url.IsValid(source)); + Assert.False(Base64Url.IsValid(source, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + } + + [Fact] + public void BasicValidationInvalidInputLengthChars() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 0); // ensure we have a invalid length + + Span source = new char[numBytes]; + + Assert.False(Base64Url.IsValid(source)); + Assert.False(Base64Url.IsValid(source, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + } + + [Fact] + public void ValidateEmptySpanBytes() + { + Span source = Span.Empty; + + Assert.True(Base64Url.IsValid(source)); + Assert.True(Base64Url.IsValid(source, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + + [Fact] + public void ValidateEmptySpanChars() + { + Span source = Span.Empty; + + Assert.True(Base64Url.IsValid(source)); + Assert.True(Base64Url.IsValid(source, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + + [Fact] + public void ValidateGuidBytes() + { + Span source = new byte[22]; + Span decodedBytes = Guid.NewGuid().ToByteArray(); + Base64Url.EncodeToUtf8(decodedBytes, source, out int _, out int _); + + Assert.True(Base64Url.IsValid(source)); + Assert.True(Base64Url.IsValid(source, out int decodedLength)); + Assert.True(decodedLength > 0); + } + + [Fact] + public void ValidateGuidChars() + { + Span source = new byte[22]; + Span decodedBytes = Guid.NewGuid().ToByteArray(); + Base64Url.EncodeToUtf8(decodedBytes, source, out int _, out int _); + Span chars = source + .ToArray() + .Select(Convert.ToChar) + .ToArray() + .AsSpan(); + + Assert.True(Base64Url.IsValid(chars)); + Assert.True(Base64Url.IsValid(chars, out int decodedLength)); + Assert.True(decodedLength > 0); + } + + [Theory] + [MemberData(nameof(ValidBase64Strings_WithCharsThatMustBeIgnored))] + public void ValidateBytesIgnoresCharsToBeIgnoredBytes(string utf8WithByteToBeIgnored, byte[] expectedBytes) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedBytes.Length, decodedLength); + } + + [Theory] + [MemberData(nameof(ValidBase64Strings_WithCharsThatMustBeIgnored))] + public void ValidateBytesIgnoresCharsToBeIgnoredChars(string utf8WithByteToBeIgnored, byte[] expectedBytes) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedBytes.Length, decodedLength); + } + + [Theory] + [MemberData(nameof(StringsOnlyWithCharsToBeIgnored))] + public void ValidateWithOnlyCharsToBeIgnoredBytes(string utf8WithByteToBeIgnored) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + + [Theory] + [MemberData(nameof(StringsOnlyWithCharsToBeIgnored))] + public void ValidateWithOnlyCharsToBeIgnoredChars(string utf8WithByteToBeIgnored) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + + [Theory] + [InlineData("YQ==", 1)] + [InlineData("YWI=", 2)] + [InlineData("YWJj", 3)] + [InlineData(" YWI=", 2)] + [InlineData("Y WI=", 2)] + [InlineData("YW I=", 2)] + [InlineData("YWI =", 2)] + [InlineData("YWI= ", 2)] + [InlineData(" YQ==", 1)] + [InlineData("Y Q==", 1)] + [InlineData("YQ ==", 1)] + [InlineData("YQ= =", 1)] + [InlineData("YQ== ", 1)] + public void ValidateWithPaddingReturnsCorrectCountBytes(string utf8WithByteToBeIgnored, int expectedLength) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedLength, decodedLength); + } + + [Theory] + [InlineData("YQ==", 1)] + [InlineData("YWI=", 2)] + [InlineData("YWJj", 3)] + [InlineData(" YWI=", 2)] + [InlineData("Y WI=", 2)] + [InlineData("YW I=", 2)] + [InlineData("YWI =", 2)] + [InlineData("YWI= ", 2)] + [InlineData(" YQ==", 1)] + [InlineData("Y Q==", 1)] + [InlineData("YQ ==", 1)] + [InlineData("YQ= =", 1)] + [InlineData("YQ== ", 1)] + public void ValidateWithPaddingReturnsCorrectCountChars(string utf8WithByteToBeIgnored, int expectedLength) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedLength, decodedLength); + } + + [Theory] + [InlineData("YQ==", 1)] + [InlineData("YWI=", 2)] + [InlineData("YWJj", 3)] + public void DecodeEmptySpan(string utf8WithByteToBeIgnored, int expectedLength) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); + + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedLength, decodedLength); + } + + [Theory] + [InlineData("YWJ", true, 2)] + //[InlineData("YW", true, 1)] + //[InlineData("Y", false, 0)] + public void SmallSizeBytes(string utf8Text, bool isValid, int expectedDecodedLength) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8Text); + + Assert.Equal(isValid, Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.Equal(isValid, Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedDecodedLength, decodedLength); + } + + [Theory] + [InlineData("YWJ", true, 2)] + //[InlineData("YW", true, 1)] + //[InlineData("Y", false, 0)] + public void InvalidSizeChars(string utf8Text, bool isValid, int expectedDecodedLength) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8Text; + + Assert.Equal(isValid, Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.Equal(isValid, Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedDecodedLength, decodedLength); + } + + [Theory] + [InlineData("YQ===")] + [InlineData("YQ=a=")] + [InlineData("YWI=a")] + [InlineData(" aYWI=a")] + [InlineData("a YWI=a")] + [InlineData("aY WI=a")] + [InlineData("aYW I=a")] + [InlineData("aYWI =a")] + [InlineData("aYWI= a")] + [InlineData("a YQ==a")] + [InlineData("aY Q==a")] + [InlineData("aYQ ==a")] + [InlineData("aYQ= =a")] + [InlineData("aYQ== a")] + [InlineData("aYQ==a ")] + public void InvalidBase64UrlBytes(string utf8WithByteToBeIgnored) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + + Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + + [Theory] + [InlineData("YQ===")] + [InlineData("YQ=a=")] + [InlineData("YWI=a")] + [InlineData("a YWI=a")] + [InlineData("aY WI=a")] + [InlineData("aYW I=a")] + [InlineData("aYWI =a")] + [InlineData("aYWI= a")] + [InlineData("a YQ==a")] + [InlineData("aY Q==a")] + [InlineData("aYQ ==a")] + [InlineData("aYQ= =a")] + [InlineData("aYQ== a")] + [InlineData("aYQ==a ")] + [InlineData("a")] + [InlineData(" a")] + [InlineData(" a")] + [InlineData(" a")] + [InlineData(" a")] + [InlineData("a ")] + [InlineData("a ")] + [InlineData("a ")] + [InlineData("a ")] + [InlineData(" a ")] + [InlineData(" a ")] + [InlineData(" a ")] + [InlineData(" a ")] + public void InvalidBase64UrlChars(string utf8WithByteToBeIgnored) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + + Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index 843d5e1b479ceb..1087e40071f094 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -13,6 +13,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 22a4ddea6ed606..333c4281f87810 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -131,6 +131,9 @@ + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index 4b1f597e5e8b5b..f19586eb120f75 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -3,13 +3,14 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace System.Buffers.Text { public static partial class Base64 { [Conditional("DEBUG")] - private static unsafe void AssertRead(byte* src, byte* srcStart, int srcLength) + internal static unsafe void AssertRead(byte* src, byte* srcStart, int srcLength) { int vectorElements = Unsafe.SizeOf(); byte* readEnd = src + vectorElements; @@ -23,7 +24,7 @@ private static unsafe void AssertRead(byte* src, byte* srcStart, int sr } [Conditional("DEBUG")] - private static unsafe void AssertWrite(byte* dest, byte* destStart, int destLength) + internal static unsafe void AssertWrite(byte* dest, byte* destStart, int destLength) { int vectorElements = Unsafe.SizeOf(); byte* writeEnd = dest + vectorElements; @@ -35,5 +36,18 @@ private static unsafe void AssertWrite(byte* dest, byte* destStart, int Debug.Fail($"Write for {typeof(TVector)} is not within safe bounds. destIndex: {destIndex}, destLength: {destLength}"); } } + + internal interface IBase64Encoder + { + static abstract int IncrementPadTwo { get; } + static abstract int IncrementPadOne { get; } + static abstract ReadOnlySpan EncodingMap { get; } + static abstract Vector256 Avx2Lut { get; } + static abstract Vector128 AdvSimdLut4 { get; } + static abstract Vector128 Ssse3AdvSimdLut { get; } + static abstract int GetMaxSrcLength(int srcLength, int destLength); + static abstract unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap); + static abstract unsafe uint EncodeTwoOptionallyPadOne(byte* oneByte, ref byte encodingMap); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 1ad2cf9faa9f30..c9bbcfb6b77127 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -832,7 +832,7 @@ private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, b [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Ssse3))] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static Vector128 SimdShuffle(Vector128 left, Vector128 right, Vector128 mask8F) + internal static Vector128 SimdShuffle(Vector128 left, Vector128 right, Vector128 mask8F) { Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index b63c711e410326..b8f9339782d17a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -35,7 +34,11 @@ public static partial class Base64 /// - NeedMoreData - only if is , otherwise the output is padded if the input is not a multiple of 3 /// It does not return InvalidData since that is not possible for base64 encoding. /// - public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => + EncodeToUtf8(bytes, utf8, out bytesConsumed, out bytesWritten, isFinalBlock); + + internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + where TBase64Encoder : IBase64Encoder { if (bytes.IsEmpty) { @@ -49,16 +52,7 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span { int srcLength = bytes.Length; int destLength = utf8.Length; - int maxSrcLength; - - if (srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength)) - { - maxSrcLength = srcLength; - } - else - { - maxSrcLength = (destLength >> 2) * 3; - } + int maxSrcLength = TBase64Encoder.GetMaxSrcLength(srcLength, destLength); byte* src = srcBytes; byte* dest = destBytes; @@ -67,19 +61,19 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span if (maxSrcLength >= 16) { - byte* end = srcMax - 64; + byte* end = srcMax - 48; if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) { - Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; } - end = srcMax - 64; + end = srcMax - 32; if (Avx2.IsSupported && (end >= src)) { - Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -88,7 +82,7 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span end = srcMax - 48; if (AdvSimd.Arm64.IsSupported && (end >= src)) { - AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -97,14 +91,14 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span end = srcMax - 16; if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) { - Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; } } - ref byte encodingMap = ref MemoryMarshal.GetReference(EncodingMap); + ref byte encodingMap = ref MemoryMarshal.GetReference(TBase64Encoder.EncodingMap); uint result = 0; srcMax -= 2; @@ -129,17 +123,17 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span if (src + 1 == srcEnd) { - result = EncodeAndPadTwo(src, ref encodingMap); + result = TBase64Encoder.EncodeOneOptionallyPadTwo(src, ref encodingMap); Unsafe.WriteUnaligned(dest, result); src += 1; - dest += 4; + dest += TBase64Encoder.IncrementPadTwo; } else if (src + 2 == srcEnd) { - result = EncodeAndPadOne(src, ref encodingMap); + result = TBase64Encoder.EncodeTwoOptionallyPadOne(src, ref encodingMap); Unsafe.WriteUnaligned(dest, result); src += 2; - dest += 4; + dest += TBase64Encoder.IncrementPadOne; } DoneExit: @@ -201,25 +195,28 @@ public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int { int encodedLength = GetMaxEncodedToUtf8Length(dataLength); if (buffer.Length < encodedLength) - goto FalseExit; + { + bytesWritten = 0; + return OperationStatus.DestinationTooSmall; + } int leftover = dataLength - (dataLength / 3) * 3; // how many bytes after packs of 3 uint destinationIndex = (uint)(encodedLength - 4); uint sourceIndex = (uint)(dataLength - leftover); uint result = 0; - ref byte encodingMap = ref MemoryMarshal.GetReference(EncodingMap); + ref byte encodingMap = ref MemoryMarshal.GetReference(Base64Encoder.EncodingMap); // encode last pack to avoid conditional in the main loop if (leftover != 0) { if (leftover == 1) { - result = EncodeAndPadTwo(bufferBytes + sourceIndex, ref encodingMap); + result = Base64Encoder.EncodeOneOptionallyPadTwo(bufferBytes + sourceIndex, ref encodingMap); } else { - result = EncodeAndPadOne(bufferBytes + sourceIndex, ref encodingMap); + result = Base64Encoder.EncodeTwoOptionallyPadOne(bufferBytes + sourceIndex, ref encodingMap); } Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); @@ -237,17 +234,14 @@ public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int bytesWritten = encodedLength; return OperationStatus.Done; - - FalseExit: - bytesWritten = 0; - return OperationStatus.DestinationTooSmall; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx512BW))] [CompExactlyDependsOn(typeof(Avx512Vbmi))] - private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Encoder : IBase64Encoder { // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/encode // If we have AVX512 support, pick off 48 bytes at a time for as long as we can. @@ -263,7 +257,7 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, 0x0d0e0c0d, 0x10110f10, 0x13141213, 0x16171516, 0x191a1819, 0x1c1d1b1c, 0x1f201e1f, 0x22232122, 0x25262425, 0x28292728, 0x2b2c2a2b, 0x2e2f2d2e).AsSByte(); - Vector512 vbmiLookup = Vector512.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8).AsSByte(); + Vector512 vbmiLookup = Vector512.Create(TBase64Encoder.EncodingMap).AsSByte(); Vector512 maskAC = Vector512.Create((uint)0x0fc0fc00).AsUInt16(); Vector512 maskBB = Vector512.Create((uint)0x3f003f00); @@ -273,7 +267,7 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, AssertRead>(src, srcStart, sourceLength); // This algorithm requires AVX512VBMI support. - // Vbmi was first introduced in CannonLake and is avaialable from IceLake on. + // Vbmi was first introduced in CannonLake and is available from IceLake on. // str = [...|PONM|LKJI|HGFE|DCBA] Vector512 str = Vector512.Load(src).AsSByte(); @@ -320,7 +314,8 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void Avx2Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Encoder : IBase64Encoder { // If we have AVX2 support, pick off 24 bytes at a time for as long as we can. // But because we read 32 bytes at a time, ensure we have enough room to do a @@ -345,15 +340,7 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref byte* destBytes, b 7, 6, 8, 7, 10, 9, 11, 10); - Vector256 lut = Vector256.Create( - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -19, -16, 0, 0, - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -19, -16, 0, 0); + Vector256 lut = TBase64Encoder.Avx2Lut; Vector256 maskAC = Vector256.Create(0x0fc0fc00).AsSByte(); Vector256 maskBB = Vector256.Create(0x003f03f0).AsSByte(); @@ -491,7 +478,8 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref byte* destBytes, b [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Encoder : IBase64Encoder { // C# implementation of https://github.com/aklomp/base64/blob/3a5add8652076612a8407627a42c768736a4263f/lib/arch/neon64/enc_loop.c Vector128 str1; @@ -504,7 +492,7 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref byte* destBytes Vector128 tblEnc1 = Vector128.Create("ABCDEFGHIJKLMNOP"u8).AsByte(); Vector128 tblEnc2 = Vector128.Create("QRSTUVWXYZabcdef"u8).AsByte(); Vector128 tblEnc3 = Vector128.Create("ghijklmnopqrstuv"u8).AsByte(); - Vector128 tblEnc4 = Vector128.Create("wxyz0123456789+/"u8).AsByte(); + Vector128 tblEnc4 = TBase64Encoder.AdvSimdLut4; byte* src = srcBytes; byte* dest = destBytes; @@ -550,7 +538,8 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref byte* destBytes [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Ssse3))] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void Vector128Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void Vector128Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Encoder : IBase64Encoder { // If we have SSSE3 support, pick off 12 bytes at a time for as long as we can. // But because we read 16 bytes at a time, ensure we have enough room to do a @@ -561,7 +550,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes, ref byte* destByt // The JIT won't hoist these "constants", so help it Vector128 shuffleVec = Vector128.Create(0x01020001, 0x04050304, 0x07080607, 0x0A0B090A).AsByte(); - Vector128 lut = Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, 0x0000F0ED).AsByte(); + Vector128 lut = TBase64Encoder.Ssse3AdvSimdLut; Vector128 maskAC = Vector128.Create(0x0fc0fc00).AsByte(); Vector128 maskBB = Vector128.Create(0x003f03f0).AsByte(); Vector128 shiftAC = Vector128.Create(0x04000040).AsUInt16(); @@ -672,7 +661,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes, ref byte* destByt } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) + internal static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) { uint t0 = threeBytes[0]; uint t1 = threeBytes[1]; @@ -695,52 +684,76 @@ private static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe uint EncodeAndPadOne(byte* twoBytes, ref byte encodingMap) + internal const uint EncodingPad = '='; // '=', for padding + + internal const int MaximumEncodeLength = (int.MaxValue / 4) * 3; // 1610612733 + + private readonly struct Base64Encoder : IBase64Encoder { - uint t0 = twoBytes[0]; - uint t1 = twoBytes[1]; + public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8; - uint i = (t0 << 16) | (t1 << 8); + public static Vector256 Avx2Lut => Vector256.Create( + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + -19, -16, 0, 0, + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + -19, -16, 0, 0); - uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); - uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); - uint i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + public static Vector128 AdvSimdLut4 => Vector128.Create("wxyz0123456789+/"u8).AsByte(); - if (BitConverter.IsLittleEndian) - { - return i0 | (i1 << 8) | (i2 << 16) | (EncodingPad << 24); - } - else - { - return (i0 << 24) | (i1 << 16) | (i2 << 8) | EncodingPad; - } - } + public static Vector128 Ssse3AdvSimdLut => Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, 0x0000F0ED).AsByte(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe uint EncodeAndPadTwo(byte* oneByte, ref byte encodingMap) - { - uint t0 = oneByte[0]; + public static int IncrementPadTwo => 4; - uint i = t0 << 8; + public static int IncrementPadOne => 4; - uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 10)); - uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 4) & 0x3F)); + public static int GetMaxSrcLength(int srcLength, int destLength) => + srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength) ? srcLength : (destLength >> 2) * 3; - if (BitConverter.IsLittleEndian) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap) { - return i0 | (i1 << 8) | (EncodingPad << 16) | (EncodingPad << 24); + uint t0 = oneByte[0]; + + uint i = t0 << 8; + + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 10)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 4) & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + return i0 | (i1 << 8) | (EncodingPad << 16) | (EncodingPad << 24); + } + else + { + return (i0 << 24) | (i1 << 16) | (EncodingPad << 8) | EncodingPad; + } } - else + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte encodingMap) { - return (i0 << 24) | (i1 << 16) | (EncodingPad << 8) | EncodingPad; - } - } + uint t0 = twoBytes[0]; + uint t1 = twoBytes[1]; - internal const uint EncodingPad = '='; // '=', for padding + uint i = (t0 << 16) | (t1 << 8); - private const int MaximumEncodeLength = (int.MaxValue / 4) * 3; // 1610612733 + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + uint i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); - internal static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8; + if (BitConverter.IsLittleEndian) + { + return i0 | (i1 << 8) | (i2 << 16) | (EncodingPad << 24); + } + else + { + return (i0 << 24) | (i1 << 16) | (i2 << 8) | EncodingPad; + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs new file mode 100644 index 00000000000000..b84e58a8a63e64 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Buffers.Text +{ + public static partial class Base64Url + { + /*// Decode from utf8 => bytes + public static OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => throw new NotImplementedException(); + public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => throw new NotImplementedException(); + + /// + /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "length". + /// + /// + /// Thrown when the specified is less than 0. + /// + public static int GetMaxDecodedFromUtf8Length(int length) => throw new NotImplementedException(); + + // IsValid + public static bool IsValid(ReadOnlySpan base64UrlText) => throw new NotImplementedException(); + public static bool IsValid(ReadOnlySpan base64UrlText, out int decodedLength) => throw new NotImplementedException(); + public static bool IsValid(ReadOnlySpan base64UrlTextUtf8) => throw new NotImplementedException(); + public static bool IsValid(ReadOnlySpan base64UrlTextUtf8, out int decodedLength) => throw new NotImplementedException(); + + // Up to this point, this is a mirror of System.Buffers.Text.Base64 + // Below are more helpers that bring over functionality similar to Convert.*Base64* + + // Encode to / decode from chars + public static bool TryDecodeFromChars(ReadOnlySpan chars, Span bytes, out int bytesWritten) => throw new NotImplementedException(); + + + // These are just accelerator methods. + // Should be efficiently implementable on top of the other ones in just a few lines. + + // Decode from chars => string + // Decode from chars => byte[] + // The names could also just be "Decode" without naming the return type + public static string DecodeToString(ReadOnlySpan chars, Encoding encoding) => throw new NotImplementedException(); + public static byte[] DecodeToByteArray(ReadOnlySpan chars) => throw new NotImplementedException();*/ + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs new file mode 100644 index 00000000000000..20b2e1256bb995 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using static System.Buffers.Text.Base64; + +namespace System.Buffers.Text +{ + public static partial class Base64Url + { + /// + /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. + /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// (default) when the input span contains the entire data to encode. + /// Set to when the source buffer contains the entirety of the data to encode. + /// Set to if this method is being called in a loop and if more input data may follow. + /// At the end of the loop, call this (potentially with an empty source buffer) passing . + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input + /// - NeedMoreData - only if is + /// It does not return InvalidData since that is not possible for base64 encoding. + /// + /// The output will not be padded even if the input is not a multiple of 3. + public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => + EncodeToUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + /// + /// Returns the length (in bytes) of the result if you were to encode binary data within a byte span of size "length". + /// + /// + /// Thrown when the specified is less than 0 or larger than 1610612733 (since encode inflates the data by 4/3). + /// + public static int GetEncodedLength(int bytesLength) + { + if ((uint)bytesLength > Base64.MaximumEncodeLength) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + + int remainder = bytesLength % 3; + + return bytesLength / 3 * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. + } + + /// + /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. + /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. + /// The output will not be padded even if the input is not a multiple of 3. + public static int EncodeToUtf8(ReadOnlySpan source, Span destination) + { + EncodeToUtf8(source, destination, out _, out int written); + + return written; + } + + /// + /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output byte array which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. + /// The output will not be padded even if the input is not a multiple of 3. + public static byte[] EncodeToUtf8(ReadOnlySpan source) + { + if (source.Length == 0) + { + return Array.Empty(); + } + + Span destination = stackalloc byte[GetEncodedLength(source.Length)]; // or new byte[GetEncodedLength(source.Length)] + EncodeToUtf8(source, destination, out _, out int written); + + return destination.Slice(0, written).ToArray(); + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// (default) when the input span contains the entire data to encode. + /// Set to when the source buffer contains the entirety of the data to encode. + /// Set to if this method is being called in a loop and if more input data may follow. + /// At the end of the loop, call this (potentially with an empty source buffer) passing . + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input + /// - NeedMoreData - only if is + /// It does not return InvalidData since that is not possible for base64 encoding. + /// + /// The output will not be padded even if the input is not a multiple of 3. + public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) + { + if (source.Length == 0) + { + bytesConsumed = 0; + charsWritten = 0; + return OperationStatus.Done; + } + + return EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out bytesConsumed, out charsWritten, isFinalBlock); + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. + /// The output will not be padded even if the input is not a multiple of 3. + public static int EncodeToChars(ReadOnlySpan source, Span destination) + { + EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int written); + return written; + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// A char array which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The output will not be padded even if the input is not a multiple of 3. + public static char[] EncodeToChars(ReadOnlySpan source) + { + if (source.Length == 0) + { + return Array.Empty(); + } + + Span destination = stackalloc char[GetEncodedLength(source.Length)]; + EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int charsWritten); + + return destination.Slice(0, charsWritten).ToArray(); + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// A string which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The output will not be padded even if the input is not a multiple of 3. + public static string EncodeToString(ReadOnlySpan source) + { + if (source.Length == 0) + { + return string.Empty; + } + + Span destination = stackalloc byte[GetEncodedLength(source.Length)]; + EncodeToUtf8(source, destination, out _, out int charsWritten); + + return destination.Slice(0, charsWritten).ToString(); // Encoding.UTF8.GetString(utf8.Slice(0, bytesWritten)) + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// if chars encoded successfully, otherwise . + /// The output will not be padded even if the input is not a multiple of 3. + public static bool TryEncodeToChars(ReadOnlySpan source, Span destination, out int charsWritten) + { + OperationStatus status = EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out charsWritten); + + return status == OperationStatus.Done; + } + + /// + /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// + /// The input span which contains binary data that needs to be encoded. + /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. + /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// if bytes encoded successfully, otherwise . + /// The output will not be padded even if the input is not a multiple of 3. + public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destination, out int charsWritten) + { + OperationStatus status = EncodeToUtf8(source, destination, out _, out charsWritten); + + return status == OperationStatus.Done; + } + + /// + /// Encode the span of binary data (in-place) into UTF-8 encoded text represented as base 64. + /// The encoded text output is larger than the binary data contained in the input (the operation inflates the data). + /// + /// The input span which contains binary data that needs to be encoded. + /// It needs to be large enough to fit the result of the operation. + /// The amount of binary data contained within the buffer that needs to be encoded + /// (and needs to be smaller than the buffer length). + /// The number of bytes written into the buffer. + /// if bytes encoded successfully, otherwise . + public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) + { + if (buffer.IsEmpty) + { + bytesWritten = 0; + return true; + } + + fixed (byte* bufferBytes = &MemoryMarshal.GetReference(buffer)) + { + int encodedLength = GetEncodedLength(dataLength); + if (buffer.Length < encodedLength) + { + bytesWritten = 0; + return false; + } + + int leftover = dataLength % 3; // how many bytes left after packs of 3 + + uint destinationIndex = leftover > 0 ? (uint)(encodedLength - leftover - 1) : (uint)(encodedLength - 4); + uint sourceIndex = (uint)(dataLength - leftover); + uint result = 0; + ref byte encodingMap = ref MemoryMarshal.GetReference(Base64UrlEncoder.EncodingMap); + + // encode last pack to avoid conditional in the main loop + if (leftover != 0) + { + if (leftover == 1) + { + result = Base64UrlEncoder.EncodeOneOptionallyPadTwo(bufferBytes + sourceIndex, ref encodingMap); + } + else + { + result = Base64UrlEncoder.EncodeTwoOptionallyPadOne(bufferBytes + sourceIndex, ref encodingMap); + } + + Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); + destinationIndex -= 4; + } + + sourceIndex -= 3; + while ((int)sourceIndex >= 0) + { + result = Encode(bufferBytes + sourceIndex, ref encodingMap); + Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); + destinationIndex -= 4; + sourceIndex -= 3; + } + + bytesWritten = encodedLength; + return true; + } + } + + private readonly struct Base64UrlEncoder : IBase64Encoder + { + public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; + + public static Vector256 Avx2Lut => Vector256.Create( + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + -17, 32, 0, 0, + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + -17, 32, 0, 0); + + public static Vector128 AdvSimdLut4 => Vector128.Create("wxyz0123456789-_"u8).AsByte(); + + public static Vector128 Ssse3AdvSimdLut => Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, 0x000020EF).AsByte(); + + public static int IncrementPadTwo => 2; + + public static int IncrementPadOne => 3; + + public static int GetMaxSrcLength(int srcLength, int destLength) => + srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? srcLength : (destLength >> 2) * 3 + destLength % 4; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap) + { + uint t0 = oneByte[0]; + + uint i = t0 << 8; + + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 10)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 4) & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + return i0 | (i1 << 8); + } + else + { + return (i0 << 8) | i1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte encodingMap) + { + uint t0 = twoBytes[0]; + uint t1 = twoBytes[1]; + + uint i = (t0 << 16) | (t1 << 8); + + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + uint i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + return i0 | (i1 << 8) | (i2 << 16); + } + else + { + return (i0 << 16) | (i1 << 8) | i2; + } + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs new file mode 100644 index 00000000000000..0bc3030db54a2f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Buffers.Text +{ + public static partial class Base64Url + { + /// Validates that the specified span of text is comprised of valid base-64 encoded data. + /// A span of text to validate. + /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . + /// TODO : Update remarks + /// If the method returns , the same text passed to and + /// would successfully decode (in the case + /// of assuming sufficient output space). Any amount of whitespace is allowed anywhere in the input, + /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. + /// + public static bool IsValid(ReadOnlySpan base64UrlText) => + Base64.IsValid(base64UrlText, out _); + + /// Validates that the specified span of text is comprised of valid base-64 encoded data. + /// A span of text to validate. + /// If the method returns true, the number of decoded bytes that will result from decoding the input text. + /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . + /// TODO : Update remarks + /// If the method returns , the same text passed to and + /// would successfully decode (in the case + /// of assuming sufficient output space). Any amount of whitespace is allowed anywhere in the input, + /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. + /// + public static bool IsValid(ReadOnlySpan base64UrlText, out int decodedLength) => + Base64.IsValid(base64UrlText, out decodedLength); + + /// Validates that the specified span of UTF-8 text is comprised of valid base-64 encoded data. + /// A span of UTF-8 text to validate. + /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . + /// TODO : Update remarks + /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n' (as bytes). + /// + public static bool IsValid(ReadOnlySpan utf8Base64UrlText) => + Base64.IsValid(utf8Base64UrlText, out _); + + /// Validates that the specified span of UTF-8 text is comprised of valid base-64 encoded data. + /// A span of UTF-8 text to validate. + /// If the method returns true, the number of decoded bytes that will result from decoding the input UTF-8 text. + /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . + /// TODO : Update remarks + /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n' (as bytes). + /// + public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decodedLength) => + Base64.IsValid(utf8Base64UrlText, out decodedLength); + + + private readonly struct Base64UrlCharValidatable : Base64.IBase64Validatable + { + private static readonly SearchValues s_validBase64UrlChars = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + + public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); + public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); + public static bool IsEncodingPad(char value) => value == Base64.EncodingPad || value == UrlEncodingPad; + public static bool ValidateAndDecodeLength(int length, out int decodedLength) + { + decodedLength = (int)((uint)length / 4 * 3) + length % 4; + return true; + } + } + + private readonly struct Base64UrlByteValidatable : Base64.IBase64Validatable + { + private static readonly SearchValues s_validBase64UrlChars = SearchValues.Create(Base64UrlEncoder.EncodingMap); + + public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); + public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); + public static bool IsEncodingPad(byte value) => value == Base64.EncodingPad || value == UrlEncodingPad; + public static bool ValidateAndDecodeLength(int length, out int decodedLength) + { + decodedLength = (int)((uint)length / 4 * 3) + length % 4; + return true; + } + } + + private const uint UrlEncodingPad = '%'; // url padding + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index 22071725a23520..cfd373a6d7bf5f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -53,7 +53,7 @@ public static bool IsValid(ReadOnlySpan base64TextUtf8) => public static bool IsValid(ReadOnlySpan base64TextUtf8, out int decodedLength) => IsValid(base64TextUtf8, out decodedLength); - private static bool IsValid(ReadOnlySpan base64Text, out int decodedLength) + internal static bool IsValid(ReadOnlySpan base64Text, out int decodedLength) where TBase64Validatable : IBase64Validatable { int length = 0, paddingCount = 0; @@ -116,14 +116,17 @@ private static bool IsValid(ReadOnlySpan base64Text, o break; } - if (length % 4 != 0) + if (!TBase64Validatable.ValidateAndDecodeLength(length, out decodedLength)) { goto Fail; } + + // Remove padding to get exact length. + decodedLength -= paddingCount; + return true; } - // Remove padding to get exact length. - decodedLength = (int)((uint)length / 4 * 3) - paddingCount; + decodedLength = 0; return true; Fail: @@ -131,11 +134,24 @@ private static bool IsValid(ReadOnlySpan base64Text, o return false; } - private interface IBase64Validatable + private static bool ValidateAndDecodeLength(int length, out int decodedLength) + { + if (length % 4 == 0) + { + decodedLength = (int)((uint)length / 4 * 3); + return true; + } + + decodedLength = 0; + return false; + } + + internal interface IBase64Validatable { static abstract int IndexOfAnyExcept(ReadOnlySpan span); static abstract bool IsWhiteSpace(T value); static abstract bool IsEncodingPad(T value); + static abstract bool ValidateAndDecodeLength(int length, out int decodedLength); } private readonly struct Base64CharValidatable : IBase64Validatable @@ -145,15 +161,17 @@ private interface IBase64Validatable public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == EncodingPad; + public static bool ValidateAndDecodeLength(int length, out int decodedLength) => Base64.ValidateAndDecodeLength(length, out decodedLength); } private readonly struct Base64ByteValidatable : IBase64Validatable { - private static readonly SearchValues s_validBase64Chars = SearchValues.Create(EncodingMap); + private static readonly SearchValues s_validBase64Chars = SearchValues.Create(Base64Encoder.EncodingMap); public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(byte value) => value == EncodingPad; + public static bool ValidateAndDecodeLength(int length, out int decodedLength) => Base64.ValidateAndDecodeLength(length, out decodedLength); } } } From ea764bb6722513f22c7b8daf896a60a9c27559e3 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 25 Apr 2024 17:36:57 -0700 Subject: [PATCH 02/38] Validation related updates --- .../Base64Url/Base64UrlValidationUnitTests.cs | 45 +++++++++++++++---- .../Text/Base64Url/Base64UrlValidator.cs | 31 ++++++++----- .../System/Buffers/Text/Base64Validator.cs | 17 +++---- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs index 384b9f60f9767c..2ed3f41f9e39d6 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -9,6 +9,27 @@ namespace System.Buffers.Text.Tests { public class Base64UrlValidationUnitTests : Base64TestBase { + public static readonly byte[] s_encodingMap = { + 65, 66, 67, 68, 69, 70, 71, 72, //A..H + 73, 74, 75, 76, 77, 78, 79, 80, //I..P + 81, 82, 83, 84, 85, 86, 87, 88, //Q..X + 89, 90, 97, 98, 99, 100, 101, 102, //Y..Z, a..f + 103, 104, 105, 106, 107, 108, 109, 110, //g..n + 111, 112, 113, 114, 115, 116, 117, 118, //o..v + 119, 120, 121, 122, 48, 49, 50, 51, //w..z, 0..3 + 52, 53, 54, 55, 56, 57, 45, 95 //4..9, -, _ + }; + + private static void InitializeDecodableBytes(Span bytes, int seed = 100) + { + var rnd = new Random(seed); + for (int i = 0; i < bytes.Length; i++) + { + int index = (byte)rnd.Next(0, s_encodingMap.Length); + bytes[i] = s_encodingMap[index]; + } + } + [Fact] public void BasicValidationBytes() { @@ -22,10 +43,10 @@ public void BasicValidationBytes() } while (numBytes % 4 != 0); // ensure we have a valid length Span source = new byte[numBytes]; - Base64TestHelper.InitializeDecodableBytes(source, numBytes); + InitializeDecodableBytes(source, numBytes); Assert.True(Base64Url.IsValid(source)); - Assert.True(Base64Url.IsValid(source, out int decodedLength)); + Assert.True(Base64Url.IsValid(source, out int decodedLength)); Assert.True(decodedLength > 0); } } @@ -43,7 +64,7 @@ public void BasicValidationChars() } while (numBytes % 4 != 0); // ensure we have a valid length Span source = new byte[numBytes]; - Base64TestHelper.InitializeDecodableBytes(source, numBytes); + InitializeDecodableBytes(source, numBytes); Span chars = source .ToArray() .Select(Convert.ToChar) @@ -203,6 +224,9 @@ public void ValidateWithOnlyCharsToBeIgnoredChars(string utf8WithByteToBeIgnored [InlineData("YQ ==", 1)] [InlineData("YQ= =", 1)] [InlineData("YQ== ", 1)] + [InlineData("YQ%%", 1)] + [InlineData("YWI%", 2)] + [InlineData("YW% ", 1)] public void ValidateWithPaddingReturnsCorrectCountBytes(string utf8WithByteToBeIgnored, int expectedLength) { byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); @@ -226,6 +250,9 @@ public void ValidateWithPaddingReturnsCorrectCountBytes(string utf8WithByteToBeI [InlineData("YQ ==", 1)] [InlineData("YQ= =", 1)] [InlineData("YQ== ", 1)] + [InlineData("YQ%%", 1)] + [InlineData("YWI%", 2)] + [InlineData("YW% ", 1)] public void ValidateWithPaddingReturnsCorrectCountChars(string utf8WithByteToBeIgnored, int expectedLength) { ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); @@ -250,8 +277,8 @@ public void DecodeEmptySpan(string utf8WithByteToBeIgnored, int expectedLength) [Theory] [InlineData("YWJ", true, 2)] - //[InlineData("YW", true, 1)] - //[InlineData("Y", false, 0)] + [InlineData("YW", true, 1)] + [InlineData("Y", false, 0)] public void SmallSizeBytes(string utf8Text, bool isValid, int expectedDecodedLength) { byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8Text); @@ -263,9 +290,9 @@ public void SmallSizeBytes(string utf8Text, bool isValid, int expectedDecodedLen [Theory] [InlineData("YWJ", true, 2)] - //[InlineData("YW", true, 1)] - //[InlineData("Y", false, 0)] - public void InvalidSizeChars(string utf8Text, bool isValid, int expectedDecodedLength) + [InlineData("YW", true, 1)] + [InlineData("Y", false, 0)] + public void SmallSizeChars(string utf8Text, bool isValid, int expectedDecodedLength) { ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8Text; @@ -290,6 +317,8 @@ public void InvalidSizeChars(string utf8Text, bool isValid, int expectedDecodedL [InlineData("aYQ= =a")] [InlineData("aYQ== a")] [InlineData("aYQ==a ")] + [InlineData("YQ+a")] // plus invalid + [InlineData("/Qab")] // slash invalid public void InvalidBase64UrlBytes(string utf8WithByteToBeIgnored) { byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 0bc3030db54a2f..502071837dacb2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -49,6 +49,21 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText) => public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decodedLength) => Base64.IsValid(utf8Base64UrlText, out decodedLength); + private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) + { + if (length == 1) + { + decodedLength = 0; + return false; + } + + // Padding is optional for Base64Url, so need to account remainder. + int remainder = length % 4; + decodedLength = (int)((uint)length / 4 * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; + return true; + } + + private const uint UrlEncodingPad = '%'; // url padding private readonly struct Base64UrlCharValidatable : Base64.IBase64Validatable { @@ -57,11 +72,8 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decoded public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == Base64.EncodingPad || value == UrlEncodingPad; - public static bool ValidateAndDecodeLength(int length, out int decodedLength) - { - decodedLength = (int)((uint)length / 4 * 3) + length % 4; - return true; - } + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => + Base64Url.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } private readonly struct Base64UrlByteValidatable : Base64.IBase64Validatable @@ -71,13 +83,8 @@ public static bool ValidateAndDecodeLength(int length, out int decodedLength) public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(byte value) => value == Base64.EncodingPad || value == UrlEncodingPad; - public static bool ValidateAndDecodeLength(int length, out int decodedLength) - { - decodedLength = (int)((uint)length / 4 * 3) + length % 4; - return true; - } + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => + Base64Url.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } - - private const uint UrlEncodingPad = '%'; // url padding } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index cfd373a6d7bf5f..8f8e22ae94263b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -116,13 +116,11 @@ internal static bool IsValid(ReadOnlySpan base64Text, break; } - if (!TBase64Validatable.ValidateAndDecodeLength(length, out decodedLength)) + if (!TBase64Validatable.ValidateAndDecodeLength(length, paddingCount, out decodedLength)) { goto Fail; } - // Remove padding to get exact length. - decodedLength -= paddingCount; return true; } @@ -134,11 +132,12 @@ internal static bool IsValid(ReadOnlySpan base64Text, return false; } - private static bool ValidateAndDecodeLength(int length, out int decodedLength) + private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { if (length % 4 == 0) { - decodedLength = (int)((uint)length / 4 * 3); + // Remove padding to get exact length. + decodedLength = (int)((uint)length / 4 * 3) - paddingCount; return true; } @@ -151,7 +150,7 @@ internal interface IBase64Validatable static abstract int IndexOfAnyExcept(ReadOnlySpan span); static abstract bool IsWhiteSpace(T value); static abstract bool IsEncodingPad(T value); - static abstract bool ValidateAndDecodeLength(int length, out int decodedLength); + static abstract bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength); } private readonly struct Base64CharValidatable : IBase64Validatable @@ -161,7 +160,8 @@ internal interface IBase64Validatable public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == EncodingPad; - public static bool ValidateAndDecodeLength(int length, out int decodedLength) => Base64.ValidateAndDecodeLength(length, out decodedLength); + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => + Base64.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } private readonly struct Base64ByteValidatable : IBase64Validatable @@ -171,7 +171,8 @@ internal interface IBase64Validatable public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(byte value) => value == EncodingPad; - public static bool ValidateAndDecodeLength(int length, out int decodedLength) => Base64.ValidateAndDecodeLength(length, out decodedLength); + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => + Base64.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } } } From 4489fcb5f04f2223964d93a0bd39e2f6d32d51ab Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Sun, 28 Apr 2024 19:02:04 -0700 Subject: [PATCH 03/38] Try fix perf regression in vectorized methods --- .../System.Memory/ref/System.Memory.cs | 4 +- .../tests/Base64Url/Base64UrlEncoderTests.cs | 2 +- .../Base64Url/Base64UrlEncodingAPIsTests.cs | 99 +++++++++++++++ .../tests/System.Memory.Tests.csproj | 1 + .../src/System/Buffers/Text/Base64.cs | 18 ++- .../src/System/Buffers/Text/Base64Decoder.cs | 118 ++++++++++-------- .../src/System/Buffers/Text/Base64Encoder.cs | 30 ++--- .../Text/Base64Url/Base64UrlDecoder.cs | 91 ++++++++++++-- .../Text/Base64Url/Base64UrlEncoder.cs | 15 +-- 9 files changed, 281 insertions(+), 97 deletions(-) create mode 100644 src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 8f6a5ac768e808..46cd070432ccd7 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -641,7 +641,9 @@ namespace System.Buffers.Text { public static class Base64Url { - public static OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } + public static int GetMaxDecodedLength(int base64Length) { throw null; } + public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs index d3403d3ddac51b..496e05ad19897f 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs @@ -30,7 +30,7 @@ public void BasicEncodingAndDecoding() string expectedText = Convert.ToBase64String(bytes, 0, value + 1).Replace('+', '-').Replace('/', '_').TrimEnd('='); Assert.Equal(expectedText, encodedText); - /*Assert.Equal(0, encodedBytes.Length % 4); + /*TODO Assert.Equal(0, encodedBytes.Length % 4); Span decodedBytes = new byte[Base64.GetMaxDecodedFromUtf8Length(encodedBytes.Length)]; Assert.Equal(OperationStatus.Done, Base64.DecodeFromUtf8(encodedBytes, decodedBytes, out consumed, out int decodedByteCount)); Assert.Equal(encodedBytes.Length, consumed); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs new file mode 100644 index 00000000000000..de0f68768ee43f --- /dev/null +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Buffers.Text.Tests +{ + public class Base64UrlEncodingAPIsTests + { + [Fact] + public static void EncodeToChars() + { + string input = "test"; + byte[] inputBytes = Convert.FromBase64String(input); + Span resultChars = new char[4]; + OperationStatus operationStatus = Base64Url.EncodeToChars(inputBytes, resultChars, out int bytesConsumed, out int charsWritten); + Assert.Equal(OperationStatus.Done, operationStatus); + Assert.Equal(4, bytesConsumed); + Assert.Equal(3, charsWritten); + } + + [Fact] + public static void ShortInputArray() + { + // Regression test for bug where a short input array caused an exception to be thrown + byte[] inputBuffer = "abc"u8.ToArray(); + char[] outputBuffer = new char[4]; + Convert.ToBase64CharArray(inputBuffer, 0, 3, outputBuffer, 0); + Convert.ToBase64CharArray(inputBuffer, 0, 2, outputBuffer, 0); + } + + [Fact] + public static void ValidOffsetOut() + { + // Regression test for bug where offsetOut parameter was ignored + char[] outputBuffer = "........".ToCharArray(); + byte[] inputBuffer = new byte[6]; + for (int i = 0; i < inputBuffer.Length; inputBuffer[i] = (byte)i++) ; + + // Convert the first half of the byte array, write to the first half of the char array + int c = Convert.ToBase64CharArray(inputBuffer, 0, 3, outputBuffer, 0); + Assert.Equal(4, c); + Assert.Equal("AAEC....", new string(outputBuffer)); + + // Convert the second half of the byte array, write to the second half of the char array + c = Convert.ToBase64CharArray(inputBuffer, 3, 3, outputBuffer, 4); + Assert.Equal(4, c); + Assert.Equal("AAECAwQF", new string(outputBuffer)); + } + + [Fact] + public static void InvalidInputBuffer() + { + Assert.Throws(() => Convert.ToBase64CharArray(null, 0, 1, new char[1], 0)); + } + + [Fact] + public static void InvalidOutputBuffer() + { + char[] inputChars = "test".ToCharArray(); + byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, null, 0)); + } + + [Fact] + public static void InvalidOffsetIn() + { + char[] inputChars = "test".ToCharArray(); + byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); + char[] outputBuffer = new char[4]; + + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, -1, inputBytes.Length, outputBuffer, 0)); + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, inputBytes.Length, inputBytes.Length, outputBuffer, 0)); + } + + [Fact] + public static void InvalidOffsetOut() + { + char[] inputChars = "test".ToCharArray(); + byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); + char[] outputBuffer = new char[4]; + + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, outputBuffer, -1)); + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, outputBuffer, 1)); + } + + [Fact] + public static void InvalidInputLength() + { + char[] inputChars = "test".ToCharArray(); + byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); + char[] outputBuffer = new char[4]; + + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, -1, outputBuffer, 0)); + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length + 1, outputBuffer, 0)); + Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 1, inputBytes.Length, outputBuffer, 0)); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index 1087e40071f094..e06d6d445bd05f 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index f19586eb120f75..4a96db04bd9161 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -39,15 +39,23 @@ internal static unsafe void AssertWrite(byte* dest, byte* destStart, in internal interface IBase64Encoder { - static abstract int IncrementPadTwo { get; } - static abstract int IncrementPadOne { get; } static abstract ReadOnlySpan EncodingMap { get; } - static abstract Vector256 Avx2Lut { get; } - static abstract Vector128 AdvSimdLut4 { get; } - static abstract Vector128 Ssse3AdvSimdLut { get; } + static abstract sbyte Avx2LutChar62 { get; } + static abstract sbyte Avx2LutChar63 { get; } + static abstract ReadOnlySpan AdvSimdLut4 { get; } + static abstract uint Ssse3AdvSimdLutE3 { get; } static abstract int GetMaxSrcLength(int srcLength, int destLength); static abstract unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap); static abstract unsafe uint EncodeTwoOptionallyPadOne(byte* oneByte, ref byte encodingMap); + static abstract int IncrementPadTwo { get; } + static abstract int IncrementPadOne { get; } + } + + internal interface IBase64Decoder + { + static abstract ReadOnlySpan DecodingMap { get; } + static abstract Vector512 VbmiLookup0 { get; } + static abstract Vector512 VbmiLookup1 { get; } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index c9bbcfb6b77127..9356f8629513ed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -35,9 +35,10 @@ public static partial class Base64 /// or if the input is incomplete (i.e. not a multiple of 4) and is . /// public static OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - DecodeFromUtf8(utf8, bytes, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + DecodeFromUtf8(utf8, bytes, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - private static unsafe OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) + internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) + where TBase64Decoder : IBase64Decoder { if (utf8.IsEmpty) { @@ -71,7 +72,7 @@ private static unsafe OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Sp byte* end = srcMax - 88; if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) { - Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -118,7 +119,7 @@ private static unsafe OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Sp maxSrcLength = (destLength / 3) * 4; } - ref sbyte decodingMap = ref MemoryMarshal.GetReference(DecodingMap); + ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); srcMax = srcBytes + maxSrcLength; while (src < srcMax) @@ -294,7 +295,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span b // Fall back to block-wise decoding. This is very slow, but it's also very non-standard // formatting of the input; whitespace is typically only found between blocks, such as // when Convert.ToBase64String inserts a line break every 76 output characters. - return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); } // Skip over the starting whitespace and continue. @@ -302,7 +303,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span b utf8 = utf8.Slice(localConsumed); // Try again after consumed whitespace - status = DecodeFromUtf8(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); + status = DecodeFromUtf8(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; if (status is not OperationStatus.InvalidData) @@ -352,9 +353,10 @@ public static int GetMaxDecodedFromUtf8Length(int length) /// hence can only be called once with all the data in the buffer. /// public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => - DecodeFromUtf8InPlace(buffer, out bytesWritten, ignoreWhiteSpace: true); + DecodeFromUtf8InPlace(buffer, out bytesWritten, ignoreWhiteSpace: true); - private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) + private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) + where TBase64Decoder : IBase64Decoder { if (buffer.IsEmpty) { @@ -374,7 +376,7 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, o goto InvalidExit; } - ref sbyte decodingMap = ref MemoryMarshal.GetReference(DecodingMap); + ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); while (sourceIndex < bufferLength - 4) { @@ -454,12 +456,13 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, o InvalidExit: bytesWritten = (int)destIndex; return ignoreWhiteSpace ? - DecodeWithWhiteSpaceFromUtf8InPlace(buffer, ref bytesWritten, sourceIndex) : // The input may have whitespace, attempt to decode while ignoring whitespace. + DecodeWithWhiteSpaceFromUtf8InPlace(buffer, ref bytesWritten, sourceIndex) : // The input may have whitespace, attempt to decode while ignoring whitespace. OperationStatus.InvalidData; } } - private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc byte[BlockSize]; @@ -511,7 +514,7 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan localIsFinalBlock = false; } - status = DecodeFromUtf8(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); + status = DecodeFromUtf8(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; @@ -557,7 +560,8 @@ private static int GetPaddingCount(ref byte ptrToLastElement) return padding; } - private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) + private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) + where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc byte[BlockSize]; @@ -607,7 +611,7 @@ private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span ut break; } - status = DecodeFromUtf8InPlace(buffer, out localBytesWritten, ignoreWhiteSpace: false); + status = DecodeFromUtf8InPlace(buffer, out localBytesWritten, ignoreWhiteSpace: false); localDestIndex += localBytesWritten; hasPaddingBeenProcessed = localBytesWritten < 3; @@ -630,7 +634,8 @@ private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span ut [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx512BW))] [CompExactlyDependsOn(typeof(Avx512Vbmi))] - private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder { // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/decode // If we have AVX512 support, pick off 64 bytes at a time for as long as we can, @@ -641,17 +646,6 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, byte* src = srcBytes; byte* dest = destBytes; - // The JIT won't hoist these "constants", so help it - Vector512 vbmiLookup0 = Vector512.Create( - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x3e808080, 0x3f808080, - 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080).AsSByte(); - Vector512 vbmiLookup1 = Vector512.Create( - 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, - 0x1211100f, 0x16151413, 0x80191817, 0x80808080, - 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, - 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080).AsSByte(); Vector512 vbmiPackedLanesControl = Vector512.Create( 0x06000102, 0x090a0405, 0x0c0d0e08, 0x16101112, 0x191a1415, 0x1c1d1e18, 0x26202122, 0x292a2425, @@ -662,7 +656,7 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, Vector512 mergeConstant1 = Vector512.Create(0x00011000).AsInt16(); // This algorithm requires AVX512VBMI support. - // Vbmi was first introduced in CannonLake and is avaialable from IceLake on. + // Vbmi was first introduced in CannonLake and is available from IceLake on. do { AssertRead>(src, srcStart, sourceLength); @@ -672,7 +666,7 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, // This step also checks for invalid inputs and exits. // After this, we have indices which are verified to have upper 2 bits set to 0 in each byte. // origIndex = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] - Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(vbmiLookup0, str, vbmiLookup1); + Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(TBase64Decoder.VbmiLookup0, str, TBase64Decoder.VbmiLookup1); Vector512 errorVec = (origIndex.AsInt32() | str.AsInt32()).AsSByte(); if (errorVec.ExtractMostSignificantBits() != 0) { @@ -699,6 +693,7 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* destBytes, destBytes = dest; } + // TODO [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) @@ -924,15 +919,15 @@ private static unsafe void Vector128Decode(ref byte* srcBytes, ref byte* destByt // 1111 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 // The JIT won't hoist these "constants", so help it - Vector128 lutHi = Vector128.Create(0x02011010, 0x08040804, 0x10101010, 0x10101010).AsByte(); - Vector128 lutLo = Vector128.Create(0x11111115, 0x11111111, 0x1A131111, 0x1A1B1B1B).AsByte(); + Vector128 lutHi = Vector128.Create(0x02011010, 0x08040804, 0x10101010, 0x10101010).AsByte(); + Vector128 lutLo = Vector128.Create(0x11111115, 0x11111111, 0x1A131111, 0x1A1B1B1B).AsByte(); Vector128 lutShift = Vector128.Create(0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000).AsSByte(); Vector128 packBytesMask = Vector128.Create(0x06000102, 0x090A0405, 0x0C0D0E08, 0xffffffff).AsSByte(); - Vector128 mergeConstant0 = Vector128.Create(0x01400140).AsByte(); + Vector128 mergeConstant0 = Vector128.Create(0x01400140).AsByte(); Vector128 mergeConstant1 = Vector128.Create(0x00011000).AsInt16(); - Vector128 one = Vector128.Create((byte)1); - Vector128 mask2F = Vector128.Create((byte)'/'); - Vector128 mask8F = Vector128.Create((byte)0x8F); + Vector128 one = Vector128.Create((byte)1); + Vector128 mask2F = Vector128.Create((byte)'/'); + Vector128 mask8F = Vector128.Create((byte)0x8F); byte* src = srcBytes; byte* dest = destBytes; @@ -1088,25 +1083,40 @@ internal static bool IsWhiteSpace(int value) return value == 32; } - // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) - private static ReadOnlySpan DecodingMap => - [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, //62 is placed at index 43 (for +), 63 at index 47 (for /) - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, //0-25 are placed at index 65-90 (for A-Z) - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ]; + private readonly struct Base64Decoder : IBase64Decoder + { + // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) + public static ReadOnlySpan DecodingMap => + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, //62 is placed at index 43 (for +), 63 at index 47 (for /) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, //0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + public static Vector512 VbmiLookup0 => Vector512.Create( + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x3e808080, 0x3f808080, + 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080).AsSByte(); + + public static Vector512 VbmiLookup1 => Vector512.Create( + 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, + 0x1211100f, 0x16151413, 0x80191817, 0x80808080, + 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, + 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080).AsSByte(); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index b8f9339782d17a..fa02ad39935288 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -340,7 +340,15 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref by 7, 6, 8, 7, 10, 9, 11, 10); - Vector256 lut = TBase64Encoder.Avx2Lut; + Vector256 lut = Vector256.Create( + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0, + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0); Vector256 maskAC = Vector256.Create(0x0fc0fc00).AsSByte(); Vector256 maskBB = Vector256.Create(0x003f03f0).AsSByte(); @@ -492,7 +500,7 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref Vector128 tblEnc1 = Vector128.Create("ABCDEFGHIJKLMNOP"u8).AsByte(); Vector128 tblEnc2 = Vector128.Create("QRSTUVWXYZabcdef"u8).AsByte(); Vector128 tblEnc3 = Vector128.Create("ghijklmnopqrstuv"u8).AsByte(); - Vector128 tblEnc4 = TBase64Encoder.AdvSimdLut4; + Vector128 tblEnc4 = Vector128.Create(TBase64Encoder.AdvSimdLut4).AsByte(); byte* src = srcBytes; byte* dest = destBytes; @@ -550,7 +558,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes, r // The JIT won't hoist these "constants", so help it Vector128 shuffleVec = Vector128.Create(0x01020001, 0x04050304, 0x07080607, 0x0A0B090A).AsByte(); - Vector128 lut = TBase64Encoder.Ssse3AdvSimdLut; + Vector128 lut = Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, TBase64Encoder.Ssse3AdvSimdLutE3).AsByte(); Vector128 maskAC = Vector128.Create(0x0fc0fc00).AsByte(); Vector128 maskBB = Vector128.Create(0x003f03f0).AsByte(); Vector128 shiftAC = Vector128.Create(0x04000040).AsUInt16(); @@ -692,19 +700,13 @@ internal static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8; - public static Vector256 Avx2Lut => Vector256.Create( - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -19, -16, 0, 0, - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -19, -16, 0, 0); + public static sbyte Avx2LutChar62 => -19; // char '+' diff + + public static sbyte Avx2LutChar63 => -16; // char '/' diff - public static Vector128 AdvSimdLut4 => Vector128.Create("wxyz0123456789+/"u8).AsByte(); + public static ReadOnlySpan AdvSimdLut4 => "wxyz0123456789+/"u8; - public static Vector128 Ssse3AdvSimdLut => Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, 0x0000F0ED).AsByte(); + public static uint Ssse3AdvSimdLutE3 => 0x0000F0ED; public static int IncrementPadTwo => 4; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index b84e58a8a63e64..bbb8ad574e6932 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -1,27 +1,96 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics; +using System.Text.Unicode; +using static System.Buffers.Text.Base64; + namespace System.Buffers.Text { public static partial class Base64Url { - /*// Decode from utf8 => bytes - public static OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => throw new NotImplementedException(); - public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => throw new NotImplementedException(); - /// /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "length". /// /// - /// Thrown when the specified is less than 0. + /// Thrown when the specified is less than 0. /// - public static int GetMaxDecodedFromUtf8Length(int length) => throw new NotImplementedException(); + public static int GetMaxDecodedLength(int base64Length) + { + if (base64Length < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + + int remainder = base64Length % 3; + + return (base64Length >> 2) * 3 + remainder > 0 ? remainder - 1 : 0; + } + + /// + /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// If the input is not a multiple of 4, it will decode as much as it can, to the closest multiple of 4. + /// + /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// (default) when the input span contains the entire data to encode. + /// Set to when the source buffer contains the entirety of the data to encode. + /// Set to if this method is being called in a loop and if more input data may follow. + /// At the end of the loop, call this (potentially with an empty source buffer) passing . + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input + /// - NeedMoreData - only if is false and the input is not a multiple of 4 + /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, + /// or if it contains invalid/more than two padding characters and is . + /// + public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => + Base64.DecodeFromUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + + private readonly struct Base64UrlDecoder : IBase64Decoder + { + // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) + public static ReadOnlySpan DecodingMap => + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, //62 is placed at index 45 (for -), 63 at index 95 (for _) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), TODO: 64 at index 61 (for =) - this is not there + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, //0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + public static Vector512 VbmiLookup0 => Vector512.Create( + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80803e80, + 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080).AsSByte(); + + public static Vector512 VbmiLookup1 => Vector512.Create( + 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, + 0x1211100f, 0x16151413, 0x80191817, 0x3f808080, + 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, + 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080).AsSByte(); + } - // IsValid - public static bool IsValid(ReadOnlySpan base64UrlText) => throw new NotImplementedException(); - public static bool IsValid(ReadOnlySpan base64UrlText, out int decodedLength) => throw new NotImplementedException(); - public static bool IsValid(ReadOnlySpan base64UrlTextUtf8) => throw new NotImplementedException(); - public static bool IsValid(ReadOnlySpan base64UrlTextUtf8, out int decodedLength) => throw new NotImplementedException(); + /*public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => throw new NotImplementedException(); // Up to this point, this is a mirror of System.Buffers.Text.Base64 // Below are more helpers that bring over functionality similar to Convert.*Base64* diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 20b2e1256bb995..e3393d7c20f41a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -258,20 +258,13 @@ public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLeng private readonly struct Base64UrlEncoder : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; + public static sbyte Avx2LutChar62 => -17; // char '-' diff - public static Vector256 Avx2Lut => Vector256.Create( - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -17, 32, 0, 0, - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - -17, 32, 0, 0); + public static sbyte Avx2LutChar63 => 32; // char '_' diff - public static Vector128 AdvSimdLut4 => Vector128.Create("wxyz0123456789-_"u8).AsByte(); + public static ReadOnlySpan AdvSimdLut4 => "wxyz0123456789-_"u8; - public static Vector128 Ssse3AdvSimdLut => Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, 0x000020EF).AsByte(); + public static uint Ssse3AdvSimdLutE3 => 0x000020EF; public static int IncrementPadTwo => 2; From 238b763992240b83a52e56ba6ebaf03a392411ae Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 6 May 2024 15:34:20 -0700 Subject: [PATCH 04/38] Add decoder implementation and unit tests --- .../System.Memory/ref/System.Memory.cs | 1 + .../tests/Base64/Base64DecoderUnitTests.cs | 15 - .../tests/Base64/Base64TestBase.cs | 14 + .../tests/Base64/Base64TestHelper.cs | 71 +- .../Base64Url/Base64UrlDecoderUnitTests.cs | 752 ++++++++++++++++++ ...rTests.cs => Base64UrlEncoderUnitTests.cs} | 42 +- ...s.cs => Base64UrlEncodingAPIsUnitTests.cs} | 6 +- .../tests/System.Memory.Tests.csproj | 5 +- .../src/System/Buffers/Text/Base64.cs | 33 +- .../src/System/Buffers/Text/Base64Decoder.cs | 279 +++++-- .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Text/Base64Url/Base64UrlDecoder.cs | 309 ++++++- .../Text/Base64Url/Base64UrlEncoder.cs | 3 +- 13 files changed, 1363 insertions(+), 169 deletions(-) create mode 100644 src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs rename src/libraries/System.Memory/tests/Base64Url/{Base64UrlEncoderTests.cs => Base64UrlEncoderUnitTests.cs} (84%) rename src/libraries/System.Memory/tests/Base64Url/{Base64UrlEncodingAPIsTests.cs => Base64UrlEncodingAPIsUnitTests.cs} (98%) diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 46cd070432ccd7..af4fa78727c461 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -643,6 +643,7 @@ public static class Base64Url { public static int GetMaxDecodedLength(int base64Length) { throw null; } public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static int DecodeFromUtf8InPlace(System.Span buffer) { throw null; } public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index bbd28fb092b016..b3aed300f21882 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -749,19 +748,5 @@ public void BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes(strin Assert.Equal(expectedWritten, decodedByteCount); Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); } - - public static IEnumerable BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes_MemberData() - { - var r = new Random(42); - for (int i = 0; i < 5; i++) - { - yield return new object[] { "AQ==" + new string(r.GetItems(" \n\t\r", i)), 4 + i, 1 }; - } - - foreach (string s in new[] { "MTIz", "M TIz", "MT Iz", "MTI z", "MTIz ", "M TI z", "M T I Z " }) - { - yield return new object[] { s + s + s + s, s.Length * 4, 12 }; - } - } } } diff --git a/src/libraries/System.Memory/tests/Base64/Base64TestBase.cs b/src/libraries/System.Memory/tests/Base64/Base64TestBase.cs index 882db3026722ea..828442602c7f22 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64TestBase.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64TestBase.cs @@ -107,5 +107,19 @@ public static IEnumerable StringsOnlyWithCharsToBeIgnored() string GetRepeatedChar(char charToInsert, int numberOfTimesToInsert) => new string(charToInsert, numberOfTimesToInsert); } + + public static IEnumerable BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes_MemberData() + { + var r = new Random(42); + for (int i = 0; i < 5; i++) + { + yield return new object[] { "AQ==" + new string(r.GetItems(" \n\t\r", i)), 4 + i, 1 }; + } + + foreach (string s in new[] { "MTIz", "M TIz", "MT Iz", "MTI z", "MTIz ", "M TI z", "M T I Z " }) + { + yield return new object[] { s + s + s + s, s.Length * 4, 12 }; + } + } } } diff --git a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs index 1ccc8e0cb42897..5ad537282c3f6b 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs @@ -24,12 +24,23 @@ public static class Base64TestHelper 52, 53, 54, 55, 56, 57, 43, 47 //4..9, +, / }; + public static readonly byte[] s_urlEncodingMap = { + 65, 66, 67, 68, 69, 70, 71, 72, //A..H + 73, 74, 75, 76, 77, 78, 79, 80, //I..P + 81, 82, 83, 84, 85, 86, 87, 88, //Q..X + 89, 90, 97, 98, 99, 100, 101, 102, //Y..Z, a..f + 103, 104, 105, 106, 107, 108, 109, 110, //g..n + 111, 112, 113, 114, 115, 116, 117, 118, //o..v + 119, 120, 121, 122, 48, 49, 50, 51, //w..z, 0..3 + 52, 53, 54, 55, 56, 57, 45, 95 //4..9, -, _ + }; + // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) public static readonly sbyte[] s_decodingMap = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, //62 is placed at index 43 (for +), 63 at index 47 (for /) - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, //0-25 are placed at index 65-90 (for A-Z) -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, @@ -44,6 +55,25 @@ public static class Base64TestHelper -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, }; + public static readonly sbyte[] s_urlDecodingMap = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, //62 is placed at index 45 (for -), 63 at index 95 (for _) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, //0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + public static bool IsByteToBeIgnored(byte charByte) => charByte is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n'; public const byte EncodingPad = (byte)'='; // '=', for padding @@ -60,6 +90,17 @@ public static byte[] InvalidBytes } } + public static byte[] UrlInvalidBytes + { + get + { + int[] indices = s_urlDecodingMap.FindAllIndexOf(InvalidByte); + // Workaround for indices.Cast().ToArray() since it throws + // InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Byte' + return indices.Select(i => (byte)i).ToArray(); + } + } + internal static void InitializeBytes(Span bytes, int seed = 100) { var rnd = new Random(seed); @@ -79,6 +120,16 @@ internal static void InitializeDecodableBytes(Span bytes, int seed = 100) } } + internal static void InitializeUrlDecodableBytes(Span bytes, int seed = 100) + { + var rnd = new Random(seed); + for (int i = 0; i < bytes.Length; i++) + { + int index = (byte)rnd.Next(0, s_urlEncodingMap.Length); + bytes[i] = s_urlEncodingMap[index]; + } + } + [Fact] public static void GenerateEncodingMapAndVerify() { @@ -117,11 +168,29 @@ public static bool VerifyEncodingCorrectness(int expectedConsumed, int expectedW return expectedText.Equals(encodedText); } + public static bool VerifyUrlEncodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span encodedBytes) + { + string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed).ToArray()) + .Replace('+', '-').Replace('/', '_').TrimEnd('='); + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + return expectedText.Equals(encodedText); + } + public static bool VerifyDecodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span decodedBytes) { string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed).ToArray()); byte[] expectedBytes = Convert.FromBase64String(sourceString); return expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, expectedWritten)); } + + public static bool VerifyUrlDecodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span decodedBytes) + { + string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed).ToArray()); + string padded = sourceString.Length % 4 == 0 ? sourceString : + sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); + string base64 = padded.Replace("_", "/").Replace("-", "+"); + byte[] expectedBytes = Convert.FromBase64String(base64); + return expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, expectedWritten)); + } } } diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs new file mode 100644 index 00000000000000..d3cf02f5f035b2 --- /dev/null +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -0,0 +1,752 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Xunit; + +namespace System.Buffers.Text.Tests +{ + public class Base64UrlDecoderUnitTests : Base64TestBase + { + [Fact] + public void BasicDecoding() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 1); // ensure we have a valid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(source.Length, consumed); + Assert.Equal(decodedBytes.Length, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(source.Length, decodedBytes.Length, source, decodedBytes)); + } + } + + [Fact] + public void BasicDecodingInvalidInputLength() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 != 1); // ensure we have a invalid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + int expectedConsumed = numBytes / 4 * 4; // decode input up to the closest multiple of 4 + int expectedDecoded = expectedConsumed / 4 * 3; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedDecoded, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedDecoded, source, decodedBytes)); + } + } + + [Fact] + public void BasicDecodingInvalidInputWithOneByteData() + { + // Only 1 byte of data is invalid, 2 - 3 bytes of data are valid as padding is optional + ReadOnlySpan source = stackalloc byte[] { (byte)'A' }; + Span decodedBytes = stackalloc byte[128]; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(0, consumed); + Assert.Equal(0, decodedByteCount); + } + + [Fact] + public void BasicDecodingWithFinalBlockFalse() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 != 0); // ensure we have a valid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + int expectedConsumed = source.Length / 4 * 4; // only consume closest multiple of four since isFinalBlock is false + + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(decodedBytes.Length, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + } + + [Fact] + public void BasicDecodingWithFinalBlockFalseInvalidInputLength() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 0); // ensure we have a invalid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + int expectedConsumed = source.Length / 4 * 4; // only consume closest multiple of four since isFinalBlock is false + int expectedDecoded = expectedConsumed / 4 * 3; + + Assert.Equal(OperationStatus.NeedMoreData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedDecoded, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedByteCount, source, decodedBytes)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeEmptySpan(bool isFinalBlock) + { + Span source = Span.Empty; + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + Assert.Equal(0, consumed); + Assert.Equal(0, decodedByteCount); + } + + [Fact] + public void DecodeGuid() + { + Span source = new byte[22]; // For Base64Url padding ignored + Span providedBytes = Guid.NewGuid().ToByteArray(); + Base64Url.EncodeToUtf8(providedBytes, source, out int _, out int _); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(22, consumed); + Assert.Equal(16, decodedByteCount); + Assert.True(providedBytes.SequenceEqual(decodedBytes)); + } + + [Fact] + public void DecodingOutputTooSmall() + { + for (int numBytes = 5; numBytes < 20; numBytes++) + { + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[3]; + int consumed, written; + if (numBytes >= 8) + { + Assert.True(OperationStatus.DestinationTooSmall == + Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written), "Number of Input Bytes: " + numBytes); + } + else + { + Assert.True(OperationStatus.InvalidData == + Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written), "Number of Input Bytes: " + numBytes); + } + int expectedConsumed = 4; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(decodedBytes.Length, written); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + + // Output too small even with padding characters in the input + { + Span source = new byte[12]; + Base64TestHelper.InitializeUrlDecodableBytes(source); + source[10] = Base64TestHelper.EncodingPad; + source[11] = Base64TestHelper.EncodingPad; + + Span decodedBytes = new byte[6]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int written)); + int expectedConsumed = 8; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(decodedBytes.Length, written); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + + { + Span source = new byte[12]; + Base64TestHelper.InitializeUrlDecodableBytes(source); + source[11] = Base64TestHelper.EncodingPad; + + Span decodedBytes = new byte[7]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int written)); + int expectedConsumed = 8; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(6, written); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, 6, source, decodedBytes)); + } + } + + [Fact] + public void DecodingOutputTooSmallWithFinalBlockFalse() + { + for (int numBytes = 8; numBytes < 20; numBytes++) + { + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[4]; + int consumed, written; + Assert.True(OperationStatus.DestinationTooSmall == + Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written, isFinalBlock: false), "Number of Input Bytes: " + numBytes); + int expectedConsumed = 4; + int expectedWritten = 3; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, written); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodingOutputTooSmallRetry(bool isFinalBlock) + { + Span source = new byte[1000]; + Base64TestHelper.InitializeUrlDecodableBytes(source); + + int outputSize = 240; + int requiredSize = Base64Url.GetMaxDecodedLength(source.Length); + + Span decodedBytes = new byte[outputSize]; + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + int expectedConsumed = decodedBytes.Length / 3 * 4; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(decodedBytes.Length, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + + decodedBytes = new byte[requiredSize - outputSize]; + source = source.Slice(consumed); + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out decodedByteCount, isFinalBlock)); + expectedConsumed = decodedBytes.Length / 3 * 4; + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(decodedBytes.Length, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + + [Theory] + [InlineData("AQ==", 1)] + [InlineData("AQI=", 2)] + [InlineData("AQID", 3)] + [InlineData("AQIDBA==", 4)] + [InlineData("AQIDBAU=", 5)] + [InlineData("AQIDBAUG", 6)] + public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, int expectedWritten) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + int expectedConsumed = inputString.Length; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + } + + [Theory] + [InlineData("A", 0, 0, OperationStatus.InvalidData)] + [InlineData("AQ", 2, 1, OperationStatus.Done)] + [InlineData("AQI", 3, 2, OperationStatus.Done)] + [InlineData("AQIDBA", 6, 4, OperationStatus.Done)] + [InlineData("AQIDBAU", 7, 5, OperationStatus.Done)] + public void BasicDecodingWithFinalBlockTrueInputWithoutPadding(string inputString, int expectedConsumed, int expectedWritten, OperationStatus expectedStatus) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); // expectedWritten == decodedBytes.Length + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + + [Theory] + [InlineData("\u00ecz_T", 0, 0)] // scalar code-path + [InlineData("z_Ta123\u00ec", 4, 3)] + [InlineData("\u00ecz_T-H7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path + [InlineData("z_T-H7sqEkerqMweH1uSw\u00ec==", 20, 15)] + [InlineData("\u00ecz_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u00ec==", 44, 33)] + public void BasicDecodingNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten) + { + Span source = Encoding.UTF8.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + } + + [Theory] + [InlineData("AQID", 3)] + [InlineData("AQIDBAUG", 6)] + public void BasicDecodingWithFinalBlockFalseKnownInputDone(string inputString, int expectedWritten) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + int expectedConsumed = inputString.Length; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); // expectedWritten == decodedBytes.Length + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + } + + [Theory] + [InlineData("A", 0, 0)] + [InlineData("AQ", 0, 0)] + [InlineData("AQI", 0, 0)] + [InlineData("AQIDB", 4, 3)] + [InlineData("AQIDBA", 4, 3)] + [InlineData("AQIDBAU", 4, 3)] + public void BasicDecodingWithFinalBlockFalseKnownInputNeedMoreData(string inputString, int expectedConsumed, int expectedWritten) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.NeedMoreData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); // expectedWritten == decodedBytes.Length + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedByteCount, source, decodedBytes)); + } + + [Theory] + [InlineData("AQ==", 0, 0)] + [InlineData("AQI=", 0, 0)] + [InlineData("AQIDBA==", 4, 3)] + [InlineData("AQIDBAU=", 4, 3)] + public void BasicDecodingWithFinalBlockFalseKnownInputInvalid(string inputString, int expectedConsumed, int expectedWritten) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock: false)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodingInvalidBytes(bool isFinalBlock) + { + // Invalid Bytes: + // 0-44 + // 46=47 + // 58-64 + // 91-94, 96 + // 123-255 + byte[] invalidBytes = Base64TestHelper.UrlInvalidBytes; + Assert.Equal(byte.MaxValue + 1 - 64, invalidBytes.Length); // 192 + + for (int j = 0; j < 8; j++) + { + Span source = "2222PPPP"u8.ToArray(); // valid input + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + for (int i = 0; i < invalidBytes.Length; i++) + { + // Don't test padding (byte 61 i.e. '='), which is tested in DecodingInvalidBytesPadding + // Don't test chars to be ignored (spaces: 9, 10, 13, 32 i.e. '\n', '\t', '\r', ' ') + if (invalidBytes[i] == Base64TestHelper.EncodingPad || + Base64TestHelper.IsByteToBeIgnored(invalidBytes[i])) + { + continue; + } + + // replace one byte with an invalid input + source[j] = invalidBytes[i]; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + + if (j < 4) + { + Assert.Equal(0, consumed); + Assert.Equal(0, decodedByteCount); + } + else + { + Assert.Equal(4, consumed); + Assert.Equal(3, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(4, 3, source, decodedBytes)); + } + } + } + + // Input that is not a multiple of 4 is not considered invalid, even if isFinalBlock = true + if (isFinalBlock) + { + Span source = "2222PPP"u8.ToArray(); // incomplete input + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(7, consumed); + Assert.Equal(5, decodedByteCount); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(7, 5, source, decodedBytes)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodingInvalidBytesPadding(bool isFinalBlock) + { + // Only last 2 bytes can be padding, all other occurrence of padding is invalid + for (int j = 0; j < 7; j++) + { + Span source = "2222PPPP"u8.ToArray(); // valid input + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + source[j] = Base64TestHelper.EncodingPad; + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + + if (j < 4) + { + Assert.Equal(0, consumed); + Assert.Equal(0, decodedByteCount); + } + else + { + Assert.Equal(4, consumed); + Assert.Equal(3, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(4, 3, source, decodedBytes)); + } + } + + // Invalid input with valid padding + { + Span source = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + source[6] = Base64TestHelper.EncodingPad; + source[7] = Base64TestHelper.EncodingPad; // invalid input - "2222P*==" + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + + Assert.Equal(4, consumed); + Assert.Equal(3, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(4, 3, source, decodedBytes)); + + source = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + source[7] = Base64TestHelper.EncodingPad; // invalid input - "2222PP**=" + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out decodedByteCount, isFinalBlock)); + + Assert.Equal(4, consumed); + Assert.Equal(3, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(4, 3, source, decodedBytes)); + } + + // The last byte or the last 2 bytes being the padding character is valid, if isFinalBlock = true + { + Span source = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + source[6] = Base64TestHelper.EncodingPad; + source[7] = Base64TestHelper.EncodingPad; // valid input - "2222PP==" + + OperationStatus expectedStatus = isFinalBlock ? OperationStatus.Done : OperationStatus.InvalidData; + int expectedConsumed = isFinalBlock ? source.Length : 4; + int expectedWritten = isFinalBlock ? 4 : 3; + + Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + + source = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; + decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + source[7] = Base64TestHelper.EncodingPad; // valid input - "2222PPP=" + + expectedConsumed = isFinalBlock ? source.Length : 4; + expectedWritten = isFinalBlock ? 5 : 3; + Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out decodedByteCount, isFinalBlock)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + } + } + + [Fact] + public void GetMaxDecodedLength() + { + Span sourceEmpty = Span.Empty; + Assert.Equal(0, Base64Url.GetMaxDecodedLength(0)); + + // int.MaxValue - (int.MaxValue % 4) => 2147483644, largest multiple of 4 less than int.MaxValue + int[] input = { 0, 4, 8, 12, 16, 20, 2000000000, 2147483640, 2147483644 }; + int[] expected = { 0, 3, 6, 9, 12, 15, 1500000000, 1610612730, 1610612733 }; + + for (int i = 0; i < input.Length; i++) + { + Assert.Equal(expected[i], Base64Url.GetMaxDecodedLength(input[i])); + } + + // Lengths that are not a multiple of 4. + int[] lengthsNotMultipleOfFour = { 1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15, 1001, 1002, 1003, 2147483645, 2147483646, 2147483647 }; + int[] expectedOutput = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 750, 751, 752, 1610612733, 1610612734, 1610612735 }; + for (int i = 0; i < lengthsNotMultipleOfFour.Length; i++) + { + Assert.Equal(expectedOutput[i], Base64Url.GetMaxDecodedLength(lengthsNotMultipleOfFour[i])); + } + + // negative input + Assert.Throws(() => Base64Url.GetMaxDecodedLength(-1)); + Assert.Throws(() => Base64Url.GetMaxDecodedLength(int.MinValue)); + } + + private static bool VerifyUrlDecodingCorrectness(string sourceString, Span decodedBytes) + { + string padded = sourceString.Length % 4 == 0 ? sourceString : + sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); + byte[] expectedBytes = Convert.FromBase64String(padded.Replace("_", "/").Replace("-", "+")); + return expectedBytes.AsSpan().SequenceEqual(decodedBytes); + } + + [Fact] + public void DecodeInPlace() + { + const int numberOfBytes = 15; + + for (int numberOfBytesToTest = 0; numberOfBytesToTest <= numberOfBytes; numberOfBytesToTest += 4) + { + Span testBytes = new byte[numberOfBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(testBytes); + string sourceString = Encoding.ASCII.GetString(testBytes.Slice(0, numberOfBytesToTest).ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(testBytes.Slice(0, numberOfBytesToTest)); + + Assert.Equal(Base64Url.GetMaxDecodedLength(numberOfBytesToTest), bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, testBytes.Slice(0, bytesWritten))); + } + } + + [Fact] + public void EncodeAndDecodeInPlace() + { + byte[] testBytes = new byte[256]; + for (int i = 0; i < 256; i++) + { + testBytes[i] = (byte)i; + } + + for (int value = 0; value < 256; value++) + { + Span sourceBytes = testBytes.AsSpan(0, value + 1); + Span buffer = new byte[Base64Url.GetEncodedLength(sourceBytes.Length)]; + + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(sourceBytes, buffer, out int consumed, out int written)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(consumed, written, sourceBytes, buffer)); + + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(sourceBytes.Length, bytesWritten); + Assert.True(sourceBytes.SequenceEqual(buffer.Slice(0, bytesWritten))); + } + } + + [Fact] + public void DecodeInPlaceInvalidBytes() + { + byte[] invalidBytes = Base64TestHelper.UrlInvalidBytes; + + for (int j = 0; j < 8; j++) + { + for (int i = 0; i < invalidBytes.Length; i++) + { + Span buffer = "2222PPPP"u8.ToArray(); // valid input + + // Don't test padding (byte 61 i.e. '='), which is tested in DecodeInPlaceInvalidBytesPadding + // Don't test chars to be ignored (spaces: 9, 10, 13, 32 i.e. '\n', '\t', '\r', ' ') + if (invalidBytes[i] == Base64TestHelper.EncodingPad || + Base64TestHelper.IsByteToBeIgnored(invalidBytes[i])) + { + continue; + } + + // replace one byte with an invalid input + buffer[j] = invalidBytes[i]; + string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + if (j < 4) + { + Assert.Equal(0, bytesWritten); + } + else + { + Assert.Equal(3, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } + } + } + + // Input that is not a multiple of 4 is valid for remainder 2-3, but invalid for 1 + { + Span buffer = "2222P"u8.ToArray(); // incomplete input + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(3, bytesWritten); // last byte is ignored + } + } + + [Fact] + public void DecodeInPlaceInvalidBytesPadding() + { + // Only last 2 bytes can be padding, all other occurrence of padding is invalid + for (int j = 0; j < 7; j++) + { + Span buffer = "2222PPPP"u8.ToArray(); // valid input + buffer[j] = Base64TestHelper.EncodingPad; + string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + if (j < 4) + { + Assert.Equal(0, bytesWritten); + } + else + { + Assert.Equal(3, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } + } + + // Invalid input with valid padding + { + Span buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + buffer[6] = Base64TestHelper.EncodingPad; + buffer[7] = Base64TestHelper.EncodingPad; // invalid input - "2222P*==" + string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(3, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } + + { + Span buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + buffer[7] = Base64TestHelper.EncodingPad; // invalid input - "2222P**=" + string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(3, bytesWritten); + Span expectedBytes = Convert.FromBase64String(sourceString); + Assert.True(expectedBytes.SequenceEqual(buffer.Slice(0, bytesWritten))); + } + + // The last byte or the last 2 bytes being the padding character is valid + { + Span buffer = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; + buffer[6] = Base64TestHelper.EncodingPad; + buffer[7] = Base64TestHelper.EncodingPad; // valid input - "2222PP==" + string sourceString = Encoding.ASCII.GetString(buffer.ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(4, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } + + { + Span buffer = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; + buffer[7] = Base64TestHelper.EncodingPad; // valid input - "2222PPP=" + string sourceString = Encoding.ASCII.GetString(buffer.ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(5, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } + } + + [Theory] + [MemberData(nameof(ValidBase64Strings_WithCharsThatMustBeIgnored))] + public void BasicDecodingIgnoresCharsToBeIgnoredAsConvertToBase64Does(string utf8WithCharsToBeIgnored, byte[] expectedBytes) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithCharsToBeIgnored); + byte[] resultBytes = new byte[5]; + OperationStatus result = Base64Url.DecodeFromUtf8(utf8BytesWithByteToBeIgnored, resultBytes, out int bytesConsumed, out int bytesWritten); + + // Control value from Convert.FromBase64String + byte[] stringBytes = Convert.FromBase64String(utf8WithCharsToBeIgnored); + + Assert.Equal(OperationStatus.Done, result); + Assert.Equal(utf8WithCharsToBeIgnored.Length, bytesConsumed); + Assert.Equal(expectedBytes.Length, bytesWritten); + Assert.True(expectedBytes.SequenceEqual(resultBytes)); + Assert.True(stringBytes.SequenceEqual(resultBytes)); + } + + [Theory] + [MemberData(nameof(ValidBase64Strings_WithCharsThatMustBeIgnored))] + public void DecodeInPlaceIgnoresCharsToBeIgnoredAsConvertToBase64Does(string utf8WithCharsToBeIgnored, byte[] expectedBytes) + { + Span utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithCharsToBeIgnored); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(utf8BytesWithByteToBeIgnored); + Span bytesOverwritten = utf8BytesWithByteToBeIgnored.Slice(0, bytesWritten); + byte[] resultBytesArray = bytesOverwritten.ToArray(); + + // Control value from Convert.FromBase64String + byte[] stringBytes = Convert.FromBase64String(utf8WithCharsToBeIgnored); + + Assert.Equal(expectedBytes.Length, bytesWritten); + Assert.True(expectedBytes.SequenceEqual(resultBytesArray)); + Assert.True(stringBytes.SequenceEqual(resultBytesArray)); + } + + [Theory] + [MemberData(nameof(StringsOnlyWithCharsToBeIgnored))] + public void BasicDecodingWithOnlyCharsToBeIgnored(string utf8WithCharsToBeIgnored) + { + byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithCharsToBeIgnored); + byte[] resultBytes = new byte[5]; + OperationStatus result = Base64Url.DecodeFromUtf8(utf8BytesWithByteToBeIgnored, resultBytes, out int bytesConsumed, out int bytesWritten); + + Assert.Equal(OperationStatus.Done, result); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [MemberData(nameof(StringsOnlyWithCharsToBeIgnored))] + public void DecodingInPlaceWithOnlyCharsToBeIgnored(string utf8WithCharsToBeIgnored) + { + Span utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithCharsToBeIgnored); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(utf8BytesWithByteToBeIgnored); + + Assert.Equal(0, bytesWritten); + } + + [Theory] + [MemberData(nameof(BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes_MemberData))] + public void BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes(string inputString, int expectedConsumed, int expectedWritten) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + } + } +} diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs similarity index 84% rename from src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs rename to src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs index 496e05ad19897f..78a13298bf7072 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs @@ -7,7 +7,7 @@ namespace System.Buffers.Text.Tests { - public class Base64UrlEncoderTests + public class Base64UrlEncoderUnitTests { [Fact] public void BasicEncodingAndDecoding() @@ -25,17 +25,15 @@ public void BasicEncodingAndDecoding() Assert.Equal(OperationStatus.Done, Base64Url.EncodeToUtf8(sourceBytes, encodedBytes, out int consumed, out int encodedBytesCount)); Assert.Equal(sourceBytes.Length, consumed); Assert.Equal(encodedBytes.Length, encodedBytesCount); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(sourceBytes.Length, encodedBytes.Length, sourceBytes, encodedBytes)); - string encodedText = Encoding.ASCII.GetString(encodedBytes.ToArray()); - string expectedText = Convert.ToBase64String(bytes, 0, value + 1).Replace('+', '-').Replace('/', '_').TrimEnd('='); - Assert.Equal(expectedText, encodedText); - - /*TODO Assert.Equal(0, encodedBytes.Length % 4); - Span decodedBytes = new byte[Base64.GetMaxDecodedFromUtf8Length(encodedBytes.Length)]; - Assert.Equal(OperationStatus.Done, Base64.DecodeFromUtf8(encodedBytes, decodedBytes, out consumed, out int decodedByteCount)); + int decodedLength = Base64Url.GetMaxDecodedLength(encodedBytes.Length); + Assert.True(sourceBytes.Length <= decodedLength); + Span decodedBytes = new byte[decodedLength]; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(encodedBytes, decodedBytes, out consumed, out int decodedByteCount)); Assert.Equal(encodedBytes.Length, consumed); Assert.Equal(sourceBytes.Length, decodedByteCount); - Assert.True(sourceBytes.SequenceEqual(decodedBytes.Slice(0, decodedByteCount)));*/ + Assert.True(sourceBytes.SequenceEqual(decodedBytes.Slice(0, decodedByteCount))); } } @@ -54,17 +52,10 @@ public void BasicEncoding() Assert.Equal(OperationStatus.Done, result); Assert.Equal(source.Length, consumed); Assert.Equal(encodedBytes.Length, encodedBytesCount); - Assert.True(VerifyEncodingCorrectness(source.Length, encodedBytes.Length, source, encodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(source.Length, encodedBytes.Length, source, encodedBytes)); } } - private static bool VerifyEncodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span encodedBytes) - { - string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed).ToArray()).Replace('+', '-').Replace('/', '_').TrimEnd('='); - string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); - return expectedText.Equals(encodedText); - } - [Fact] public void BasicEncodingWithFinalBlockFalse() { @@ -84,7 +75,7 @@ public void BasicEncodingWithFinalBlockFalse() Assert.Equal(expectedStatus, Base64Url.EncodeToUtf8(source, encodedBytes, out int consumed, out int encodedBytesCount, isFinalBlock: false)); Assert.Equal(expectedConsumed, consumed); Assert.Equal(expectedWritten, encodedBytesCount); - Assert.True(VerifyEncodingCorrectness(expectedConsumed, expectedWritten, source, encodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(expectedConsumed, expectedWritten, source, encodedBytes)); } } @@ -170,7 +161,7 @@ public void EncodingOutputTooSmall(bool isFinalBlock) int expectedConsumed = 3; Assert.Equal(expectedConsumed, consumed); Assert.Equal(encodedBytes.Length, written); - Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); } } @@ -190,7 +181,7 @@ public void EncodingOutputTooSmallRetry(bool isFinalBlock) int expectedConsumed = encodedBytes.Length / 4 * 3; Assert.Equal(expectedConsumed, consumed); Assert.Equal(encodedBytes.Length, written); - Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); encodedBytes = new byte[requiredSize - outputSize]; source = source.Slice(consumed); @@ -198,7 +189,7 @@ public void EncodingOutputTooSmallRetry(bool isFinalBlock) expectedConsumed = encodedBytes.Length / 4 * 3; Assert.Equal(expectedConsumed, consumed); Assert.Equal(encodedBytes.Length, written); - Assert.True(VerifyEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlEncodingCorrectness(expectedConsumed, encodedBytes.Length, source, encodedBytes)); } [Theory] @@ -276,12 +267,9 @@ public void TryEncodeInPlace() for (int numberOfBytesToTest = 0; numberOfBytesToTest <= numberOfBytes; numberOfBytesToTest++) { - Span sliced = testBytes.Slice(0, numberOfBytesToTest); - Span dest = new byte[Base64Url.GetEncodedLength(numberOfBytesToTest)]; - //var expectedText = Convert.ToBase64String(testBytes.Slice(0, numberOfBytesToTest).ToArray()).Replace('+', '-').Replace('/', '_').TrimEnd('='); - var status = Base64Url.EncodeToUtf8(sliced, dest, out _, out int _); - Assert.Equal(OperationStatus.Done, status); - var expectedText = Encoding.ASCII.GetString(dest.ToArray()); + var expectedText = Convert.ToBase64String(testBytes.Slice(0, numberOfBytesToTest).ToArray()) + .Replace('+', '-').Replace('/', '_').TrimEnd('='); + Assert.True(Base64Url.TryEncodeToUtf8InPlace(testBytes, numberOfBytesToTest, out int bytesWritten)); Assert.Equal(Base64Url.GetEncodedLength(numberOfBytesToTest), bytesWritten); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs similarity index 98% rename from src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs rename to src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs index de0f68768ee43f..501a4df391f814 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs @@ -5,9 +5,9 @@ namespace System.Buffers.Text.Tests { - public class Base64UrlEncodingAPIsTests + public class Base64UrlEncodingAPIsUnitTests { - [Fact] + /*[Fact] public static void EncodeToChars() { string input = "test"; @@ -17,7 +17,7 @@ public static void EncodeToChars() Assert.Equal(OperationStatus.Done, operationStatus); Assert.Equal(4, bytesConsumed); Assert.Equal(3, charsWritten); - } + }*/ [Fact] public static void ShortInputArray() diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index e06d6d445bd05f..b240ad67413eca 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -13,8 +13,9 @@ - - + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index 4a96db04bd9161..3b0e7476e36acd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -54,8 +54,37 @@ internal interface IBase64Encoder internal interface IBase64Decoder { static abstract ReadOnlySpan DecodingMap { get; } - static abstract Vector512 VbmiLookup0 { get; } - static abstract Vector512 VbmiLookup1 { get; } + static abstract ReadOnlySpan VbmiLookup0 { get; } + static abstract ReadOnlySpan VbmiLookup1 { get; } + static abstract ReadOnlySpan Avx2LutHigh { get; } + static abstract ReadOnlySpan Avx2LutLow { get; } + static abstract ReadOnlySpan Avx2LutShift { get; } + static abstract byte MaskSlashOrUnderscore { get; } + static abstract ReadOnlySpan Vector128LutHigh { get; } + static abstract ReadOnlySpan Vector128LutLow { get; } + static abstract ReadOnlySpan Vector128LutShift { get; } + static abstract int SrcLength(bool isFinalBlock, int utf8Length); + static abstract int GetMaxDecodedLength(int utf8Length); + static abstract bool TryDecode128Core( + Vector128 str, + Vector128 hiNibbles, + Vector128 maskSlashOrUnderscore, + Vector128 mask8F, + Vector128 lutLow, + Vector128 lutHigh, + Vector128 lutShift, + Vector128 shiftForUnderscore, + out Vector128 result); + static abstract bool TryDecode256Core( + Vector256 str, + Vector256 hiNibbles, + Vector256 maskSlashOrUnderscore, + Vector256 lutLow, + Vector256 lutHigh, + Vector256 lutShift, + Vector256 shiftForUnderscore, + out Vector256 result); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 9356f8629513ed..1d09302ac9dbf5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -50,10 +50,10 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp fixed (byte* srcBytes = &MemoryMarshal.GetReference(utf8)) fixed (byte* destBytes = &MemoryMarshal.GetReference(bytes)) { - int srcLength = utf8.Length & ~0x3; // only decode input up to the closest multiple of 4. + int srcLength = TBase64Decoder.SrcLength(isFinalBlock, utf8.Length); int destLength = bytes.Length; int maxSrcLength = srcLength; - int decodedLength = GetMaxDecodedFromUtf8Length(srcLength); + int decodedLength = TBase64Decoder.GetMaxDecodedLength(srcLength); // max. 2 padding chars if (destLength < decodedLength - 2) @@ -83,7 +83,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp end = srcMax - 45; if (Avx2.IsSupported && (end >= src)) { - Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -94,7 +94,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp end = srcMax - 24; if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) { - Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -117,6 +117,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp // Therefore, (destLength / 3) * 4 will always be less than 2147483641 Debug.Assert(destLength < (int.MaxValue / 4 * 3)); maxSrcLength = (destLength / 3) * 4; + srcLength &= ~0x3; // Round down to multiple of 4, this only affect Base64UrlDecoder path } ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); @@ -160,11 +161,35 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp // if isFinalBlock is false, we will never reach this point - // Handle last four bytes. There are 0, 1, 2 padding chars. - uint t0 = srcEnd[-4]; - uint t1 = srcEnd[-3]; - uint t2 = srcEnd[-2]; - uint t3 = srcEnd[-1]; + uint t0; + uint t1; + uint t2; + uint t3; + // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. + long remaining = srcEnd - src; + switch (remaining) + { + case 2: + t0 = srcEnd[-2]; + t1 = srcEnd[-1]; + t2 = EncodingPad; + t3 = EncodingPad; + break; + case 3: + t0 = srcEnd[-3]; + t1 = srcEnd[-2]; + t2 = srcEnd[-1]; + t3 = EncodingPad; + break; + case 4: + t0 = srcEnd[-4]; + t1 = srcEnd[-3]; + t2 = srcEnd[-2]; + t3 = srcEnd[-1]; + break; + default: + goto InvalidDataExit; + } int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); @@ -197,6 +222,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp WriteThreeLowOrderBytes(dest, i0); dest += 3; + src += 4; } else if (t2 != EncodingPad) { @@ -218,6 +244,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp dest[0] = (byte)(i0 >> 16); dest[1] = (byte)(i0 >> 8); dest += 2; + src += remaining; } else { @@ -232,10 +259,9 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp dest[0] = (byte)(i0 >> 16); dest += 1; + src += remaining; } - src += 4; - if (srcLength != utf8.Length) { goto InvalidDataExit; @@ -461,7 +487,7 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span } } - private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; @@ -560,7 +586,7 @@ private static int GetPaddingCount(ref byte ptrToLastElement) return padding; } - private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) + internal static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; @@ -646,6 +672,9 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref byte* src = srcBytes; byte* dest = destBytes; + // The JIT won't hoist these "constants", so help it + Vector512 vbmiLookup0 = Vector512.Create(TBase64Decoder.VbmiLookup0).AsSByte(); + Vector512 vbmiLookup1 = Vector512.Create(TBase64Decoder.VbmiLookup1).AsSByte(); Vector512 vbmiPackedLanesControl = Vector512.Create( 0x06000102, 0x090a0405, 0x0c0d0e08, 0x16101112, 0x191a1415, 0x1c1d1e18, 0x26202122, 0x292a2425, @@ -666,7 +695,7 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref // This step also checks for invalid inputs and exits. // After this, we have indices which are verified to have upper 2 bits set to 0 in each byte. // origIndex = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] - Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(TBase64Decoder.VbmiLookup0, str, TBase64Decoder.VbmiLookup1); + Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(vbmiLookup0, str, vbmiLookup1); Vector512 errorVec = (origIndex.AsInt32() | str.AsInt32()).AsSByte(); if (errorVec.ExtractMostSignificantBits() != 0) { @@ -693,10 +722,10 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref destBytes = dest; } - // TODO [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder { // If we have AVX2 support, pick off 32 bytes at a time for as long as we can, // but make sure that we quit before seeing any == markers at the end of the @@ -707,35 +736,11 @@ private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, b // See SSSE3-version below for an explanation of how the code works. // The JIT won't hoist these "constants", so help it - Vector256 lutHi = Vector256.Create( - 0x10, 0x10, 0x01, 0x02, - 0x04, 0x08, 0x04, 0x08, - 0x10, 0x10, 0x10, 0x10, - 0x10, 0x10, 0x10, 0x10, - 0x10, 0x10, 0x01, 0x02, - 0x04, 0x08, 0x04, 0x08, - 0x10, 0x10, 0x10, 0x10, - 0x10, 0x10, 0x10, 0x10); - - Vector256 lutLo = Vector256.Create( - 0x15, 0x11, 0x11, 0x11, - 0x11, 0x11, 0x11, 0x11, - 0x11, 0x11, 0x13, 0x1A, - 0x1B, 0x1B, 0x1B, 0x1A, - 0x15, 0x11, 0x11, 0x11, - 0x11, 0x11, 0x11, 0x11, - 0x11, 0x11, 0x13, 0x1A, - 0x1B, 0x1B, 0x1B, 0x1A); - - Vector256 lutShift = Vector256.Create( - 0, 16, 19, 4, - -65, -65, -71, -71, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 16, 19, 4, - -65, -65, -71, -71, - 0, 0, 0, 0, - 0, 0, 0, 0); + Vector256 lutHi = Vector256.Create(TBase64Decoder.Avx2LutHigh); + + Vector256 lutLo = Vector256.Create(TBase64Decoder.Avx2LutLow); + + Vector256 lutShift = Vector256.Create(TBase64Decoder.Avx2LutShift); Vector256 packBytesInLaneMask = Vector256.Create( 2, 1, 0, 6, @@ -757,7 +762,8 @@ private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, b -1, -1, -1, -1, -1, -1, -1, -1).AsInt32(); - Vector256 mask2F = Vector256.Create((sbyte)'/'); + Vector256 maskSlashOrUnderscore = Vector256.Create((sbyte)TBase64Decoder.MaskSlashOrUnderscore); + Vector256 shiftForUnderscore = Vector256.Create((sbyte)33); Vector256 mergeConstant0 = Vector256.Create(0x01400140).AsSByte(); Vector256 mergeConstant1 = Vector256.Create(0x00011000).AsInt16(); @@ -770,20 +776,13 @@ private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, b AssertRead>(src, srcStart, sourceLength); Vector256 str = Avx.LoadVector256(src).AsSByte(); - Vector256 hiNibbles = Avx2.And(Avx2.ShiftRightLogical(str.AsInt32(), 4).AsSByte(), mask2F); - Vector256 loNibbles = Avx2.And(str, mask2F); - Vector256 hi = Avx2.Shuffle(lutHi, hiNibbles); - Vector256 lo = Avx2.Shuffle(lutLo, loNibbles); + Vector256 hiNibbles = Avx2.And(Avx2.ShiftRightLogical(str.AsInt32(), 4).AsSByte(), maskSlashOrUnderscore); - if (!Avx.TestZ(lo, hi)) + if (!TBase64Decoder.TryDecode256Core(str, hiNibbles, maskSlashOrUnderscore, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) { break; } - Vector256 eq2F = Avx2.CompareEqual(str, mask2F); - Vector256 shift = Avx2.Shuffle(lutShift, Avx2.Add(eq2F, hiNibbles)); - str = Avx2.Add(str, shift); - // in, lower lane, bits, upper case are most significant bits, lower case are least significant bits: // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG @@ -842,7 +841,8 @@ internal static Vector128 SimdShuffle(Vector128 left, Vector128(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder { Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian); @@ -919,16 +919,16 @@ private static unsafe void Vector128Decode(ref byte* srcBytes, ref byte* destByt // 1111 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 // The JIT won't hoist these "constants", so help it - Vector128 lutHi = Vector128.Create(0x02011010, 0x08040804, 0x10101010, 0x10101010).AsByte(); - Vector128 lutLo = Vector128.Create(0x11111115, 0x11111111, 0x1A131111, 0x1A1B1B1B).AsByte(); - Vector128 lutShift = Vector128.Create(0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000).AsSByte(); + Vector128 lutHi = Vector128.Create(TBase64Decoder.Vector128LutHigh).AsByte(); + Vector128 lutLo = Vector128.Create(TBase64Decoder.Vector128LutLow).AsByte(); + Vector128 lutShift = Vector128.Create(TBase64Decoder.Vector128LutShift).AsSByte(); Vector128 packBytesMask = Vector128.Create(0x06000102, 0x090A0405, 0x0C0D0E08, 0xffffffff).AsSByte(); Vector128 mergeConstant0 = Vector128.Create(0x01400140).AsByte(); Vector128 mergeConstant1 = Vector128.Create(0x00011000).AsInt16(); Vector128 one = Vector128.Create((byte)1); - Vector128 mask2F = Vector128.Create((byte)'/'); + Vector128 mask2F = Vector128.Create(TBase64Decoder.MaskSlashOrUnderscore); Vector128 mask8F = Vector128.Create((byte)0x8F); - + Vector128 shiftForUnderscore = Vector128.Create((byte)33); byte* src = srcBytes; byte* dest = destBytes; @@ -940,23 +940,12 @@ private static unsafe void Vector128Decode(ref byte* srcBytes, ref byte* destByt // lookup Vector128 hiNibbles = Vector128.ShiftRightLogical(str.AsInt32(), 4).AsByte() & mask2F; - Vector128 loNibbles = str & mask2F; - Vector128 hi = SimdShuffle(lutHi, hiNibbles, mask8F); - Vector128 lo = SimdShuffle(lutLo, loNibbles, mask8F); - // Check for invalid input: if any "and" values from lo and hi are not zero, - // fall back on bytewise code to do error checking and reporting: - if ((lo & hi) != Vector128.Zero) + if (!TBase64Decoder.TryDecode128Core(str, hiNibbles, mask2F, mask8F, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) { break; } - Vector128 eq2F = Vector128.Equals(str, mask2F); - Vector128 shift = SimdShuffle(lutShift.AsByte(), (eq2F + hiNibbles), mask8F); - - // Now simply add the delta values to the input: - str += shift; - // in, bits, upper case are most significant bits, lower case are least significant bits // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG @@ -1015,7 +1004,7 @@ private static unsafe void Vector128Decode(ref byte* srcBytes, ref byte* destByt } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) + internal static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) { uint t0 = encodedBytes[0]; uint t1 = encodedBytes[1]; @@ -1039,7 +1028,7 @@ private static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void WriteThreeLowOrderBytes(byte* destination, int value) + internal static unsafe void WriteThreeLowOrderBytes(byte* destination, int value) { destination[0] = (byte)(value >> 16); destination[1] = (byte)(value >> 8); @@ -1091,7 +1080,7 @@ internal static bool IsWhiteSpace(int value) -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, //62 is placed at index 43 (for +), 63 at index 47 (for /) - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, //0-25 are placed at index 65-90 (for A-Z) -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, @@ -1106,17 +1095,133 @@ internal static bool IsWhiteSpace(int value) -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ]; - public static Vector512 VbmiLookup0 => Vector512.Create( - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x3e808080, 0x3f808080, - 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080).AsSByte(); - - public static Vector512 VbmiLookup1 => Vector512.Create( - 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, - 0x1211100f, 0x16151413, 0x80191817, 0x80808080, - 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, - 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080).AsSByte(); + public static ReadOnlySpan VbmiLookup0 => + [ + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x3e808080, 0x3f808080, + 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080 + ]; + + public static ReadOnlySpan VbmiLookup1 => + [ + 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, + 0x1211100f, 0x16151413, 0x80191817, 0x80808080, + 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, + 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080 + ]; + + public static ReadOnlySpan Avx2LutHigh => + [ + 0x10, 0x10, 0x01, 0x02, + 0x04, 0x08, 0x04, 0x08, + 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x01, 0x02, + 0x04, 0x08, 0x04, 0x08, + 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10 + ]; + + public static ReadOnlySpan Avx2LutLow => + [ + 0x15, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x13, 0x1A, + 0x1B, 0x1B, 0x1B, 0x1A, + 0x15, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x13, 0x1A, + 0x1B, 0x1B, 0x1B, 0x1A + ]; + + public static ReadOnlySpan Avx2LutShift => + [ + 0, 16, 19, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 16, 19, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0 + ]; + + public static byte MaskSlashOrUnderscore => (byte)'/'; + + public static ReadOnlySpan Vector128LutHigh => [0x02011010, 0x08040804, 0x10101010, 0x10101010]; + + public static ReadOnlySpan Vector128LutLow => [0x11111115, 0x11111111, 0x1A131111, 0x1A1B1B1B]; + + public static ReadOnlySpan Vector128LutShift => [0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + + public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); + public static int SrcLength(bool _, int utf8Length) => utf8Length & ~0x3; // only decode input up to the closest multiple of 4. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(Ssse3))] + public static bool TryDecode128Core( + Vector128 str, + Vector128 hiNibbles, + Vector128 maskSlashOrUnderscore, + Vector128 mask8F, + Vector128 lutLow, + Vector128 lutHigh, + Vector128 lutShift, + Vector128 _, + out Vector128 result) + { + Vector128 loNibbles = str & maskSlashOrUnderscore; + Vector128 hi = SimdShuffle(lutHigh, hiNibbles, mask8F); + Vector128 lo = SimdShuffle(lutLow, loNibbles, mask8F); + + // Check for invalid input: if any "and" values from lo and hi are not zero, + // fall back on bytewise code to do error checking and reporting: + if ((lo & hi) != Vector128.Zero) + { + result = default; + return false; + } + + Vector128 eq2F = Vector128.Equals(str, maskSlashOrUnderscore); + Vector128 shift = SimdShuffle(lutShift.AsByte(), (eq2F + hiNibbles), mask8F); + + // Now simply add the delta values to the input: + result = str + shift; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static bool TryDecode256Core( + Vector256 str, + Vector256 hiNibbles, + Vector256 maskSlashOrUnderscore, + Vector256 lutLow, + Vector256 lutHigh, + Vector256 lutShift, + Vector256 _, + out Vector256 result) + { + Vector256 loNibbles = Avx2.And(str, maskSlashOrUnderscore); + Vector256 hi = Avx2.Shuffle(lutHigh, hiNibbles); + Vector256 lo = Avx2.Shuffle(lutLow, loNibbles); + + if (!Avx.TestZ(lo, hi)) + { + result = default; + return false; + } + + Vector256 eq2F = Avx2.CompareEqual(str, maskSlashOrUnderscore); + Vector256 shift = Avx2.Shuffle(lutShift, Avx2.Add(eq2F, hiNibbles)); + + result = Avx2.Add(str, shift); + + return true; + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index fa02ad39935288..7ebf8d4dacfaaa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -61,7 +61,7 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan if (maxSrcLength >= 16) { - byte* end = srcMax - 48; + byte* end = srcMax - 64; if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) { Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index bbb8ad574e6932..e71a8be6f48781 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -4,14 +4,16 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; -using System.Runtime.Intrinsics; using System.Text.Unicode; using static System.Buffers.Text.Base64; namespace System.Buffers.Text { + // AVX2 and Vector128 version based on https://github.com/gfoidl/Base64/blob/master/source/gfoidl.Base64/Internal/Encodings/Base64UrlEncoding.cs + public static partial class Base64Url { /// @@ -27,9 +29,9 @@ public static int GetMaxDecodedLength(int base64Length) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); } - int remainder = base64Length % 3; + int remainder = base64Length % 4; - return (base64Length >> 2) * 3 + remainder > 0 ? remainder - 1 : 0; + return (base64Length >> 2) * 3 + (remainder > 0 ? remainder - 1 : 0); } /// @@ -52,17 +54,159 @@ public static int GetMaxDecodedLength(int base64Length) /// or if it contains invalid/more than two padding characters and is . /// public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - Base64.DecodeFromUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + DecodeFromUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + + /// + /// Decode the span of UTF-8 encoded text in Base64Url (in-place) into binary data. + /// The decoded binary output is smaller than the text data contained in the input (the operation deflates the data). + /// TODO: If the input is not a multiple of 4, it will not decode any. + /// + /// The input span which contains the base 64 text data that needs to be decoded. + /// TODO: It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - InvalidData - if the input contains bytes outside of the expected base 64 range, or if it contains invalid/more than two padding characters. + /// It does not return DestinationTooSmall since that is not possible for base 64 decoding. + /// It does not return NeedMoreData since this method tramples the data in the buffer and + /// hence can only be called once with all the data in the buffer. + /// + public static int DecodeFromUtf8InPlace(Span buffer) + { + DecodeFromUtf8InPlace(buffer, out int bytesWritten, ignoreWhiteSpace: true); + return bytesWritten; + } + + private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) + where TBase64Decoder : IBase64Decoder + { + if (buffer.IsEmpty) + { + bytesWritten = 0; + return OperationStatus.Done; + } + + fixed (byte* bufferBytes = &MemoryMarshal.GetReference(buffer)) + { + uint bufferLength = (uint)buffer.Length; + uint sourceIndex = 0; + uint destIndex = 0; + + ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); + + while (sourceIndex + 4 < bufferLength) + { + int result = Decode(bufferBytes + sourceIndex, ref decodingMap); + if (result < 0) + { + goto InvalidExit; + } + + WriteThreeLowOrderBytes(bufferBytes + destIndex, result); + destIndex += 3; + sourceIndex += 4; + } + + uint t0; + uint t1; + uint t2; + uint t3; + + switch (bufferLength - sourceIndex) + { + case 2: + t0 = bufferBytes[bufferLength - 2]; + t1 = bufferBytes[bufferLength - 1]; + t2 = EncodingPad; + t3 = EncodingPad; + break; + case 3: + t0 = bufferBytes[bufferLength - 3]; + t1 = bufferBytes[bufferLength - 2]; + t2 = bufferBytes[bufferLength - 1]; + t3 = EncodingPad; + break; + case 4: + t0 = bufferBytes[bufferLength - 4]; + t1 = bufferBytes[bufferLength - 3]; + t2 = bufferBytes[bufferLength - 2]; + t3 = bufferBytes[bufferLength - 1]; + break; + default: + goto InvalidExit; + } + + int i0 = Unsafe.Add(ref decodingMap, t0); + int i1 = Unsafe.Add(ref decodingMap, t1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + + if (t3 != EncodingPad) + { + int i2 = Unsafe.Add(ref decodingMap, t2); + int i3 = Unsafe.Add(ref decodingMap, t3); + + i2 <<= 6; + + i0 |= i3; + i0 |= i2; + + if (i0 < 0) + { + goto InvalidExit; + } + + WriteThreeLowOrderBytes(bufferBytes + destIndex, i0); + destIndex += 3; + } + else if (t2 != EncodingPad) + { + int i2 = Unsafe.Add(ref decodingMap, t2); + + i2 <<= 6; + + i0 |= i2; + + if (i0 < 0) + { + goto InvalidExit; + } + + bufferBytes[destIndex] = (byte)(i0 >> 16); + bufferBytes[destIndex + 1] = (byte)(i0 >> 8); + destIndex += 2; + } + else + { + if (i0 < 0) + { + goto InvalidExit; + } + + bufferBytes[destIndex] = (byte)(i0 >> 16); + destIndex += 1; + } + + bytesWritten = (int)destIndex; + return OperationStatus.Done; + + InvalidExit: + bytesWritten = (int)destIndex; + return ignoreWhiteSpace ? + DecodeWithWhiteSpaceFromUtf8InPlace(buffer, ref bytesWritten, sourceIndex) : // The input may have whitespace, attempt to decode while ignoring whitespace. + OperationStatus.InvalidData; + } + } private readonly struct Base64UrlDecoder : IBase64Decoder { - // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) public static ReadOnlySpan DecodingMap => [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, //62 is placed at index 45 (for -), 63 at index 95 (for _) - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9), TODO: 64 at index 61 (for =) - this is not there + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, //0-25 are placed at index 65-90 (for A-Z) -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, @@ -77,35 +221,140 @@ public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span VbmiLookup0 => Vector512.Create( - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80803e80, - 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080).AsSByte(); - - public static Vector512 VbmiLookup1 => Vector512.Create( - 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, - 0x1211100f, 0x16151413, 0x80191817, 0x3f808080, - 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, - 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080).AsSByte(); - } + public static ReadOnlySpan VbmiLookup0 => + [ + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80803e80, + 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080 + ]; + + public static ReadOnlySpan VbmiLookup1 => + [ + 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, + 0x1211100f, 0x16151413, 0x80191817, 0x3f808080, + 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, + 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080 + ]; + + public static ReadOnlySpan Avx2LutHigh => + [ + 0x00, 0x00, 0x2d, 0x39, + 0x4f, 0x5a, 0x6f, 0x7a, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2d, 0x39, + 0x4f, 0x5a, 0x6f, 0x7a, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]; + + public static ReadOnlySpan Avx2LutLow => + [ + 0x01, 0x01, 0x2d, 0x30, + 0x41, 0x50, 0x61, 0x70, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x2d, 0x30, + 0x41, 0x50, 0x61, 0x70, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01 + ]; + + public static ReadOnlySpan Avx2LutShift => + [ + 0, 0, 17, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 17, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0 + ]; + + public static byte MaskSlashOrUnderscore => (byte)'_'; // underscore + + public static ReadOnlySpan Vector128LutHigh => [ 0x392d0000, 0x7a6f5a4f, 0x00000000, 0x00000000 ]; - /*public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => throw new NotImplementedException(); + public static ReadOnlySpan Vector128LutLow => [0x302d0101, 0x70615041, 0x01010101, 0x01010101]; - // Up to this point, this is a mirror of System.Buffers.Text.Base64 - // Below are more helpers that bring over functionality similar to Convert.*Base64* + public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; - // Encode to / decode from chars - public static bool TryDecodeFromChars(ReadOnlySpan chars, Span bytes, out int bytesWritten) => throw new NotImplementedException(); + public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); + public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(Ssse3))] + public static bool TryDecode128Core( + Vector128 str, + Vector128 hiNibbles, + Vector128 maskSlashOrUnderscore, + Vector128 mask8F, + Vector128 lutLow, + Vector128 lutHigh, + Vector128 lutShift, + Vector128 shiftForUnderscore, + out Vector128 result) + { + Vector128 lowerBound = SimdShuffle(lutLow, hiNibbles, mask8F); + Vector128 upperBound = SimdShuffle(lutHigh, hiNibbles, mask8F); + Vector128 below = Vector128.LessThan(str, lowerBound); + Vector128 above = Vector128.GreaterThan(str, upperBound); + Vector128 eq5F = Vector128.Equals(str, maskSlashOrUnderscore); - // These are just accelerator methods. - // Should be efficiently implementable on top of the other ones in just a few lines. + // Take care as arguments are flipped in order! + Vector128 outside = Vector128.AndNot(below | above, eq5F); + + if (outside != Vector128.Zero) + { + result = default; + return false; + } + + Vector128 shift = SimdShuffle(lutShift.AsByte(), hiNibbles, mask8F); + str += shift; + + result = str + (eq5F & shiftForUnderscore); + return true; + } - // Decode from chars => string - // Decode from chars => byte[] - // The names could also just be "Decode" without naming the return type - public static string DecodeToString(ReadOnlySpan chars, Encoding encoding) => throw new NotImplementedException(); - public static byte[] DecodeToByteArray(ReadOnlySpan chars) => throw new NotImplementedException();*/ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static bool TryDecode256Core( + Vector256 str, + Vector256 hiNibbles, + Vector256 maskSlashOrUnderscore, + Vector256 lutLow, + Vector256 lutHigh, + Vector256 lutShift, + Vector256 shiftForUnderscore, + out Vector256 result) + { + Vector256 lowerBound = Avx2.Shuffle(lutLow, hiNibbles); + Vector256 upperBound = Avx2.Shuffle(lutHigh, hiNibbles); + + Vector256 below = Vector256.LessThan(str, lowerBound); + Vector256 above = Vector256.GreaterThan(str, upperBound); + Vector256 eq5F = Vector256.Equals(str, maskSlashOrUnderscore); + + // Take care as arguments are flipped in order! + Vector256 outside = Vector256.AndNot(below | above, eq5F); + + if (outside != Vector256.Zero) + { + result = default; + return false; + } + + Vector256 shift = Avx2.Shuffle(lutShift, hiNibbles); + str += shift; + + result = str + (eq5F & shiftForUnderscore); + return true; + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index e3393d7c20f41a..c31a1ba67a2df6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -44,7 +44,7 @@ public static int GetEncodedLength(int bytesLength) int remainder = bytesLength % 3; - return bytesLength / 3 * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. + return (bytesLength / 3) * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. } /// @@ -258,6 +258,7 @@ public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLeng private readonly struct Base64UrlEncoder : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; + public static sbyte Avx2LutChar62 => -17; // char '-' diff public static sbyte Avx2LutChar63 => 32; // char '_' diff From f378eb5b473e40983058cf39770ca0d48b851847 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 7 May 2024 14:16:48 -0700 Subject: [PATCH 05/38] Share code in place decoding code --- .../System.Memory/ref/System.Memory.cs | 13 +- .../src/System/Buffers/Text/Base64.cs | 1 + .../src/System/Buffers/Text/Base64Decoder.cs | 42 ++++- .../Text/Base64Url/Base64UrlDecoder.cs | 155 ++++++------------ .../Text/Base64Url/Base64UrlEncoder.cs | 69 +++++--- 5 files changed, 138 insertions(+), 142 deletions(-) diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index af4fa78727c461..c3c261714fb313 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -641,23 +641,30 @@ namespace System.Buffers.Text { public static class Base64Url { - public static int GetMaxDecodedLength(int base64Length) { throw null; } + public static byte[] DecodeFromChars(System.ReadOnlySpan source) { throw null; } + public static int DecodeFromChars(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus DecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static byte[] DecodeFromUtf8(System.ReadOnlySpan source) { throw null; } + public static int DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } public static int DecodeFromUtf8InPlace(System.Span buffer) { throw null; } public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } - public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } public static string EncodeToString(System.ReadOnlySpan source) { throw null; } public static byte[] EncodeToUtf8(System.ReadOnlySpan source) { throw null; } public static int EncodeToUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } public static System.Buffers.OperationStatus EncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } public static int GetEncodedLength(int bytesLength) { throw null; } + public static int GetMaxDecodedLength(int base64Length) { throw null; } public static bool IsValid(System.ReadOnlySpan base64UrlText) { throw null; } public static bool IsValid(System.ReadOnlySpan base64UrlText, out int decodedLength) { throw null; } public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText) { throw null; } public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText, out int decodedLength) { throw null; } - public static bool TryEncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } + public static bool TryDecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryDecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } + public static bool TryEncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } public static bool TryEncodeToUtf8InPlace(System.Span buffer, int dataLength, out int bytesWritten) { throw null; } } public static partial class Utf8Formatter diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index 3b0e7476e36acd..dc87593b2acc6a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -65,6 +65,7 @@ internal interface IBase64Decoder static abstract ReadOnlySpan Vector128LutShift { get; } static abstract int SrcLength(bool isFinalBlock, int utf8Length); static abstract int GetMaxDecodedLength(int utf8Length); + static abstract bool IsInValidLength(int bufferLength); static abstract bool TryDecode128Core( Vector128 str, Vector128 hiNibbles, diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 1d09302ac9dbf5..bde5b7dbf25be2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -381,7 +381,7 @@ public static int GetMaxDecodedFromUtf8Length(int length) public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => DecodeFromUtf8InPlace(buffer, out bytesWritten, ignoreWhiteSpace: true); - private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) + internal static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) where TBase64Decoder : IBase64Decoder { if (buffer.IsEmpty) @@ -396,15 +396,14 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span uint sourceIndex = 0; uint destIndex = 0; - // only decode input if it is a multiple of 4 - if (bufferLength % 4 != 0) + if (TBase64Decoder.IsInValidLength(buffer.Length)) { goto InvalidExit; } ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); - while (sourceIndex < bufferLength - 4) + while (sourceIndex + 4 < bufferLength) { int result = Decode(bufferBytes + sourceIndex, ref decodingMap); if (result < 0) @@ -417,10 +416,34 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span sourceIndex += 4; } - uint t0 = bufferBytes[bufferLength - 4]; - uint t1 = bufferBytes[bufferLength - 3]; - uint t2 = bufferBytes[bufferLength - 2]; - uint t3 = bufferBytes[bufferLength - 1]; + uint t0; + uint t1; + uint t2; + uint t3; + + switch (bufferLength - sourceIndex) + { + case 2: + t0 = bufferBytes[bufferLength - 2]; + t1 = bufferBytes[bufferLength - 1]; + t2 = EncodingPad; + t3 = EncodingPad; + break; + case 3: + t0 = bufferBytes[bufferLength - 3]; + t1 = bufferBytes[bufferLength - 2]; + t2 = bufferBytes[bufferLength - 1]; + t3 = EncodingPad; + break; + case 4: + t0 = bufferBytes[bufferLength - 4]; + t1 = bufferBytes[bufferLength - 3]; + t2 = bufferBytes[bufferLength - 2]; + t3 = bufferBytes[bufferLength - 1]; + break; + default: + goto InvalidExit; + } int i0 = Unsafe.Add(ref decodingMap, t0); int i1 = Unsafe.Add(ref decodingMap, t1); @@ -1156,6 +1179,9 @@ internal static bool IsWhiteSpace(int value) public static ReadOnlySpan Vector128LutShift => [0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000]; public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); + + public static bool IsInValidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 + public static int SrcLength(bool _, int utf8Length) => utf8Length & ~0x3; // only decode input up to the closest multiple of 4. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index e71a8be6f48781..072378f84fb8dc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -75,128 +75,72 @@ public static int DecodeFromUtf8InPlace(Span buffer) return bytesWritten; } - private static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) - where TBase64Decoder : IBase64Decoder + public static int DecodeFromUtf8(ReadOnlySpan source, Span destination) { - if (buffer.IsEmpty) + OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); + + if (OperationStatus.Done == status) { - bytesWritten = 0; - return OperationStatus.Done; + return bytesWritten; } - fixed (byte* bufferBytes = &MemoryMarshal.GetReference(buffer)) + if (OperationStatus.DestinationTooSmall == status) { - uint bufferLength = (uint)buffer.Length; - uint sourceIndex = 0; - uint destIndex = 0; - - ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); - - while (sourceIndex + 4 < bufferLength) - { - int result = Decode(bufferBytes + sourceIndex, ref decodingMap); - if (result < 0) - { - goto InvalidExit; - } - - WriteThreeLowOrderBytes(bufferBytes + destIndex, result); - destIndex += 3; - sourceIndex += 4; - } - - uint t0; - uint t1; - uint t2; - uint t3; - - switch (bufferLength - sourceIndex) - { - case 2: - t0 = bufferBytes[bufferLength - 2]; - t1 = bufferBytes[bufferLength - 1]; - t2 = EncodingPad; - t3 = EncodingPad; - break; - case 3: - t0 = bufferBytes[bufferLength - 3]; - t1 = bufferBytes[bufferLength - 2]; - t2 = bufferBytes[bufferLength - 1]; - t3 = EncodingPad; - break; - case 4: - t0 = bufferBytes[bufferLength - 4]; - t1 = bufferBytes[bufferLength - 3]; - t2 = bufferBytes[bufferLength - 2]; - t3 = bufferBytes[bufferLength - 1]; - break; - default: - goto InvalidExit; - } - - int i0 = Unsafe.Add(ref decodingMap, t0); - int i1 = Unsafe.Add(ref decodingMap, t1); + throw new ArgumentException("DestinationTooSmall", nameof(destination)); + } - i0 <<= 18; - i1 <<= 12; + throw new InvalidOperationException("InvalidData"); + } - i0 |= i1; + public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) + { + OperationStatus status = DecodeFromUtf8(source, destination, out _, out bytesWritten); - if (t3 != EncodingPad) - { - int i2 = Unsafe.Add(ref decodingMap, t2); - int i3 = Unsafe.Add(ref decodingMap, t3); + return status == OperationStatus.Done; + } - i2 <<= 6; + public static byte[] DecodeFromUtf8(ReadOnlySpan source) + { + Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; + OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); - i0 |= i3; - i0 |= i2; + return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : + throw new InvalidOperationException("InvalidData"); + } - if (i0 < 0) - { - goto InvalidExit; - } + public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, + out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) => + DecodeFromUtf8(MemoryMarshal.AsBytes(source), destination, out charsConsumed, out bytesWritten, isFinalBlock); - WriteThreeLowOrderBytes(bufferBytes + destIndex, i0); - destIndex += 3; - } - else if (t2 != EncodingPad) - { - int i2 = Unsafe.Add(ref decodingMap, t2); - - i2 <<= 6; + public static int DecodeFromChars(ReadOnlySpan source, Span destination) + { + OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); - i0 |= i2; + if (OperationStatus.Done == status) + { + return bytesWritten; + } - if (i0 < 0) - { - goto InvalidExit; - } + if (OperationStatus.DestinationTooSmall == status) + { + throw new ArgumentException("DestinationTooSmall", nameof(destination)); + } - bufferBytes[destIndex] = (byte)(i0 >> 16); - bufferBytes[destIndex + 1] = (byte)(i0 >> 8); - destIndex += 2; - } - else - { - if (i0 < 0) - { - goto InvalidExit; - } + throw new InvalidOperationException("InvalidData"); + } - bufferBytes[destIndex] = (byte)(i0 >> 16); - destIndex += 1; - } + public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) + { + return TryDecodeFromUtf8(MemoryMarshal.AsBytes(source), destination, out bytesWritten); + } - bytesWritten = (int)destIndex; - return OperationStatus.Done; + public static byte[] DecodeFromChars(ReadOnlySpan source) + { + Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; + OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); - InvalidExit: - bytesWritten = (int)destIndex; - return ignoreWhiteSpace ? - DecodeWithWhiteSpaceFromUtf8InPlace(buffer, ref bytesWritten, sourceIndex) : // The input may have whitespace, attempt to decode while ignoring whitespace. - OperationStatus.InvalidData; - } + return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : + throw new InvalidOperationException("InvalidData"); } private readonly struct Base64UrlDecoder : IBase64Decoder @@ -282,6 +226,9 @@ private static unsafe OperationStatus DecodeFromUtf8InPlace(Span public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); + + public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // Should we fail here? One byte cannot be decoded completely + public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index c31a1ba67a2df6..696eb9cfa58032 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -28,7 +29,8 @@ public static partial class Base64Url /// It does not return InvalidData since that is not possible for base64 encoding. /// /// The output will not be padded even if the input is not a multiple of 3. - public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => + public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, + Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => EncodeToUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); /// @@ -40,7 +42,9 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Spa public static int GetEncodedLength(int bytesLength) { if ((uint)bytesLength > Base64.MaximumEncodeLength) + { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } int remainder = bytesLength % 3; @@ -56,9 +60,20 @@ public static int GetEncodedLength(int bytesLength) /// The output will not be padded even if the input is not a multiple of 3. public static int EncodeToUtf8(ReadOnlySpan source, Span destination) { - EncodeToUtf8(source, destination, out _, out int written); + OperationStatus status = EncodeToUtf8(source, destination, out _, out int bytesWritten); + + if (OperationStatus.Done == status) + { + return bytesWritten; + } + + if (OperationStatus.DestinationTooSmall == status) + { + throw new ArgumentException("DestinationTooSmall", nameof(destination)); + } - return written; + Debug.Fail("Unreachable code"); + return 0; } /// @@ -69,15 +84,11 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination /// The output will not be padded even if the input is not a multiple of 3. public static byte[] EncodeToUtf8(ReadOnlySpan source) { - if (source.Length == 0) - { - return Array.Empty(); - } - - Span destination = stackalloc byte[GetEncodedLength(source.Length)]; // or new byte[GetEncodedLength(source.Length)] - EncodeToUtf8(source, destination, out _, out int written); + Span destination = stackalloc byte[GetEncodedLength(source.Length)]; + OperationStatus status = EncodeToUtf8(source, destination, out _, out int bytesWritten); - return destination.Slice(0, written).ToArray(); + return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : + throw new InvalidOperationException("InvalidData"); } /// @@ -98,17 +109,9 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) /// It does not return InvalidData since that is not possible for base64 encoding. /// /// The output will not be padded even if the input is not a multiple of 3. - public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) - { - if (source.Length == 0) - { - bytesConsumed = 0; - charsWritten = 0; - return OperationStatus.Done; - } - - return EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out bytesConsumed, out charsWritten, isFinalBlock); - } + public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, + out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) => + EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out bytesConsumed, out charsWritten, isFinalBlock); /// /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. @@ -119,8 +122,20 @@ public static OperationStatus EncodeToChars(ReadOnlySpan source, SpanThe output will not be padded even if the input is not a multiple of 3. public static int EncodeToChars(ReadOnlySpan source, Span destination) { - EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int written); - return written; + OperationStatus status = EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int charsWritten); + + if (OperationStatus.Done == status) + { + return charsWritten; + } + + if (OperationStatus.DestinationTooSmall == status) + { + throw new ArgumentException("DestinationTooSmall", nameof(destination)); + } + + Debug.Fail("Unreachable code"); + return 0; } /// @@ -181,12 +196,12 @@ public static bool TryEncodeToChars(ReadOnlySpan source, Span destin /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. - /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// if bytes encoded successfully, otherwise . /// The output will not be padded even if the input is not a multiple of 3. - public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destination, out int charsWritten) + public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) { - OperationStatus status = EncodeToUtf8(source, destination, out _, out charsWritten); + OperationStatus status = EncodeToUtf8(source, destination, out _, out bytesWritten); return status == OperationStatus.Done; } From 3f6ea88ba4cb408c3954917a060c36aee2f1576a Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 13 May 2024 14:22:50 -0700 Subject: [PATCH 06/38] Add span oveloads with vectorization --- .../tests/Base64/Base64TestHelper.cs | 22 +- .../Base64UrlEncodingAPIsUnitTests.cs | 568 ++++++++++-- .../src/System/Buffers/Text/Base64Decoder.cs | 2 +- .../Text/Base64Url/Base64UrlDecoder.cs | 815 +++++++++++++++++- .../Text/Base64Url/Base64UrlEncoder.cs | 655 +++++++++++++- 5 files changed, 1983 insertions(+), 79 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs index 5ad537282c3f6b..4637f042aaa0ee 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs @@ -120,6 +120,16 @@ internal static void InitializeDecodableBytes(Span bytes, int seed = 100) } } + internal static void InitializeUrlDecodableBytes(Span bytes, int seed = 100) + { + var rnd = new Random(seed); + for (int i = 0; i < bytes.Length; i++) + { + int index = (byte)rnd.Next(0, s_urlEncodingMap.Length); + bytes[i] = (char)s_urlEncodingMap[index]; + } + } + internal static void InitializeUrlDecodableBytes(Span bytes, int seed = 100) { var rnd = new Random(seed); @@ -163,29 +173,29 @@ public static int[] FindAllIndexOf(this IEnumerable values, T valueToFind) public static bool VerifyEncodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span encodedBytes) { - string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed).ToArray()); - string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed)); + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten)); return expectedText.Equals(encodedText); } public static bool VerifyUrlEncodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span encodedBytes) { - string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed).ToArray()) + string expectedText = Convert.ToBase64String(source.Slice(0, expectedConsumed)) .Replace('+', '-').Replace('/', '_').TrimEnd('='); - string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten).ToArray()); + string encodedText = Encoding.ASCII.GetString(encodedBytes.Slice(0, expectedWritten)); return expectedText.Equals(encodedText); } public static bool VerifyDecodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span decodedBytes) { - string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed).ToArray()); + string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed)); byte[] expectedBytes = Convert.FromBase64String(sourceString); return expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, expectedWritten)); } public static bool VerifyUrlDecodingCorrectness(int expectedConsumed, int expectedWritten, Span source, Span decodedBytes) { - string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed).ToArray()); + string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed)); string padded = sourceString.Length % 4 == 0 ? sourceString : sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); string base64 = padded.Replace("_", "/").Replace("-", "+"); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs index 501a4df391f814..27d0bd21489d68 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs @@ -1,99 +1,563 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Linq; +using System.Text; using Xunit; namespace System.Buffers.Text.Tests { public class Base64UrlEncodingAPIsUnitTests { - /*[Fact] - public static void EncodeToChars() + [Theory] + [InlineData("", 0)] + [InlineData("t", 2)] + [InlineData("te", 3)] + [InlineData("tes", 4)] + [InlineData("test", 6)] + [InlineData("test/", 7)] + [InlineData("test/+", 8)] + public static void DecodeEncodeToFromCharsStringRoundTrip(string str, int expectedWritten) { - string input = "test"; - byte[] inputBytes = Convert.FromBase64String(input); - Span resultChars = new char[4]; + byte[] inputBytes = Encoding.UTF8.GetBytes(str); + Span resultChars = new char[Base64Url.GetEncodedLength(inputBytes.Length)]; OperationStatus operationStatus = Base64Url.EncodeToChars(inputBytes, resultChars, out int bytesConsumed, out int charsWritten); Assert.Equal(OperationStatus.Done, operationStatus); - Assert.Equal(4, bytesConsumed); - Assert.Equal(3, charsWritten); - }*/ + Assert.Equal(str.Length, bytesConsumed); + Assert.Equal(expectedWritten, charsWritten); + string result = Base64Url.EncodeToString(inputBytes); + Assert.Equal(result, resultChars); + Assert.Equal(expectedWritten, Base64Url.EncodeToChars(inputBytes, resultChars)); + Assert.True(Base64Url.TryEncodeToChars(inputBytes, resultChars, out charsWritten)); + Assert.Equal(expectedWritten, charsWritten); + Assert.Equal(result, resultChars); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(resultChars.Length)]; + operationStatus = Base64Url.DecodeFromChars(resultChars, decodedBytes, out bytesConsumed, out int bytesWritten); + Assert.Equal(OperationStatus.Done, operationStatus); + Assert.Equal(resultChars.Length, bytesConsumed); + Assert.Equal(str.Length, bytesWritten); + Assert.Equal(inputBytes, decodedBytes); + Assert.Equal(str.Length, Base64Url.DecodeFromChars(resultChars, decodedBytes)); + Assert.True(Base64Url.TryDecodeFromChars(resultChars, decodedBytes, out bytesConsumed)); + Assert.Equal(str.Length, bytesConsumed); + Assert.Equal(inputBytes, decodedBytes); + Assert.Equal(str, Encoding.UTF8.GetString(decodedBytes)); + } + + [Fact] + public void EncodingWithLargeSpan() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes = rnd.Next(100, 1000 * 1000); + Span source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + + Span encodedBytes = new char[Base64Url.GetEncodedLength(source.Length)]; + OperationStatus result = Base64Url.EncodeToChars(source, encodedBytes, out int consumed, out int encodedBytesCount); + Assert.Equal(OperationStatus.Done, result); + Assert.Equal(source.Length, consumed); + Assert.Equal(encodedBytes.Length, encodedBytesCount); + string expectedText = Convert.ToBase64String(source).Replace('+', '-').Replace('/', '_').TrimEnd('='); + Assert.Equal(expectedText, encodedBytes); + } + } + + [Fact] + public void DecodeWithLargeSpan() + { + var rnd = new Random(42); + for (int i = 0; i < 10; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 1); // ensure we have a valid length + + Span source = new char[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromChars(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(source.Length, consumed); + Assert.Equal(decodedBytes.Length, decodedByteCount); + + string sourceString = source.ToString(); + string padded = sourceString.Length % 4 == 0 ? sourceString : + sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); + string base64 = padded.Replace("_", "/").Replace("-", "+"); + byte[] expectedBytes = Convert.FromBase64String(base64); + Assert.True(expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, decodedByteCount))); + } + } + + public static IEnumerable EncodeToStringTests_TestData() + { + yield return new object[] { Enumerable.Range(0, 0).Select(i => (byte)i).ToArray(), "" }; + yield return new object[] { Enumerable.Range(0, 1).Select(i => (byte)i).ToArray(), "AA" }; + yield return new object[] { Enumerable.Range(0, 2).Select(i => (byte)i).ToArray(), "AAE" }; + yield return new object[] { Enumerable.Range(0, 3).Select(i => (byte)i).ToArray(), "AAEC" }; + yield return new object[] { Enumerable.Range(0, 4).Select(i => (byte)i).ToArray(), "AAECAw" }; + yield return new object[] { Enumerable.Range(0, 5).Select(i => (byte)i).ToArray(), "AAECAwQ" }; + yield return new object[] { Enumerable.Range(0, 6).Select(i => (byte)i).ToArray(), "AAECAwQF" }; + yield return new object[] { Enumerable.Range(0, 7).Select(i => (byte)i).ToArray(), "AAECAwQFBg" }; + yield return new object[] { Enumerable.Range(0, 8).Select(i => (byte)i).ToArray(), "AAECAwQFBgc" }; + yield return new object[] { Enumerable.Range(0, 9).Select(i => (byte)i).ToArray(), "AAECAwQFBgcI" }; + yield return new object[] { Enumerable.Range(0, 10).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQ" }; + yield return new object[] { Enumerable.Range(0, 11).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQo" }; + yield return new object[] { Enumerable.Range(0, 12).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoL" }; + yield return new object[] { Enumerable.Range(0, 13).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA" }; + yield return new object[] { Enumerable.Range(0, 14).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0" }; + yield return new object[] { Enumerable.Range(0, 15).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0O" }; + yield return new object[] { Enumerable.Range(0, 16).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODw" }; + yield return new object[] { Enumerable.Range(0, 17).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxA" }; + yield return new object[] { Enumerable.Range(0, 18).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAR" }; + yield return new object[] { Enumerable.Range(0, 19).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREg" }; + yield return new object[] { Enumerable.Range(0, 20).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhM" }; + yield return new object[] { Enumerable.Range(0, 21).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMU" }; + yield return new object[] { Enumerable.Range(0, 22).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFQ" }; + yield return new object[] { Enumerable.Range(0, 23).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRY" }; + yield return new object[] { Enumerable.Range(0, 24).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX" }; + yield return new object[] { Enumerable.Range(0, 25).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGA" }; + yield return new object[] { Enumerable.Range(0, 26).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBk" }; + yield return new object[] { Enumerable.Range(0, 27).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBka" }; + yield return new object[] { Enumerable.Range(0, 28).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGw" }; + yield return new object[] { Enumerable.Range(0, 29).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxw" }; + yield return new object[] { Enumerable.Range(0, 30).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwd" }; + yield return new object[] { Enumerable.Range(0, 31).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHg" }; + yield return new object[] { Enumerable.Range(0, 32).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" }; + yield return new object[] { Enumerable.Range(0, 33).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8g" }; + yield return new object[] { Enumerable.Range(0, 34).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gIQ" }; + yield return new object[] { Enumerable.Range(0, 35).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISI" }; + yield return new object[] { Enumerable.Range(0, 36).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIj" }; + yield return new object[] { Enumerable.Range(0, 37).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJA" }; + yield return new object[] { Enumerable.Range(0, 38).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCU" }; + yield return new object[] { Enumerable.Range(0, 39).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm" }; + yield return new object[] { Enumerable.Range(0, 40).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJw" }; + yield return new object[] { Enumerable.Range(0, 41).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJyg" }; + yield return new object[] { Enumerable.Range(0, 42).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygp" }; + yield return new object[] { Enumerable.Range(0, 43).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKg" }; + yield return new object[] { Enumerable.Range(0, 44).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKis" }; + yield return new object[] { Enumerable.Range(0, 45).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKiss" }; + yield return new object[] { Enumerable.Range(0, 46).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLQ" }; + yield return new object[] { Enumerable.Range(0, 47).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4" }; + yield return new object[] { Enumerable.Range(0, 48).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v" }; + yield return new object[] { Enumerable.Range(0, 49).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMA" }; + yield return new object[] { Enumerable.Range(0, 50).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDE" }; + yield return new object[] { Enumerable.Range(0, 51).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEy" }; + yield return new object[] { Enumerable.Range(0, 52).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMw" }; + yield return new object[] { Enumerable.Range(0, 53).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ" }; + yield return new object[] { Enumerable.Range(0, 54).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1" }; + yield return new object[] { Enumerable.Range(0, 55).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Ng" }; + yield return new object[] { Enumerable.Range(0, 56).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc" }; + yield return new object[] { Enumerable.Range(0, 57).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4" }; + yield return new object[] { Enumerable.Range(0, 58).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OQ" }; + yield return new object[] { Enumerable.Range(0, 59).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo" }; + yield return new object[] { Enumerable.Range(0, 60).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7" }; + yield return new object[] { Enumerable.Range(0, 61).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PA" }; + yield return new object[] { Enumerable.Range(0, 62).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0" }; + yield return new object[] { Enumerable.Range(0, 63).Select(i => (byte)i).ToArray(), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-" }; + yield return new object[] { Encoding.Unicode.GetBytes("aaaabbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccdd"), "YQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAA" }; + yield return new object[] { Encoding.Unicode.GetBytes("vbnmbbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx"), "dgBiAG4AbQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAHgA" }; + yield return new object[] { Encoding.Unicode.GetBytes("rrrrbbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccdd\0"), "cgByAHIAcgBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAAAA" }; + yield return new object[] { Encoding.Unicode.GetBytes("uuuubbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccdd\0feffe"), "dQB1AHUAdQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAAAAZgBlAGYAZgBlAA" }; + yield return new object[] { Encoding.Unicode.GetBytes("kkkkkbbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx\u043F\u0440\u0438\u0432\u0435\u0442\u043C\u0438\u0440\u4F60\u597D\u4E16\u754C"), "awBrAGsAawBrAGIAYgBiAGIAYwBjAGMAYwBkAGQAZABkAGQAZABkAGUAZQBlAGUAZQBhAGEAYQBhAGIAYgBiAGIAYwBjAGMAYwBkAGQAZABkAGQAZABkAGUAZQBlAGUAZQBhAGEAYQBhAGIAYgBiAGIAYwBjAGMAYwBkAGQAeAA_BEAEOAQyBDUEQgQ8BDgEQARgT31ZFk5MdQ" }; + yield return new object[] { Encoding.Unicode.GetBytes(",,,,bbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx\u043F\u0440\u0438\u0432\u0435\u0442\u043C\u0438\u0440\u4F60\u597D\u4E16\u754Cddddeeeeea"), "LAAsACwALABiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAHgAPwRABDgEMgQ1BEIEPAQ4BEAEYE99WRZOTHVkAGQAZABkAGUAZQBlAGUAZQBhAA" }; + yield return new object[] { Encoding.Unicode.GetBytes("____bbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddaaaabbbbccccdddddddeeeeeaaaabbbbccccdcccd"), "XwBfAF8AXwBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAYwBjAGMAZAA" }; + yield return new object[] { Encoding.Unicode.GetBytes(" bbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddaaaabbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccd"), "IAAgACAAIABiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQA" }; + yield return new object[] { Encoding.Unicode.GetBytes("\0\0bbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddaaaabbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx"), "AAAAAGIAYgBiAGIAYwBjAGMAYwBkAGQAZABkAGQAZABkAGUAZQBlAGUAZQBhAGEAYQBhAGIAYgBiAGIAYwBjAGMAYwBkAGQAZABkAGQAZABkAGUAZQBlAGUAZQBhAGEAYQBhAGIAYgBiAGIAYwBjAGMAYwBkAGQAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAHgA" }; + yield return new object[] { Encoding.Unicode.GetBytes("eeeebbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccdgggdaaaabbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx"), "ZQBlAGUAZQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABnAGcAZwBkAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZAB4AA" }; + } + + + [Theory] + [MemberData(nameof(EncodeToStringTests_TestData))] + public static void EncodeToStringTests(byte[] inputBytes, string expectedBase64) + { + Assert.Equal(expectedBase64, Base64Url.EncodeToString(inputBytes)); + Span chars = new char[Base64Url.GetEncodedLength(inputBytes.Length)]; + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToChars(inputBytes, chars, out int _, out int charsWritten)); + Assert.Equal(expectedBase64, chars.Slice(0, charsWritten)); + } [Fact] - public static void ShortInputArray() + public static void Roundtrip() { - // Regression test for bug where a short input array caused an exception to be thrown - byte[] inputBuffer = "abc"u8.ToArray(); - char[] outputBuffer = new char[4]; - Convert.ToBase64CharArray(inputBuffer, 0, 3, outputBuffer, 0); - Convert.ToBase64CharArray(inputBuffer, 0, 2, outputBuffer, 0); + string input = "test"; + Verify(input, result => + { + Assert.Equal(3, result.Length); + + uint triplet = (uint)((result[0] << 16) | (result[1] << 8) | result[2]); + Assert.Equal(45, triplet >> 18); // 't' + Assert.Equal(30, (triplet << 14) >> 26); // 'e' + Assert.Equal(44, (triplet << 20) >> 26); // 's' + Assert.Equal(45, (triplet << 26) >> 26); // 't' + + Assert.Equal(input, Base64Url.EncodeToString(result)); + }); } [Fact] - public static void ValidOffsetOut() + public static void PartialRoundtripWithoutPadding() { - // Regression test for bug where offsetOut parameter was ignored - char[] outputBuffer = "........".ToCharArray(); - byte[] inputBuffer = new byte[6]; - for (int i = 0; i < inputBuffer.Length; inputBuffer[i] = (byte)i++) ; + string input = "ab"; + Verify(input, result => + { + Assert.Equal(1, result.Length); - // Convert the first half of the byte array, write to the first half of the char array - int c = Convert.ToBase64CharArray(inputBuffer, 0, 3, outputBuffer, 0); - Assert.Equal(4, c); - Assert.Equal("AAEC....", new string(outputBuffer)); + string roundtrippedString = Base64Url.EncodeToString(result); + Assert.NotEqual(input, roundtrippedString); + Assert.Equal(input[0], roundtrippedString[0]); + }); + } + + [Fact] + public static void PartialRoundtripWithPadding2() + { + string input = "ab=="; + Verify(input, result => + { + Assert.Equal(1, result.Length); - // Convert the second half of the byte array, write to the second half of the char array - c = Convert.ToBase64CharArray(inputBuffer, 3, 3, outputBuffer, 4); - Assert.Equal(4, c); - Assert.Equal("AAECAwQF", new string(outputBuffer)); + string roundtrippedString = Base64Url.EncodeToString(result); + Assert.NotEqual(input, roundtrippedString); + Assert.Equal(input[0], roundtrippedString[0]); + }); } [Fact] - public static void InvalidInputBuffer() + public static void PartialRoundtripWithPadding1() { - Assert.Throws(() => Convert.ToBase64CharArray(null, 0, 1, new char[1], 0)); + string input = "789="; + Verify(input, result => + { + Assert.Equal(2, result.Length); + + string roundtrippedString = Base64Url.EncodeToString(result); + Assert.NotEqual(input, roundtrippedString); + Assert.Equal(input[0], roundtrippedString[0]); + Assert.Equal(input[1], roundtrippedString[1]); + }); } [Fact] - public static void InvalidOutputBuffer() + public static void ParseWithWhitespace() { - char[] inputChars = "test".ToCharArray(); - byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, null, 0)); + Verify("abc= \t \r\n ="); } [Fact] - public static void InvalidOffsetIn() + public static void RoundtripWithWhitespace2() { - char[] inputChars = "test".ToCharArray(); - byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); - char[] outputBuffer = new char[4]; + string input = "abc= \t\n\t\r "; + VerifyRoundtrip(input, "abc"); + } - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, -1, inputBytes.Length, outputBuffer, 0)); - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, inputBytes.Length, inputBytes.Length, outputBuffer, 0)); + [Fact] + public static void RoundtripWithWhitespace3() + { + string input = " \r\n\t abc = \t\n\t\r "; + VerifyRoundtrip(input, "abc"); } [Fact] - public static void InvalidOffsetOut() + public static void RoundtripWithWhitespace4() { - char[] inputChars = "test".ToCharArray(); - byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); - char[] outputBuffer = new char[4]; + string expected = "test"; + string input = expected.Insert(1, new string(' ', 17)).PadLeft(31, ' ').PadRight(12, ' '); + VerifyRoundtrip(input, expected, expectedLengthBytes: 3); + } - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, outputBuffer, -1)); - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length, outputBuffer, 1)); + [Fact] + public static void RoundtripLargeString() + { + string input = new string('a', 10000); + VerifyRoundtrip(input, input); } [Fact] - public static void InvalidInputLength() + public static void InvalidInput() { - char[] inputChars = "test".ToCharArray(); - byte[] inputBytes = Convert.FromBase64CharArray(inputChars, 0, inputChars.Length); - char[] outputBuffer = new char[4]; + // Input must not contain invalid characters + VerifyInvalidInput("2+34"); + VerifyInvalidInput("23/4"); + + // Input must not contain 3 or more padding characters in a row + VerifyInvalidInput("a==="); + VerifyInvalidInput("abc====="); + VerifyInvalidInput("a===\r \t \n"); + + // Input must not contain padding characters in the middle of the string + VerifyInvalidInput("No=n"); + VerifyInvalidInput("abcd====abcd"); + + // Input must not contain extra trailing padding characters + VerifyInvalidInput("="); + VerifyInvalidInput("abc==="); + } - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, -1, outputBuffer, 0)); - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 0, inputBytes.Length + 1, outputBuffer, 0)); - Assert.Throws(() => Convert.ToBase64CharArray(inputBytes, 1, inputBytes.Length, outputBuffer, 0)); + [Fact] + public static void ExtraPaddingCharacter() + { + VerifyInvalidInput("abcdxyz=" + "="); + } + + [Fact] + public static void InvalidCharactersInInput() + { + ushort[] invalidChars = { 30122, 62608, 13917, 19498, 2473, 40845, 35988, 2281, 51246, 36372 }; + + foreach (char ch in invalidChars) + { + var builder = new StringBuilder("abc"); + builder.Insert(1, ch); + VerifyInvalidInput(builder.ToString()); + } + } + + private static void VerifyRoundtrip(string input, string expected = null, int? expectedLengthBytes = null) + { + if (expected == null) + { + expected = input; + } + + Verify(input, result => + { + if (expectedLengthBytes.HasValue) + { + Assert.Equal(expectedLengthBytes.Value, result.Length); + } + Assert.Equal(expected, Base64Url.EncodeToString(result)); + }); + } + + private static void VerifyInvalidInput(string input) + { + char[] inputChars = input.ToCharArray(); + + Assert.Throws(() => Base64Url.DecodeFromChars(input)); // or FormatException? + } + + private static void Verify(string input, Action action = null) + { + if (action != null) + { + action(Base64Url.DecodeFromChars(input)); + } + } + + [Fact] + public static void Base64_AllMethodsRoundtripConsistently() + { + var r = new Random(42); + for (int length = 0; length < 128; length++) + { + var original = new byte[length]; + r.NextBytes(original); + + string encodedString = Base64Url.EncodeToString(original); + + char[] encodedArray = new char[encodedString.Length]; + Assert.Equal(OperationStatus.Done, Base64Url.EncodeToChars(original, encodedArray, out _, out int charsWritten)); + Assert.Equal(encodedArray.Length, charsWritten); + AssertExtensions.SequenceEqual(encodedString, encodedArray); + + char[] encodedSpan = new char[encodedString.Length]; + Assert.True(Base64Url.TryEncodeToChars(original, encodedSpan, out charsWritten)); + Assert.Equal(encodedSpan.Length, charsWritten); + AssertExtensions.SequenceEqual(encodedString, encodedSpan); + + AssertExtensions.SequenceEqual(original, Base64Url.DecodeFromChars(encodedString)); + Span decodedBytes = new byte[original.Length]; + int decoded = Base64Url.DecodeFromChars(encodedArray, decodedBytes); + Assert.Equal(original.Length, decoded); + AssertExtensions.SequenceEqual(original, decodedBytes); + + byte[] actualBytes = new byte[original.Length]; + Assert.True(Base64Url.TryDecodeFromChars(encodedSpan, actualBytes, out int bytesWritten)); + Assert.Equal(original.Length, bytesWritten); + AssertExtensions.SequenceEqual(original, actualBytes); + } + } + + [Theory] + [MemberData(nameof(Base64TestData))] + public static void TryDecodeFromChars(string encodedAsString, byte[] expected) + { + ReadOnlySpan encoded = encodedAsString; + if (expected == null) + { + Span actual = new byte[Base64Url.GetMaxDecodedLength(encodedAsString.Length)]; + Assert.False(Base64Url.TryDecodeFromChars(encoded, actual, out int bytesWritten)); + } + else + { + // Destination buffer size enough + { + Span actual = new byte[Base64Url.GetMaxDecodedLength(encodedAsString.Length)]; + Assert.True(Base64Url.TryDecodeFromChars(encoded, actual, out int bytesWritten)); + Assert.Equal(expected, actual.Slice(0, bytesWritten)); + Assert.Equal(expected.Length, bytesWritten); + } + + // Buffer too short + if (expected.Length != 0) + { + byte[] actual = new byte[expected.Length - 1]; + Assert.False(Base64Url.TryDecodeFromChars(encoded, actual, out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + } + } + + public static IEnumerable Base64TestData + { + get + { + foreach ((string bse64UrlString, byte[] expectedArray) tuple in Base64TestDataSeed) + { + yield return new object[] { tuple.bse64UrlString, tuple.expectedArray }; + yield return new object[] { InsertSpaces(tuple.bse64UrlString, 1), tuple.expectedArray }; + yield return new object[] { InsertSpaces(tuple.bse64UrlString, 4), tuple.expectedArray }; + } + } + } + + public static IEnumerable<(string, byte[])> Base64TestDataSeed + { + get + { + // Empty + yield return ("", Array.Empty()); + + // All whitespace characters. + yield return (" \t\r\n", Array.Empty()); + + // Invalid Input length + yield return ("A", null); + + // Cannot continue past end pad + yield return ("AAA=BBBB", null); + yield return ("AA==BBBB", null); + + // Cannot have more than two end pads + yield return ("A===", null); + yield return ("====", null); + + // Verify negative entries of charmap. + for (int i = 0; i < 256; i++) + { + char c = (char)i; + if (!IsValidBase64Char(c)) + { + string text = new string(c, 1) + "AAA"; + yield return (text, null); + } + } + + // Verify >255 character handling. + string largerThanByte = new string((char)256, 1); + yield return (largerThanByte + "AAA", null); + yield return ("A" + largerThanByte + "AA", null); + yield return ("AA" + largerThanByte + "A", null); + yield return ("AAA" + largerThanByte, null); + yield return ("AAAA" + largerThanByte + "AAA", null); + yield return ("AAAA" + "A" + largerThanByte + "AA", null); + yield return ("AAAA" + "AA" + largerThanByte + "A", null); + yield return ("AAAA" + "AAA" + largerThanByte, null); + + // Verify positive entries of charmap. + yield return ("-A==", new byte[] { 0xf8 }); + yield return ("_A=", new byte[] { 0xfc }); + yield return ("0A==", new byte[] { 0xd0 }); + yield return ("1A==", new byte[] { 0xd4 }); + yield return ("2A==", new byte[] { 0xd8 }); + yield return ("3A==", new byte[] { 0xdc }); + yield return ("4A", new byte[] { 0xe0 }); + yield return ("5A=", new byte[] { 0xe4 }); + yield return ("6A==", new byte[] { 0xe8 }); + yield return ("7A==", new byte[] { 0xec }); + yield return ("8A", new byte[] { 0xf0 }); + yield return ("9A=", new byte[] { 0xf4 }); + yield return ("AA=", new byte[] { 0x00 }); + yield return ("BA", new byte[] { 0x04 }); + yield return ("CA", new byte[] { 0x08 }); + yield return ("DA==", new byte[] { 0x0c }); + yield return ("EA==", new byte[] { 0x10 }); + yield return ("FA==", new byte[] { 0x14 }); + yield return ("GA==", new byte[] { 0x18 }); + yield return ("HA==", new byte[] { 0x1c }); + yield return ("IA==", new byte[] { 0x20 }); + yield return ("JA==", new byte[] { 0x24 }); + yield return ("KA==", new byte[] { 0x28 }); + yield return ("LA==", new byte[] { 0x2c }); + yield return ("MA==", new byte[] { 0x30 }); + yield return ("NA==", new byte[] { 0x34 }); + yield return ("OA==", new byte[] { 0x38 }); + yield return ("PA==", new byte[] { 0x3c }); + yield return ("QA==", new byte[] { 0x40 }); + yield return ("RA==", new byte[] { 0x44 }); + yield return ("SA==", new byte[] { 0x48 }); + yield return ("TA==", new byte[] { 0x4c }); + yield return ("UA==", new byte[] { 0x50 }); + yield return ("VA==", new byte[] { 0x54 }); + yield return ("WA==", new byte[] { 0x58 }); + yield return ("XA==", new byte[] { 0x5c }); + yield return ("YA==", new byte[] { 0x60 }); + yield return ("ZA==", new byte[] { 0x64 }); + yield return ("aA==", new byte[] { 0x68 }); + yield return ("bA==", new byte[] { 0x6c }); + yield return ("cA==", new byte[] { 0x70 }); + yield return ("dA==", new byte[] { 0x74 }); + yield return ("eA==", new byte[] { 0x78 }); + yield return ("fA==", new byte[] { 0x7c }); + yield return ("gA==", new byte[] { 0x80 }); + yield return ("hA==", new byte[] { 0x84 }); + yield return ("iA==", new byte[] { 0x88 }); + yield return ("jA==", new byte[] { 0x8c }); + yield return ("kA==", new byte[] { 0x90 }); + yield return ("lA==", new byte[] { 0x94 }); + yield return ("mA==", new byte[] { 0x98 }); + yield return ("nA==", new byte[] { 0x9c }); + yield return ("oA==", new byte[] { 0xa0 }); + yield return ("pA==", new byte[] { 0xa4 }); + yield return ("qA==", new byte[] { 0xa8 }); + yield return ("rA==", new byte[] { 0xac }); + yield return ("sA==", new byte[] { 0xb0 }); + yield return ("tA==", new byte[] { 0xb4 }); + yield return ("uA==", new byte[] { 0xb8 }); + yield return ("vA==", new byte[] { 0xbc }); + yield return ("wA==", new byte[] { 0xc0 }); + yield return ("xA==", new byte[] { 0xc4 }); + yield return ("yA==", new byte[] { 0xc8 }); + yield return ("zA==", new byte[] { 0xcc }); + } + } + + private static string InsertSpaces(string text, int period) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < text.Length; i++) + { + if ((i % period) == 0) + { + sb.Append(" "); + } + sb.Append(text[i]); + } + sb.Append(" "); + return sb.ToString(); + } + + private static bool IsValidBase64Char(char c) + { + return char.IsAsciiLetterOrDigit(c) || c is '-' or '_' || char.IsWhiteSpace(c); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index bde5b7dbf25be2..4b7a80d3d51249 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -510,7 +510,7 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa } } - internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 072378f84fb8dc..43fb86d2e97fc2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -110,7 +110,816 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) => - DecodeFromUtf8(MemoryMarshal.AsBytes(source), destination, out charsConsumed, out bytesWritten, isFinalBlock); + DecodeFromChars(source, destination, out charsConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + + internal static unsafe OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, + out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) + where TBase64Decoder : IBase64Decoder + { + if (source.IsEmpty) + { + bytesConsumed = 0; + bytesWritten = 0; + return OperationStatus.Done; + } + + fixed (char* srcBytes = &MemoryMarshal.GetReference(source)) + fixed (byte* destBytes = &MemoryMarshal.GetReference(destination)) + { + int srcLength = TBase64Decoder.SrcLength(isFinalBlock, source.Length); + int destLength = destination.Length; + int maxSrcLength = srcLength; + int decodedLength = TBase64Decoder.GetMaxDecodedLength(srcLength); + + // max. 2 padding chars + if (destLength < decodedLength - 2) + { + // For overflow see comment below + maxSrcLength = destLength / 3 * 4; + } + + char* src = srcBytes; + byte* dest = destBytes; + char* srcEnd = srcBytes + (uint)srcLength; + char* srcMax = srcBytes + (uint)maxSrcLength; + + if (maxSrcLength >= 24) + { + char* end = srcMax - 88; + if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) + { + Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); + + if (src == srcEnd) + { + goto DoneExit; + } + } + + end = srcMax - 45; + if (Avx2.IsSupported && (end >= src)) + { + Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); + + if (src == srcEnd) + { + goto DoneExit; + } + } + + end = srcMax - 24; + if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) + { + Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); + + if (src == srcEnd) + { + goto DoneExit; + } + } + } + + // Last bytes could have padding characters, so process them separately and treat them as valid only if isFinalBlock is true + // if isFinalBlock is false, padding characters are considered invalid + int skipLastChunk = isFinalBlock ? 4 : 0; + + if (destLength >= decodedLength) + { + maxSrcLength = srcLength - skipLastChunk; + } + else + { + // This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733) + // Therefore, (destLength / 3) * 4 will always be less than 2147483641 + Debug.Assert(destLength < (int.MaxValue / 4 * 3)); + maxSrcLength = (destLength / 3) * 4; + srcLength &= ~0x3; // Round down to multiple of 4, this only affect Base64UrlDecoder path + } + + ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); + srcMax = srcBytes + maxSrcLength; + + while (src < srcMax) + { + int result = Decode(src, ref decodingMap); + + if (result < 0) + { + goto InvalidDataExit; + } + + WriteThreeLowOrderBytes(dest, result); + src += 4; + dest += 3; + } + + if (maxSrcLength != srcLength - skipLastChunk) + { + goto DestinationTooSmallExit; + } + + // If input is less than 4 bytes, srcLength == sourceIndex == 0 + // If input is not a multiple of 4, sourceIndex == srcLength != 0 + if (src == srcEnd) + { + if (isFinalBlock) + { + goto InvalidDataExit; + } + + if (src == srcBytes + source.Length) + { + goto DoneExit; + } + + goto NeedMoreDataExit; + } + + // if isFinalBlock is false, we will never reach this point + + uint t0; + uint t1; + uint t2; + uint t3; + // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. + long remaining = srcEnd - src; + switch (remaining) + { + case 2: + t0 = srcEnd[-2]; + t1 = srcEnd[-1]; + t2 = EncodingPad; + t3 = EncodingPad; + break; + case 3: + t0 = srcEnd[-3]; + t1 = srcEnd[-2]; + t2 = srcEnd[-1]; + t3 = EncodingPad; + break; + case 4: + t0 = srcEnd[-4]; + t1 = srcEnd[-3]; + t2 = srcEnd[-2]; + t3 = srcEnd[-1]; + break; + default: + goto InvalidDataExit; + } + + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + { + goto InvalidDataExit; + } + + int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); + int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + + byte* destMax = destBytes + (uint)destLength; + + if (t3 != EncodingPad) + { + int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); + int i3 = Unsafe.Add(ref decodingMap, (IntPtr)t3); + + i2 <<= 6; + + i0 |= i3; + i0 |= i2; + + if (i0 < 0) + { + goto InvalidDataExit; + } + if (dest + 3 > destMax) + { + goto DestinationTooSmallExit; + } + + WriteThreeLowOrderBytes(dest, i0); + dest += 3; + src += 4; + } + else if (t2 != EncodingPad) + { + int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); + + i2 <<= 6; + + i0 |= i2; + + if (i0 < 0) + { + goto InvalidDataExit; + } + if (dest + 2 > destMax) + { + goto DestinationTooSmallExit; + } + + dest[0] = (byte)(i0 >> 16); + dest[1] = (byte)(i0 >> 8); + dest += 2; + src += remaining; + } + else + { + if (i0 < 0) + { + goto InvalidDataExit; + } + if (dest + 1 > destMax) + { + goto DestinationTooSmallExit; + } + + dest[0] = (byte)(i0 >> 16); + dest += 1; + src += remaining; + } + + if (srcLength != source.Length) + { + goto InvalidDataExit; + } + + DoneExit: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.Done; + + DestinationTooSmallExit: + if (srcLength != source.Length && isFinalBlock) + { + goto InvalidDataExit; // if input is not a multiple of 4, and there is no more data, return invalid data instead + } + + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.DestinationTooSmall; + + NeedMoreDataExit: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.NeedMoreData; + + InvalidDataExit: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return ignoreWhiteSpace ? + InvalidDataFallback(source, destination, ref bytesConsumed, ref bytesWritten, isFinalBlock) : + OperationStatus.InvalidData; + } + + static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) + { + utf8 = utf8.Slice(bytesConsumed); + bytes = bytes.Slice(bytesWritten); + + OperationStatus status; + do + { + int localConsumed = IndexOfAnyExceptWhiteSpace(utf8); + if (localConsumed < 0) + { + // The remainder of the input is all whitespace. Mark it all as having been consumed, + // and mark the operation as being done. + bytesConsumed += utf8.Length; + status = OperationStatus.Done; + break; + } + + if (localConsumed == 0) + { + // Non-whitespace was found at the beginning of the input. Since it wasn't consumed + // by the previous call to DecodeFromUtf8, it must be part of a Base64 sequence + // that was interrupted by whitespace or something else considered invalid. + // Fall back to block-wise decoding. This is very slow, but it's also very non-standard + // formatting of the input; whitespace is typically only found between blocks, such as + // when Convert.ToBase64String inserts a line break every 76 output characters. + return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + } + + // Skip over the starting whitespace and continue. + bytesConsumed += localConsumed; + utf8 = utf8.Slice(localConsumed); + + // Try again after consumed whitespace + status = DecodeFromChars(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); + bytesConsumed += localConsumed; + bytesWritten += localWritten; + if (status is not OperationStatus.InvalidData) + { + break; + } + + utf8 = utf8.Slice(localConsumed); + bytes = bytes.Slice(localWritten); + } + while (!utf8.IsEmpty); + + return status; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx512BW))] + [CompExactlyDependsOn(typeof(Avx512Vbmi))] + private static unsafe void Avx512Decode(ref char* srcBytes, ref byte* destBytes, char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + { + // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/decode + // If we have AVX512 support, pick off 64 bytes at a time for as long as we can, + // but make sure that we quit before seeing any == markers at the end of the + // string. Also, because we write 16 zeroes at the end of the output, ensure + // that there are at least 22 valid bytes of input data remaining to close the + // gap. 64 + 2 + 22 = 88 bytes. + ushort* src = (ushort*)srcBytes; + byte* dest = destBytes; + + // The JIT won't hoist these "constants", so help it + Vector512 vbmiLookup0 = Vector512.Create(TBase64Decoder.VbmiLookup0).AsSByte(); + Vector512 vbmiLookup1 = Vector512.Create(TBase64Decoder.VbmiLookup1).AsSByte(); + Vector512 vbmiPackedLanesControl = Vector512.Create( + 0x06000102, 0x090a0405, 0x0c0d0e08, 0x16101112, + 0x191a1415, 0x1c1d1e18, 0x26202122, 0x292a2425, + 0x2c2d2e28, 0x36303132, 0x393a3435, 0x3c3d3e38, + 0x00000000, 0x00000000, 0x00000000, 0x00000000).AsByte(); + + Vector512 mergeConstant0 = Vector512.Create(0x01400140).AsSByte(); + Vector512 mergeConstant1 = Vector512.Create(0x00011000).AsInt16(); + + // This algorithm requires AVX512VBMI support. + // Vbmi was first introduced in CannonLake and is available from IceLake on. + do + { + AssertRead>(src, srcStart, sourceLength); + Vector512 utf16VectorLower = Vector512.Load(src); + Vector512 utf16VectorUpper = Vector512.Load(src + 32); + Vector512 str = Vector512.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); + + // Step 1: Translate encoded Base64 input to their original indices + // This step also checks for invalid inputs and exits. + // After this, we have indices which are verified to have upper 2 bits set to 0 in each byte. + // origIndex = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] + Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(vbmiLookup0, str, vbmiLookup1); + Vector512 errorVec = (origIndex.AsInt32() | str.AsInt32()).AsSByte(); + if (errorVec.ExtractMostSignificantBits() != 0) + { + break; + } + + // Step 2: Now we need to reshuffle bits to remove the 0 bits. + // multiAdd1: [...|0000cccc|ccdddddd|0000aaaa|aabbbbbb] + Vector512 multiAdd1 = Avx512BW.MultiplyAddAdjacent(origIndex.AsByte(), mergeConstant0); + // multiAdd1: [...|00000000|aaaaaabb|bbbbcccc|ccdddddd] + Vector512 multiAdd2 = Avx512BW.MultiplyAddAdjacent(multiAdd1, mergeConstant1); + + // Step 3: Pack 48 bytes + str = Avx512Vbmi.PermuteVar64x8(multiAdd2.AsByte(), vbmiPackedLanesControl).AsSByte(); + + Base64.AssertWrite>(dest, destStart, destLength); + str.Store((sbyte*)dest); + src += 64; + dest += 48; + } + while (src <= srcEnd); + + srcBytes = (char*)src; + destBytes = dest; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + private static unsafe void Avx2Decode(ref char* srcBytes, ref byte* destBytes, + char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + { + // If we have AVX2 support, pick off 32 bytes at a time for as long as we can, + // but make sure that we quit before seeing any == markers at the end of the + // string. Also, because we write 8 zeroes at the end of the output, ensure + // that there are at least 11 valid bytes of input data remaining to close the + // gap. 32 + 2 + 11 = 45 bytes. + + // See SSSE3-version below for an explanation of how the code works. + + // The JIT won't hoist these "constants", so help it + Vector256 lutHi = Vector256.Create(TBase64Decoder.Avx2LutHigh); + + Vector256 lutLo = Vector256.Create(TBase64Decoder.Avx2LutLow); + + Vector256 lutShift = Vector256.Create(TBase64Decoder.Avx2LutShift); + + Vector256 packBytesInLaneMask = Vector256.Create( + 2, 1, 0, 6, + 5, 4, 10, 9, + 8, 14, 13, 12, + -1, -1, -1, -1, + 2, 1, 0, 6, + 5, 4, 10, 9, + 8, 14, 13, 12, + -1, -1, -1, -1); + + Vector256 packLanesControl = Vector256.Create( + 0, 0, 0, 0, + 1, 0, 0, 0, + 2, 0, 0, 0, + 4, 0, 0, 0, + 5, 0, 0, 0, + 6, 0, 0, 0, + -1, -1, -1, -1, + -1, -1, -1, -1).AsInt32(); + + Vector256 maskSlashOrUnderscore = Vector256.Create((sbyte)TBase64Decoder.MaskSlashOrUnderscore); + Vector256 shiftForUnderscore = Vector256.Create((sbyte)33); + Vector256 mergeConstant0 = Vector256.Create(0x01400140).AsSByte(); + Vector256 mergeConstant1 = Vector256.Create(0x00011000).AsInt16(); + + ushort* src = (ushort*)srcBytes; + byte* dest = destBytes; + + //while (remaining >= 45) + do + { + AssertRead>(src, srcStart, sourceLength); + Vector256 utf16VectorLower = Avx.LoadVector256(src); + Vector256 utf16VectorUpper = Avx.LoadVector256(src + 16); + Vector256 str = Vector256.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); + + Vector256 hiNibbles = Avx2.And(Avx2.ShiftRightLogical(str.AsInt32(), 4).AsSByte(), maskSlashOrUnderscore); + + if (!TBase64Decoder.TryDecode256Core(str, hiNibbles, maskSlashOrUnderscore, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) + { + break; + } + + // in, lower lane, bits, upper case are most significant bits, lower case are least significant bits: + // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ + // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG + // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD + // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA + + Vector256 merge_ab_and_bc = Avx2.MultiplyAddAdjacent(str.AsByte(), mergeConstant0); + // 0000kkkk LLllllll 0000JJJJ JJjjKKKK + // 0000hhhh IIiiiiii 0000GGGG GGggHHHH + // 0000eeee FFffffff 0000DDDD DDddEEEE + // 0000bbbb CCcccccc 0000AAAA AAaaBBBB + + Vector256 output = Avx2.MultiplyAddAdjacent(merge_ab_and_bc, mergeConstant1); + // 00000000 JJJJJJjj KKKKkkkk LLllllll + // 00000000 GGGGGGgg HHHHhhhh IIiiiiii + // 00000000 DDDDDDdd EEEEeeee FFffffff + // 00000000 AAAAAAaa BBBBbbbb CCcccccc + + // Pack bytes together in each lane: + output = Avx2.Shuffle(output.AsSByte(), packBytesInLaneMask).AsInt32(); + // 00000000 00000000 00000000 00000000 + // LLllllll KKKKkkkk JJJJJJjj IIiiiiii + // HHHHhhhh GGGGGGgg FFffffff EEEEeeee + // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa + + // Pack lanes + str = Avx2.PermuteVar8x32(output, packLanesControl).AsSByte(); + + Base64.AssertWrite>(dest, destStart, destLength); + Avx.Store(dest, str.AsByte()); + + src += 32; + dest += 24; + } + while (src <= srcEnd); + + srcBytes = (char*)src; + destBytes = dest; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(Ssse3))] + private static unsafe void Vector128Decode(ref char* srcBytes, ref byte* destBytes, char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + { + Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian); + + // If we have Vector128 support, pick off 16 bytes at a time for as long as we can, + // but make sure that we quit before seeing any == markers at the end of the + // string. Also, because we write four zeroes at the end of the output, ensure + // that there are at least 6 valid bytes of input data remaining to close the + // gap. 16 + 2 + 6 = 24 bytes. + + // The input consists of six character sets in the Base64 alphabet, + // which we need to map back to the 6-bit values they represent. + // There are three ranges, two singles, and then there's the rest. + // + // # From To Add Characters + // 1 [43] [62] +19 + + // 2 [47] [63] +16 / + // 3 [48..57] [52..61] +4 0..9 + // 4 [65..90] [0..25] -65 A..Z + // 5 [97..122] [26..51] -71 a..z + // (6) Everything else => invalid input + + // We will use LUTS for character validation & offset computation + // Remember that 0x2X and 0x0X are the same index for _mm_shuffle_epi8, + // this allows to mask with 0x2F instead of 0x0F and thus save one constant declaration (register and/or memory access) + + // For offsets: + // Perfect hash for lut = ((src>>4)&0x2F)+((src==0x2F)?0xFF:0x00) + // 0000 = garbage + // 0001 = / + // 0010 = + + // 0011 = 0-9 + // 0100 = A-Z + // 0101 = A-Z + // 0110 = a-z + // 0111 = a-z + // 1000 >= garbage + + // For validation, here's the table. + // A character is valid if and only if the AND of the 2 lookups equals 0: + + // hi \ lo 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 + // LUT 0x15 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x13 0x1A 0x1B 0x1B 0x1B 0x1A + + // 0000 0X10 char NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI + // andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + + // 0001 0x10 char DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US + // andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + + // 0010 0x01 char ! " # $ % & ' ( ) * + , - . / + // andlut 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x00 0x01 0x01 0x01 0x00 + + // 0011 0x02 char 0 1 2 3 4 5 6 7 8 9 : ; < = > ? + // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x02 0x02 0x02 0x02 0x02 + + // 0100 0x04 char @ A B C D E F G H I J K L M N 0 + // andlut 0x04 0x00 0x00 0x00 0X00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + + // 0101 0x08 char P Q R S T U V W X Y Z [ \ ] ^ _ + // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08 + + // 0110 0x04 char ` a b c d e f g h i j k l m n o + // andlut 0x04 0x00 0x00 0x00 0X00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + // 0111 0X08 char p q r s t u v w x y z { | } ~ + // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08 + + // 1000 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1001 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1010 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1011 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1100 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1101 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1110 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + // 1111 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 + + // The JIT won't hoist these "constants", so help it + Vector128 lutHi = Vector128.Create(TBase64Decoder.Vector128LutHigh).AsByte(); + Vector128 lutLo = Vector128.Create(TBase64Decoder.Vector128LutLow).AsByte(); + Vector128 lutShift = Vector128.Create(TBase64Decoder.Vector128LutShift).AsSByte(); + Vector128 packBytesMask = Vector128.Create(0x06000102, 0x090A0405, 0x0C0D0E08, 0xffffffff).AsSByte(); + Vector128 mergeConstant0 = Vector128.Create(0x01400140).AsByte(); + Vector128 mergeConstant1 = Vector128.Create(0x00011000).AsInt16(); + Vector128 one = Vector128.Create((byte)1); + Vector128 mask2F = Vector128.Create(TBase64Decoder.MaskSlashOrUnderscore); + Vector128 mask8F = Vector128.Create((byte)0x8F); + Vector128 shiftForUnderscore = Vector128.Create((byte)33); + ushort* src = (ushort*)srcBytes; + byte* dest = destBytes; + + //while (remaining >= 24) + do + { + AssertRead>(src, srcStart, sourceLength); + Vector128 utf16VectorLower = Vector128.LoadUnsafe(ref *src); + Vector128 utf16VectorUpper = Vector128.LoadUnsafe(ref *src, 8); + Vector128 str = Vector128.Narrow(utf16VectorLower, utf16VectorUpper); + + // lookup + Vector128 hiNibbles = Vector128.ShiftRightLogical(str.AsInt32(), 4).AsByte() & mask2F; + + if (!TBase64Decoder.TryDecode128Core(str, hiNibbles, mask2F, mask8F, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) + { + break; + } + + // in, bits, upper case are most significant bits, lower case are least significant bits + // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ + // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG + // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD + // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA + + Vector128 merge_ab_and_bc; + if (Ssse3.IsSupported) + { + merge_ab_and_bc = Ssse3.MultiplyAddAdjacent(str.AsByte(), mergeConstant0.AsSByte()); + } + else + { + Vector128 evens = AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(str, one).GetLower(), 6); + Vector128 odds = AdvSimd.Arm64.TransposeOdd(str, Vector128.Zero).AsUInt16(); + merge_ab_and_bc = Vector128.Add(evens, odds).AsInt16(); + } + // 0000kkkk LLllllll 0000JJJJ JJjjKKKK + // 0000hhhh IIiiiiii 0000GGGG GGggHHHH + // 0000eeee FFffffff 0000DDDD DDddEEEE + // 0000bbbb CCcccccc 0000AAAA AAaaBBBB + + Vector128 output; + if (Ssse3.IsSupported) + { + output = Sse2.MultiplyAddAdjacent(merge_ab_and_bc, mergeConstant1); + } + else + { + Vector128 ievens = AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(merge_ab_and_bc, one.AsInt16()).GetLower(), 12); + Vector128 iodds = AdvSimd.Arm64.TransposeOdd(merge_ab_and_bc, Vector128.Zero).AsInt32(); + output = Vector128.Add(ievens, iodds).AsInt32(); + } + // 00000000 JJJJJJjj KKKKkkkk LLllllll + // 00000000 GGGGGGgg HHHHhhhh IIiiiiii + // 00000000 DDDDDDdd EEEEeeee FFffffff + // 00000000 AAAAAAaa BBBBbbbb CCcccccc + + // Pack bytes together: + str = SimdShuffle(output.AsByte(), packBytesMask.AsByte(), mask8F); + // 00000000 00000000 00000000 00000000 + // LLllllll KKKKkkkk JJJJJJjj IIiiiiii + // HHHHhhhh GGGGGGgg FFffffff EEEEeeee + // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa + + Base64.AssertWrite>(dest, destStart, destLength); + str.Store(dest); + + src += 16; + dest += 12; + } + while (src <= srcEnd); + + srcBytes = (char*)src; + destBytes = dest; + } + + [Conditional("DEBUG")] + private static unsafe void AssertRead(ushort* src, ushort* srcStart, int srcLength) + { + int vectorElements = Unsafe.SizeOf(); + ushort* readEnd = src + vectorElements; + ushort* srcEnd = srcStart + srcLength; + + if (readEnd > srcEnd) + { + int srcIndex = (int)(src - srcStart); + Debug.Fail($"Read for {typeof(TVector)} is not within safe bounds. srcIndex: {srcIndex}, srcLength: {srcLength}"); + } + } + + internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder + { + const int BlockSize = 4; + Span buffer = stackalloc char[BlockSize]; + OperationStatus status = OperationStatus.Done; + + while (!utf8.IsEmpty) + { + int encodedIdx = 0; + int bufferIdx = 0; + int skipped = 0; + + for (; encodedIdx < utf8.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) + { + if (IsWhiteSpace(utf8[encodedIdx])) + { + skipped++; + } + else + { + buffer[bufferIdx] = utf8[encodedIdx]; + bufferIdx++; + } + } + + utf8 = utf8.Slice(encodedIdx); + bytesConsumed += skipped; + + if (bufferIdx == 0) + { + continue; + } + + bool hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + bool localIsFinalBlock = !hasAnotherBlock; + + // If this block contains padding and there's another block, then only whitespace may follow for being valid. + if (hasAnotherBlock) + { + int paddingCount = GetPaddingCount(ref buffer[^1]); + if (paddingCount > 0) + { + hasAnotherBlock = false; + localIsFinalBlock = true; + } + } + + if (localIsFinalBlock && !isFinalBlock) + { + localIsFinalBlock = false; + } + + status = DecodeFromChars(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); + bytesConsumed += localConsumed; + bytesWritten += localWritten; + + if (status != OperationStatus.Done) + { + return status; + } + + // The remaining data must all be whitespace in order to be valid. + if (!hasAnotherBlock) + { + for (int i = 0; i < utf8.Length; ++i) + { + if (!IsWhiteSpace(utf8[i])) + { + // Revert previous dest increment, since an invalid state followed. + bytesConsumed -= localConsumed; + bytesWritten -= localWritten; + + return OperationStatus.InvalidData; + } + + bytesConsumed++; + } + + break; + } + + bytes = bytes.Slice(localWritten); + } + + return status; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPaddingCount(ref char ptrToLastElement) + { + int padding = 0; + + if (ptrToLastElement == EncodingPad) padding++; + if (Unsafe.Subtract(ref ptrToLastElement, 1) == EncodingPad) padding++; + + return padding; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!IsWhiteSpace(span[i])) + { + return i; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe int Decode(char* encodedBytes, ref sbyte decodingMap) + { + uint t0 = encodedBytes[0]; + uint t1 = encodedBytes[1]; + uint t2 = encodedBytes[2]; + uint t3 = encodedBytes[3]; + + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + { + return -1; // One or more chars falls outside the 00..ff range, invalid Base64Url character. + } + + int i0 = Unsafe.Add(ref decodingMap, t0); + int i1 = Unsafe.Add(ref decodingMap, t1); + int i2 = Unsafe.Add(ref decodingMap, t2); + int i3 = Unsafe.Add(ref decodingMap, t3); + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; + } public static int DecodeFromChars(ReadOnlySpan source, Span destination) { @@ -131,7 +940,9 @@ public static int DecodeFromChars(ReadOnlySpan source, Span destinat public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) { - return TryDecodeFromUtf8(MemoryMarshal.AsBytes(source), destination, out bytesWritten); + OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); + + return OperationStatus.Done == status; } public static byte[] DecodeFromChars(ReadOnlySpan source) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 696eb9cfa58032..2b2601c23d29c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -5,6 +5,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using System.Text; using static System.Buffers.Text.Base64; namespace System.Buffers.Text @@ -111,7 +114,633 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) => - EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out bytesConsumed, out charsWritten, isFinalBlock); + EncodeToChars(source, destination, out bytesConsumed, out charsWritten, isFinalBlock); + + internal static unsafe OperationStatus EncodeToChars(ReadOnlySpan bytes, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + where TBase64Encoder : IBase64Encoder + { + if (bytes.IsEmpty) + { + bytesConsumed = 0; + bytesWritten = 0; + return OperationStatus.Done; + } + + fixed (byte* srcBytes = &MemoryMarshal.GetReference(bytes)) + fixed (char* destBytes = &MemoryMarshal.GetReference(destination)) + { + int srcLength = bytes.Length; + int destLength = destination.Length; + int maxSrcLength = TBase64Encoder.GetMaxSrcLength(srcLength, destLength); + + byte* src = srcBytes; + char* dest = destBytes; + byte* srcEnd = srcBytes + (uint)srcLength; + byte* srcMax = srcBytes + (uint)maxSrcLength; + + if (maxSrcLength >= 16) + { + byte* end = srcMax - 64; + if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) + { + Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); + + if (src == srcEnd) + goto DoneExit; + } + + end = srcMax - 32; + if (Avx2.IsSupported && (end >= src)) + { + Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); + + if (src == srcEnd) + goto DoneExit; + } + + end = srcMax - 48; + if (AdvSimd.Arm64.IsSupported && (end >= src)) + { + AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); + + if (src == srcEnd) + goto DoneExit; + } + + end = srcMax - 16; + if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) + { + Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); + + if (src == srcEnd) + goto DoneExit; + } + } + + ref byte encodingMap = ref MemoryMarshal.GetReference(TBase64Encoder.EncodingMap); + uint result = 0; + + srcMax -= 2; + while (src < srcMax) + { + EncodeAndWrite(src, dest, ref encodingMap); + src += 3; + dest += 4; + } + + if (srcMax + 2 != srcEnd) + goto DestinationTooSmallExit; + + if (!isFinalBlock) + { + if (src == srcEnd) + goto DoneExit; + + goto NeedMoreData; + } + + if (src + 1 == srcEnd) + { + result = TBase64Encoder.EncodeOneOptionallyPadTwo(src, ref encodingMap); + if (BitConverter.IsLittleEndian) + { + dest[0] = (char)(result & 0x7F); + dest[1] = (char)(result >> 8); + } + else + { + dest[0] = (char)(result >> 8); + dest[1] = (char)(result & 0x7F); + } + + src += 1; + dest += TBase64Encoder.IncrementPadTwo; + } + else if (src + 2 == srcEnd) + { + result = TBase64Encoder.EncodeTwoOptionallyPadOne(src, ref encodingMap); + if (BitConverter.IsLittleEndian) + { + dest[0] = (char)(result & 0x7F); + dest[1] = (char)((result >> 8) & 0x7F); + dest[2] = (char)(result >> 16); + } + else + { + dest[0] = (char)(result >> 16); + dest[1] = (char)((result >> 8) & 0x7F); + dest[2] = (char)(result & 0x7F); + } + + src += 2; + dest += TBase64Encoder.IncrementPadOne; + } + + DoneExit: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.Done; + + DestinationTooSmallExit: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.DestinationTooSmall; + + NeedMoreData: + bytesConsumed = (int)(src - srcBytes); + bytesWritten = (int)(dest - destBytes); + return OperationStatus.NeedMoreData; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx512BW))] + [CompExactlyDependsOn(typeof(Avx512Vbmi))] + private static unsafe void Avx512Encode(ref byte* srcBytes, ref char* destBytes, + byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) + where TBase64Encoder : IBase64Encoder + { + // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/encode + // If we have AVX512 support, pick off 48 bytes at a time for as long as we can. + // But because we read 64 bytes at a time, ensure we have enough room to do a + // full 64-byte read without segfaulting. + + byte* src = srcBytes; + ushort* dest = (ushort*)destBytes; + + // The JIT won't hoist these "constants", so help it + Vector512 shuffleVecVbmi = Vector512.Create( + 0x01020001, 0x04050304, 0x07080607, 0x0a0b090a, + 0x0d0e0c0d, 0x10110f10, 0x13141213, 0x16171516, + 0x191a1819, 0x1c1d1b1c, 0x1f201e1f, 0x22232122, + 0x25262425, 0x28292728, 0x2b2c2a2b, 0x2e2f2d2e).AsSByte(); + Vector512 vbmiLookup = Vector512.Create(TBase64Encoder.EncodingMap).AsSByte(); + + Vector512 maskAC = Vector512.Create((uint)0x0fc0fc00).AsUInt16(); + Vector512 maskBB = Vector512.Create((uint)0x3f003f00); + Vector512 shiftAC = Vector512.Create((uint)0x0006000a).AsUInt16(); + Vector512 shiftBB = Vector512.Create((uint)0x00080004).AsUInt16(); + + Base64.AssertRead>(src, srcStart, sourceLength); + + // This algorithm requires AVX512VBMI support. + // Vbmi was first introduced in CannonLake and is available from IceLake on. + + // str = [...|PONM|LKJI|HGFE|DCBA] + Vector512 str = Vector512.Load(src).AsSByte(); + + while (true) + { + // Step 1 : Split 48 bytes into 64 bytes with each byte using 6-bits from input + // str = [...|KLJK|HIGH|EFDE|BCAB] + str = Avx512Vbmi.PermuteVar64x8(str, shuffleVecVbmi); + + // TO-DO- This can be achieved faster with multishift + // Consider the first 4 bytes - BCAB + // temp1 = [...|0000cccc|cc000000|aaaaaa00|00000000] + Vector512 temp1 = (str.AsUInt16() & maskAC); + + // temp2 = [...|00000000|00cccccc|00000000|00aaaaaa] + Vector512 temp2 = Avx512BW.ShiftRightLogicalVariable(temp1, shiftAC).AsUInt16(); + + // temp3 = [...|ccdddddd|00000000|aabbbbbb|cccc0000] + Vector512 temp3 = Avx512BW.ShiftLeftLogicalVariable(str.AsUInt16(), shiftBB).AsUInt16(); + + // str = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] + str = Vector512.ConditionalSelect(maskBB, temp3.AsUInt32(), temp2.AsUInt32()).AsSByte(); + + // Step 2: Now we have the indices calculated. Next step is to use these indices to translate. + str = Avx512Vbmi.PermuteVar64x8(vbmiLookup, str); + + AssertWrite>(dest, destStart, destLength); + (Vector512 utf16LowVector, Vector512 utf16HighVector) = Vector512.Widen(str.AsByte()); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector512.Count); + + src += 48; + dest += 64; + + if (src > srcEnd) + break; + + Base64.AssertRead>(src, srcStart, sourceLength); + str = Vector512.Load(src).AsSByte(); + } + + srcBytes = src; + destBytes = (char*)dest; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + private static unsafe void Avx2Encode(ref byte* srcBytes, ref char* destBytes, + byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) + where TBase64Encoder : IBase64Encoder + { + // If we have AVX2 support, pick off 24 bytes at a time for as long as we can. + // But because we read 32 bytes at a time, ensure we have enough room to do a + // full 32-byte read without segfaulting. + + // translation from SSSE3 into AVX2 of procedure + // This one works with shifted (4 bytes) input in order to + // be able to work efficiently in the 2 128-bit lanes + + // srcBytes, bytes MSB to LSB: + // 0 0 0 0 x w v u t s r q p o n m + // l k j i h g f e d c b a 0 0 0 0 + + // The JIT won't hoist these "constants", so help it + Vector256 shuffleVec = Vector256.Create( + 5, 4, 6, 5, + 8, 7, 9, 8, + 11, 10, 12, 11, + 14, 13, 15, 14, + 1, 0, 2, 1, + 4, 3, 5, 4, + 7, 6, 8, 7, + 10, 9, 11, 10); + + Vector256 lut = Vector256.Create( + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0, + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0); + + Vector256 maskAC = Vector256.Create(0x0fc0fc00).AsSByte(); + Vector256 maskBB = Vector256.Create(0x003f03f0).AsSByte(); + Vector256 shiftAC = Vector256.Create(0x04000040).AsUInt16(); + Vector256 shiftBB = Vector256.Create(0x01000010).AsInt16(); + Vector256 const51 = Vector256.Create((byte)51); + Vector256 const25 = Vector256.Create((sbyte)25); + + byte* src = srcBytes; + ushort* dest = (ushort*)destBytes; + + // first load is done at c-0 not to get a segfault + Base64.AssertRead>(src, srcStart, sourceLength); + Vector256 str = Avx.LoadVector256(src).AsSByte(); + + // shift by 4 bytes, as required by Reshuffle + str = Avx2.PermuteVar8x32(str.AsInt32(), Vector256.Create( + 0, 0, 0, 0, + 0, 0, 0, 0, + 1, 0, 0, 0, + 2, 0, 0, 0, + 3, 0, 0, 0, + 4, 0, 0, 0, + 5, 0, 0, 0, + 6, 0, 0, 0).AsInt32()).AsSByte(); + + // Next loads are done at src-4, as required by Reshuffle, so shift it once + src -= 4; + + while (true) + { + // Reshuffle + str = Avx2.Shuffle(str, shuffleVec); + // str, bytes MSB to LSB: + // w x v w + // t u s t + // q r p q + // n o m n + // k l j k + // h i g h + // e f d e + // b c a b + + Vector256 t0 = Avx2.And(str, maskAC); + // bits, upper case are most significant bits, lower case are least significant bits. + // 0000wwww XX000000 VVVVVV00 00000000 + // 0000tttt UU000000 SSSSSS00 00000000 + // 0000qqqq RR000000 PPPPPP00 00000000 + // 0000nnnn OO000000 MMMMMM00 00000000 + // 0000kkkk LL000000 JJJJJJ00 00000000 + // 0000hhhh II000000 GGGGGG00 00000000 + // 0000eeee FF000000 DDDDDD00 00000000 + // 0000bbbb CC000000 AAAAAA00 00000000 + + Vector256 t2 = Avx2.And(str, maskBB); + // 00000000 00xxxxxx 000000vv WWWW0000 + // 00000000 00uuuuuu 000000ss TTTT0000 + // 00000000 00rrrrrr 000000pp QQQQ0000 + // 00000000 00oooooo 000000mm NNNN0000 + // 00000000 00llllll 000000jj KKKK0000 + // 00000000 00iiiiii 000000gg HHHH0000 + // 00000000 00ffffff 000000dd EEEE0000 + // 00000000 00cccccc 000000aa BBBB0000 + + Vector256 t1 = Avx2.MultiplyHigh(t0.AsUInt16(), shiftAC); + // 00000000 00wwwwXX 00000000 00VVVVVV + // 00000000 00ttttUU 00000000 00SSSSSS + // 00000000 00qqqqRR 00000000 00PPPPPP + // 00000000 00nnnnOO 00000000 00MMMMMM + // 00000000 00kkkkLL 00000000 00JJJJJJ + // 00000000 00hhhhII 00000000 00GGGGGG + // 00000000 00eeeeFF 00000000 00DDDDDD + // 00000000 00bbbbCC 00000000 00AAAAAA + + Vector256 t3 = Avx2.MultiplyLow(t2.AsInt16(), shiftBB); + // 00xxxxxx 00000000 00vvWWWW 00000000 + // 00uuuuuu 00000000 00ssTTTT 00000000 + // 00rrrrrr 00000000 00ppQQQQ 00000000 + // 00oooooo 00000000 00mmNNNN 00000000 + // 00llllll 00000000 00jjKKKK 00000000 + // 00iiiiii 00000000 00ggHHHH 00000000 + // 00ffffff 00000000 00ddEEEE 00000000 + // 00cccccc 00000000 00aaBBBB 00000000 + + str = Avx2.Or(t1.AsSByte(), t3.AsSByte()); + // 00xxxxxx 00wwwwXX 00vvWWWW 00VVVVVV + // 00uuuuuu 00ttttUU 00ssTTTT 00SSSSSS + // 00rrrrrr 00qqqqRR 00ppQQQQ 00PPPPPP + // 00oooooo 00nnnnOO 00mmNNNN 00MMMMMM + // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ + // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG + // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD + // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA + + // Translation + // LUT contains Absolute offset for all ranges: + // Translate values 0..63 to the Base64 alphabet. There are five sets: + // # From To Abs Index Characters + // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ + // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz + // 2 [52..61] [48..57] -4 [2..11] 0123456789 + // 3 [62] [43] -19 12 + + // 4 [63] [47] -16 13 / + + // Create LUT indices from input: + // the index for range #0 is right, others are 1 less than expected: + Vector256 indices = Avx2.SubtractSaturate(str.AsByte(), const51); + + // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0: + Vector256 mask = Avx2.CompareGreaterThan(str, const25); + + // subtract -1, so add 1 to indices for range #[1..4], All indices are now correct: + Vector256 tmp = Avx2.Subtract(indices.AsSByte(), mask); + + // Add offsets to input values: + str = Avx2.Add(str, Avx2.Shuffle(lut, tmp)); + + AssertWrite>(dest, destStart, destLength); + (Vector256 utf16LowVector, Vector256 utf16HighVector) = Vector256.Widen(str.AsByte()); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector256.Count); + + src += 24; + dest += 32; + + if (src > srcEnd) + break; + + // Load at src-4, as required by Reshuffle (already shifted by -4) + Base64.AssertRead>(src, srcStart, sourceLength); + str = Avx.LoadVector256(src).AsSByte(); + } + + srcBytes = src + 4; + destBytes = (char*)dest; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref char* destBytes, + byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) + where TBase64Encoder : IBase64Encoder + { + // C# implementation of https://github.com/aklomp/base64/blob/3a5add8652076612a8407627a42c768736a4263f/lib/arch/neon64/enc_loop.c + Vector128 str1; + Vector128 str2; + Vector128 str3; + Vector128 res1; + Vector128 res2; + Vector128 res3; + Vector128 res4; + Vector128 tblEnc1 = Vector128.Create("ABCDEFGHIJKLMNOP"u8).AsByte(); + Vector128 tblEnc2 = Vector128.Create("QRSTUVWXYZabcdef"u8).AsByte(); + Vector128 tblEnc3 = Vector128.Create("ghijklmnopqrstuv"u8).AsByte(); + Vector128 tblEnc4 = Vector128.Create(TBase64Encoder.AdvSimdLut4).AsByte(); + byte* src = srcBytes; + ushort* dest = (ushort*)destBytes; + + // If we have Neon support, pick off 48 bytes at a time for as long as we can. + do + { + // Load 48 bytes and deinterleave: + Base64.AssertRead>(src, srcStart, sourceLength); + (str1, str2, str3) = AdvSimd.Arm64.LoadVector128x3AndUnzip(src); + + // Divide bits of three input bytes over four output bytes: + res1 = AdvSimd.ShiftRightLogical(str1, 2); + res2 = AdvSimd.ShiftRightLogical(str2, 4); + res3 = AdvSimd.ShiftRightLogical(str3, 6); + res2 = AdvSimd.ShiftLeftAndInsert(res2, str1, 4); + res3 = AdvSimd.ShiftLeftAndInsert(res3, str2, 2); + + // Clear top two bits: + res2 &= AdvSimd.DuplicateToVector128((byte)0x3F); + res3 &= AdvSimd.DuplicateToVector128((byte)0x3F); + res4 = str3 & AdvSimd.DuplicateToVector128((byte)0x3F); + + // The bits have now been shifted to the right locations; + // translate their values 0..63 to the Base64 alphabet. + // Use a 64-byte table lookup: + res1 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res1); + res2 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res2); + res3 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res3); + res4 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res4); + + // Interleave and store result: + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); + (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); + AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); + (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); + (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); + AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); + + src += 48; + dest += 64; + } while (src <= srcEnd); + + srcBytes = src; + destBytes = (char*)dest; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Ssse3))] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + private static unsafe void Vector128Encode(ref byte* srcBytes, ref char* destBytes, + byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) + where TBase64Encoder : IBase64Encoder + { + // If we have SSSE3 support, pick off 12 bytes at a time for as long as we can. + // But because we read 16 bytes at a time, ensure we have enough room to do a + // full 16-byte read without segfaulting. + + // srcBytes, bytes MSB to LSB: + // 0 0 0 0 l k j i h g f e d c b a + + // The JIT won't hoist these "constants", so help it + Vector128 shuffleVec = Vector128.Create(0x01020001, 0x04050304, 0x07080607, 0x0A0B090A).AsByte(); + Vector128 lut = Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, TBase64Encoder.Ssse3AdvSimdLutE3).AsByte(); + Vector128 maskAC = Vector128.Create(0x0fc0fc00).AsByte(); + Vector128 maskBB = Vector128.Create(0x003f03f0).AsByte(); + Vector128 shiftAC = Vector128.Create(0x04000040).AsUInt16(); + Vector128 shiftBB = Vector128.Create(0x01000010).AsInt16(); + Vector128 const51 = Vector128.Create((byte)51); + Vector128 const25 = Vector128.Create((sbyte)25); + Vector128 mask8F = Vector128.Create((byte)0x8F); + + byte* src = srcBytes; + ushort* dest = (ushort*)destBytes; + + //while (remaining >= 16) + do + { + Base64.AssertRead>(src, srcStart, sourceLength); + Vector128 str = Vector128.LoadUnsafe(ref *src); + + // Reshuffle + str = SimdShuffle(str, shuffleVec, mask8F); + // str, bytes MSB to LSB: + // k l j k + // h i g h + // e f d e + // b c a b + + Vector128 t0 = str & maskAC; + // bits, upper case are most significant bits, lower case are least significant bits + // 0000kkkk LL000000 JJJJJJ00 00000000 + // 0000hhhh II000000 GGGGGG00 00000000 + // 0000eeee FF000000 DDDDDD00 00000000 + // 0000bbbb CC000000 AAAAAA00 00000000 + + Vector128 t2 = str & maskBB; + // 00000000 00llllll 000000jj KKKK0000 + // 00000000 00iiiiii 000000gg HHHH0000 + // 00000000 00ffffff 000000dd EEEE0000 + // 00000000 00cccccc 000000aa BBBB0000 + + Vector128 t1; + if (Ssse3.IsSupported) + { + t1 = Sse2.MultiplyHigh(t0.AsUInt16(), shiftAC); + } + else + { + Vector128 odd = Vector128.ShiftRightLogical(AdvSimd.Arm64.UnzipOdd(t0.AsUInt16(), t0.AsUInt16()), 6); + Vector128 even = Vector128.ShiftRightLogical(AdvSimd.Arm64.UnzipEven(t0.AsUInt16(), t0.AsUInt16()), 10); + t1 = AdvSimd.Arm64.ZipLow(even, odd); + } + // 00000000 00kkkkLL 00000000 00JJJJJJ + // 00000000 00hhhhII 00000000 00GGGGGG + // 00000000 00eeeeFF 00000000 00DDDDDD + // 00000000 00bbbbCC 00000000 00AAAAAA + + Vector128 t3 = t2.AsInt16() * shiftBB; + // 00llllll 00000000 00jjKKKK 00000000 + // 00iiiiii 00000000 00ggHHHH 00000000 + // 00ffffff 00000000 00ddEEEE 00000000 + // 00cccccc 00000000 00aaBBBB 00000000 + + str = t1.AsByte() | t3.AsByte(); + // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ + // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG + // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD + // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA + + // Translation + // LUT contains Absolute offset for all ranges: + // Translate values 0..63 to the Base64 alphabet. There are five sets: + // # From To Abs Index Characters + // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ + // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz + // 2 [52..61] [48..57] -4 [2..11] 0123456789 + // 3 [62] [43] -19 12 + + // 4 [63] [47] -16 13 / + + // Create LUT indices from input: + // the index for range #0 is right, others are 1 less than expected: + Vector128 indices; + if (Ssse3.IsSupported) + { + indices = Sse2.SubtractSaturate(str.AsByte(), const51); + } + else + { + indices = AdvSimd.SubtractSaturate(str.AsByte(), const51); + } + + // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0: + Vector128 mask = Vector128.GreaterThan(str.AsSByte(), const25); + + // subtract -1, so add 1 to indices for range #[1..4], All indices are now correct: + Vector128 tmp = indices.AsSByte() - mask; + + // Add offsets to input values: + str += SimdShuffle(lut, tmp.AsByte(), mask8F); + + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector, Vector128 utf16HighVector) = Vector128.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector128.Count); + + src += 12; + dest += 16; + } + while (src <= srcEnd); + + srcBytes = src; + destBytes = (char*)dest; + } + + private static unsafe void AssertWrite(ushort* dest, ushort* destStart, int destLength) + { + int vectorElements = Unsafe.SizeOf(); + ushort* writeEnd = dest + vectorElements; + ushort* destEnd = destStart + destLength; + + if (writeEnd > destEnd) + { + int destIndex = (int)(dest - destStart); + Debug.Fail($"Write for {typeof(TVector)} is not within safe bounds. destIndex: {destIndex}, destLength: {destLength}"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void EncodeAndWrite(byte* threeBytes, char* destination, ref byte encodingMap) + { + uint t0 = threeBytes[0]; + uint t1 = threeBytes[1]; + uint t2 = threeBytes[2]; + + uint i = (t0 << 16) | (t1 << 8) | t2; + + byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + destination[0] = (char)i0; + destination[1] = (char)i1; + destination[2] = (char)i2; + destination[3] = (char)i3; + } + else + { + destination[0] = (char)i3; + destination[1] = (char)i2; + destination[2] = (char)i1; + destination[3] = (char)i0; + } + } /// /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. @@ -122,7 +751,7 @@ public static OperationStatus EncodeToChars(ReadOnlySpan source, SpanThe output will not be padded even if the input is not a multiple of 3. public static int EncodeToChars(ReadOnlySpan source, Span destination) { - OperationStatus status = EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int charsWritten); + OperationStatus status = EncodeToChars(source, destination, out _, out int charsWritten); if (OperationStatus.Done == status) { @@ -146,15 +775,10 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio /// The output will not be padded even if the input is not a multiple of 3. public static char[] EncodeToChars(ReadOnlySpan source) { - if (source.Length == 0) - { - return Array.Empty(); - } - Span destination = stackalloc char[GetEncodedLength(source.Length)]; - EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out int charsWritten); + EncodeToChars(source, destination, out _, out _); - return destination.Slice(0, charsWritten).ToArray(); + return destination.ToArray(); } /// @@ -165,15 +789,10 @@ public static char[] EncodeToChars(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static string EncodeToString(ReadOnlySpan source) { - if (source.Length == 0) - { - return string.Empty; - } - - Span destination = stackalloc byte[GetEncodedLength(source.Length)]; - EncodeToUtf8(source, destination, out _, out int charsWritten); + Span destination = stackalloc char[GetEncodedLength(source.Length)]; + EncodeToChars(source, destination, out _, out _); - return destination.Slice(0, charsWritten).ToString(); // Encoding.UTF8.GetString(utf8.Slice(0, bytesWritten)) + return new string(destination); } /// @@ -186,7 +805,7 @@ public static string EncodeToString(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static bool TryEncodeToChars(ReadOnlySpan source, Span destination, out int charsWritten) { - OperationStatus status = EncodeToUtf8(source, MemoryMarshal.AsBytes(destination), out _, out charsWritten); + OperationStatus status = EncodeToChars(source, destination, out _, out charsWritten); return status == OperationStatus.Done; } From 2fe5d49355c59df577396eba127b0d25d24c968f Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 16 May 2024 15:28:18 -0700 Subject: [PATCH 07/38] Generalize Span char/byte implementations --- .../tests/Base64/Base64ValidationUnitTests.cs | 32 +- .../Base64Url/Base64UrlDecoderUnitTests.cs | 90 +- .../Base64UrlEncodingAPIsUnitTests.cs | 40 +- .../Base64Url/Base64UrlValidationUnitTests.cs | 59 +- .../src/System/Buffers/Text/Base64.cs | 55 +- .../src/System/Buffers/Text/Base64Decoder.cs | 235 ++- .../src/System/Buffers/Text/Base64Encoder.cs | 204 ++- .../Text/Base64Url/Base64UrlDecoder.cs | 1378 ++++++++--------- .../Text/Base64Url/Base64UrlEncoder.cs | 898 +++-------- .../Text/Base64Url/Base64UrlValidator.cs | 2 +- .../System/Buffers/Text/Base64Validator.cs | 2 +- 11 files changed, 1302 insertions(+), 1693 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs index ba08c79e53f226..54149adedc3ae9 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs @@ -69,6 +69,7 @@ public void BasicValidationInvalidInputLengthBytes() } while (numBytes % 4 == 0); // ensure we have a invalid length Span source = new byte[numBytes]; + Base64TestHelper.InitializeDecodableBytes(source, numBytes); Assert.False(Base64.IsValid(source)); Assert.False(Base64.IsValid(source, out int decodedLength)); @@ -88,10 +89,16 @@ public void BasicValidationInvalidInputLengthChars() numBytes = rnd.Next(100, 1000 * 1000); } while (numBytes % 4 == 0); // ensure we have a invalid length - Span source = new char[numBytes]; + Span source = new byte[numBytes]; + Base64TestHelper.InitializeDecodableBytes(source, numBytes); + Span chars = source + .ToArray() + .Select(Convert.ToChar) + .ToArray() + .AsSpan(); - Assert.False(Base64.IsValid(source)); - Assert.False(Base64.IsValid(source, out int decodedLength)); + Assert.False(Base64.IsValid(chars)); + Assert.False(Base64.IsValid(chars, out int decodedLength)); Assert.Equal(0, decodedLength); } } @@ -235,19 +242,6 @@ public void ValidateWithPaddingReturnsCorrectCountChars(string utf8WithByteToBeI Assert.Equal(expectedLength, decodedLength); } - [Theory] - [InlineData("YQ==", 1)] - [InlineData("YWI=", 2)] - [InlineData("YWJj", 3)] - public void DecodeEmptySpan(string utf8WithByteToBeIgnored, int expectedLength) - { - ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); - - Assert.True(Base64.IsValid(utf8BytesWithByteToBeIgnored)); - Assert.True(Base64.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); - Assert.Equal(expectedLength, decodedLength); - } - [Theory] [InlineData("YWJ")] [InlineData("YW")] @@ -329,10 +323,10 @@ public void InvalidBase64Bytes(string utf8WithByteToBeIgnored) [InlineData(" a ")] public void InvalidBase64Chars(string utf8WithByteToBeIgnored) { - byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + ReadOnlySpan utf8CharsWithCharToBeIgnored = utf8WithByteToBeIgnored; - Assert.False(Base64.IsValid(utf8BytesWithByteToBeIgnored)); - Assert.False(Base64.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.False(Base64.IsValid(utf8CharsWithCharToBeIgnored)); + Assert.False(Base64.IsValid(utf8CharsWithCharToBeIgnored, out int decodedLength)); Assert.Equal(0, decodedLength); } } diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index d3cf02f5f035b2..1843286032fc39 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -80,7 +80,7 @@ public void BasicDecodingWithFinalBlockFalse() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 != 0); // ensure we have a valid length + } while (numBytes % 4 != 0); // ensure we have a complete length Span source = new byte[numBytes]; Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); @@ -105,7 +105,7 @@ public void BasicDecodingWithFinalBlockFalseInvalidInputLength() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 == 0); // ensure we have a invalid length + } while (numBytes % 4 == 0); // ensure we have a incomplete length Span source = new byte[numBytes]; Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); @@ -271,7 +271,7 @@ public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, in [Theory] [InlineData("A", 0, 0, OperationStatus.InvalidData)] - [InlineData("AQ", 2, 1, OperationStatus.Done)] + [InlineData("AQ", 2, 1, OperationStatus.Done)] // Padding is optional [InlineData("AQI", 3, 2, OperationStatus.Done)] [InlineData("AQIDBA", 6, 4, OperationStatus.Done)] [InlineData("AQIDBAU", 7, 5, OperationStatus.Done)] @@ -287,12 +287,14 @@ public void BasicDecodingWithFinalBlockTrueInputWithoutPadding(string inputStrin } [Theory] - [InlineData("\u00ecz_T", 0, 0)] // scalar code-path - [InlineData("z_Ta123\u00ec", 4, 3)] - [InlineData("\u00ecz_T-H7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path - [InlineData("z_T-H7sqEkerqMweH1uSw\u00ec==", 20, 15)] - [InlineData("\u00ecz_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path - [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u00ec==", 44, 33)] + [InlineData("\u5948cz_T", 0, 0)] // scalar code-path + [InlineData("z_Ta123\u5948", 4, 3)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path + [InlineData("z_T-H7sqEkerqMweH1uSw\u5948==", 20, 15)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u5948==", 44, 33)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, 0)] // Vector512 / Avx512Vbmi code-path + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789\u5948==", 92, 69)] public void BasicDecodingNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten) { Span source = Encoding.UTF8.GetBytes(inputString); @@ -320,8 +322,8 @@ public void BasicDecodingWithFinalBlockFalseKnownInputDone(string inputString, i [Theory] [InlineData("A", 0, 0)] - [InlineData("AQ", 0, 0)] - [InlineData("AQI", 0, 0)] + [InlineData("AQ", 0, 0)] // when FinalBlock: false incomplete bytes ignored + [InlineData("AQI", 0, 0)] [InlineData("AQIDB", 4, 3)] [InlineData("AQIDBA", 4, 3)] [InlineData("AQIDBAU", 4, 3)] @@ -400,7 +402,7 @@ public void DecodingInvalidBytes(bool isFinalBlock) } } - // Input that is not a multiple of 4 is not considered invalid, even if isFinalBlock = true + // When isFinalBlock = true input that is not a multiple of 4 is invalid for Base64, but valid for Base64Url if (isFinalBlock) { Span source = "2222PPP"u8.ToArray(); // incomplete input @@ -567,7 +569,7 @@ public void EncodeAndDecodeInPlace() } [Fact] - public void DecodeInPlaceInvalidBytes() + public void DecodeInPlaceInvalidBytesThrowsFormatException() { byte[] invalidBytes = Base64TestHelper.UrlInvalidBytes; @@ -575,7 +577,7 @@ public void DecodeInPlaceInvalidBytes() { for (int i = 0; i < invalidBytes.Length; i++) { - Span buffer = "2222PPPP"u8.ToArray(); // valid input + byte[] buffer = "2222PPPP"u8.ToArray(); // valid input // Don't test padding (byte 61 i.e. '='), which is tested in DecodeInPlaceInvalidBytesPadding // Don't test chars to be ignored (spaces: 9, 10, 13, 32 i.e. '\n', '\t', '\r', ' ') @@ -587,73 +589,44 @@ public void DecodeInPlaceInvalidBytes() // replace one byte with an invalid input buffer[j] = invalidBytes[i]; - string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); - int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); - if (j < 4) - { - Assert.Equal(0, bytesWritten); - } - else - { - Assert.Equal(3, bytesWritten); - Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); - } + Assert.Throws(() => Base64Url.DecodeFromUtf8InPlace(buffer)); } } // Input that is not a multiple of 4 is valid for remainder 2-3, but invalid for 1 { - Span buffer = "2222P"u8.ToArray(); // incomplete input - int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); - - Assert.Equal(3, bytesWritten); // last byte is ignored + byte[] buffer = "2222P"u8.ToArray(); // incomplete input + Assert.Throws(() => Base64Url.DecodeFromUtf8InPlace(buffer)); } } [Fact] - public void DecodeInPlaceInvalidBytesPadding() + public void DecodeInPlaceInvalidBytesPaddingThrowsFormatException() { // Only last 2 bytes can be padding, all other occurrence of padding is invalid for (int j = 0; j < 7; j++) { - Span buffer = "2222PPPP"u8.ToArray(); // valid input + byte[] buffer = "2222PPPP"u8.ToArray(); // valid input buffer[j] = Base64TestHelper.EncodingPad; - string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); - int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); - if (j < 4) - { - Assert.Equal(0, bytesWritten); - } - else - { - Assert.Equal(3, bytesWritten); - Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); - } + Assert.Throws(() => Base64Url.DecodeFromUtf8InPlace(buffer)); } // Invalid input with valid padding { - Span buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + byte[] buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; buffer[6] = Base64TestHelper.EncodingPad; buffer[7] = Base64TestHelper.EncodingPad; // invalid input - "2222P*==" - string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); - int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); - Assert.Equal(3, bytesWritten); - Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + Assert.Throws(() => Base64Url.DecodeFromUtf8InPlace(buffer)); } { - Span buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; + byte[] buffer = new byte[] { 50, 50, 50, 50, 80, 42, 42, 42 }; buffer[7] = Base64TestHelper.EncodingPad; // invalid input - "2222P**=" - string sourceString = Encoding.ASCII.GetString(buffer.Slice(0, 4).ToArray()); - int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); - Assert.Equal(3, bytesWritten); - Span expectedBytes = Convert.FromBase64String(sourceString); - Assert.True(expectedBytes.SequenceEqual(buffer.Slice(0, bytesWritten))); + Assert.Throws(() => Base64Url.DecodeFromUtf8InPlace(buffer)); } // The last byte or the last 2 bytes being the padding character is valid @@ -677,6 +650,17 @@ public void DecodeInPlaceInvalidBytesPadding() Assert.Equal(5, bytesWritten); Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); } + + // The last byte or the last 2 bytes being the padding character is valid + { + Span buffer = new byte[] { 50, 50, 50, 50, 80, 80 }; // valid input without padding "2222PP" + + string sourceString = Encoding.ASCII.GetString(buffer.ToArray()); + int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); + + Assert.Equal(4, bytesWritten); + Assert.True(VerifyUrlDecodingCorrectness(sourceString, buffer.Slice(0, bytesWritten))); + } } [Theory] diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs index 27d0bd21489d68..c405836f5a2629 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs @@ -173,6 +173,25 @@ public static IEnumerable EncodeToStringTests_TestData() yield return new object[] { Encoding.Unicode.GetBytes("eeeebbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccdgggdaaaabbbbccccdddddddeeeeeaaaabbbbccccdddddddeeeeeaaaabbbbccccddx"), "ZQBlAGUAZQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABkAGQAZABkAGQAZABlAGUAZQBlAGUAYQBhAGEAYQBiAGIAYgBiAGMAYwBjAGMAZABnAGcAZwBkAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZABkAGQAZABkAGQAZQBlAGUAZQBlAGEAYQBhAGEAYgBiAGIAYgBjAGMAYwBjAGQAZAB4AA" }; } + [Theory] + //[InlineData("\u5948cz_T", 0, 0)] // scalar code-path + //[InlineData("z_Ta123\u5948", 4, 3)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path + /*[InlineData("z_T-H7sqEkerqMweH1uSw\u5948==", 20, 15)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u5948==", 44, 33)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, 0)] // Vector512 / Avx512Vbmi code-path + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789\u5948==", 92, 69)]*/ + public void BasicDecodingNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten) + { + Span source = inputString.ToArray(); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(OperationStatus.InvalidData, Base64Url.DecodeFromChars(source, decodedBytes, out int consumed, out int decodedByteCount)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(expectedWritten, decodedByteCount); + } + [Theory] [MemberData(nameof(EncodeToStringTests_TestData))] @@ -184,6 +203,25 @@ public static void EncodeToStringTests(byte[] inputBytes, string expectedBase64) Assert.Equal(expectedBase64, chars.Slice(0, charsWritten)); } + [Fact] + public void EncodingOutputTooSmall() + { + for (int numBytes = 4; numBytes < 20; numBytes++) + { + byte[] source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + int expectedConsumed = 3; + char[] encodedBytes = new char[4]; + + Assert.Equal(OperationStatus.DestinationTooSmall, Base64Url.EncodeToChars(source, encodedBytes, out int consumed, out int written)); + Assert.Equal(expectedConsumed, consumed); + Assert.Equal(encodedBytes.Length, written); + Assert.True(source.AsSpan().Slice(0, consumed).SequenceEqual(Base64Url.DecodeFromChars(encodedBytes))); + + Assert.Throws("destination", () => Base64Url.EncodeToChars(source, encodedBytes)); + } + } + [Fact] public static void Roundtrip() { @@ -341,7 +379,7 @@ private static void VerifyInvalidInput(string input) { char[] inputChars = input.ToCharArray(); - Assert.Throws(() => Base64Url.DecodeFromChars(input)); // or FormatException? + Assert.Throws(() => Base64Url.DecodeFromChars(input)); // or FormatException? } private static void Verify(string input, Action action = null) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs index 2ed3f41f9e39d6..d94cbbc5ac9f88 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -9,27 +9,6 @@ namespace System.Buffers.Text.Tests { public class Base64UrlValidationUnitTests : Base64TestBase { - public static readonly byte[] s_encodingMap = { - 65, 66, 67, 68, 69, 70, 71, 72, //A..H - 73, 74, 75, 76, 77, 78, 79, 80, //I..P - 81, 82, 83, 84, 85, 86, 87, 88, //Q..X - 89, 90, 97, 98, 99, 100, 101, 102, //Y..Z, a..f - 103, 104, 105, 106, 107, 108, 109, 110, //g..n - 111, 112, 113, 114, 115, 116, 117, 118, //o..v - 119, 120, 121, 122, 48, 49, 50, 51, //w..z, 0..3 - 52, 53, 54, 55, 56, 57, 45, 95 //4..9, -, _ - }; - - private static void InitializeDecodableBytes(Span bytes, int seed = 100) - { - var rnd = new Random(seed); - for (int i = 0; i < bytes.Length; i++) - { - int index = (byte)rnd.Next(0, s_encodingMap.Length); - bytes[i] = s_encodingMap[index]; - } - } - [Fact] public void BasicValidationBytes() { @@ -40,10 +19,10 @@ public void BasicValidationBytes() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 != 0); // ensure we have a valid length + } while (numBytes % 4 == 1); // ensure we have a valid length Span source = new byte[numBytes]; - InitializeDecodableBytes(source, numBytes); + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); Assert.True(Base64Url.IsValid(source)); Assert.True(Base64Url.IsValid(source, out int decodedLength)); @@ -61,10 +40,10 @@ public void BasicValidationChars() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 != 0); // ensure we have a valid length + } while (numBytes % 4 == 1); // ensure we have a valid length Span source = new byte[numBytes]; - InitializeDecodableBytes(source, numBytes); + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); Span chars = source .ToArray() .Select(Convert.ToChar) @@ -77,7 +56,7 @@ public void BasicValidationChars() } } - [Fact] + /*[Fact] TODO: Should we always account bytes length having remainder of 1 as invalid? public void BasicValidationInvalidInputLengthBytes() { var rnd = new Random(42); @@ -87,10 +66,10 @@ public void BasicValidationInvalidInputLengthBytes() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 == 0); // ensure we have a invalid length + } while (numBytes % 4 != 1); // only remainder of 1 is invalid length Span source = new byte[numBytes]; - + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); Assert.False(Base64Url.IsValid(source)); Assert.False(Base64Url.IsValid(source, out int decodedLength)); Assert.Equal(0, decodedLength); @@ -107,15 +86,16 @@ public void BasicValidationInvalidInputLengthChars() do { numBytes = rnd.Next(100, 1000 * 1000); - } while (numBytes % 4 == 0); // ensure we have a invalid length + } while (numBytes % 4 != 1); // ensure we have a invalid length Span source = new char[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); Assert.False(Base64Url.IsValid(source)); Assert.False(Base64Url.IsValid(source, out int decodedLength)); Assert.Equal(0, decodedLength); } - } + }*/ [Fact] public void ValidateEmptySpanBytes() @@ -262,19 +242,6 @@ public void ValidateWithPaddingReturnsCorrectCountChars(string utf8WithByteToBeI Assert.Equal(expectedLength, decodedLength); } - [Theory] - [InlineData("YQ==", 1)] - [InlineData("YWI=", 2)] - [InlineData("YWJj", 3)] - public void DecodeEmptySpan(string utf8WithByteToBeIgnored, int expectedLength) - { - ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); - - Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); - Assert.True(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); - Assert.Equal(expectedLength, decodedLength); - } - [Theory] [InlineData("YWJ", true, 2)] [InlineData("YW", true, 1)] @@ -358,10 +325,10 @@ public void InvalidBase64UrlBytes(string utf8WithByteToBeIgnored) [InlineData(" a ")] public void InvalidBase64UrlChars(string utf8WithByteToBeIgnored) { - byte[] utf8BytesWithByteToBeIgnored = UTF8Encoding.UTF8.GetBytes(utf8WithByteToBeIgnored); + ReadOnlySpan utf8CharsWithCharToBeIgnored = utf8WithByteToBeIgnored; - Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored)); - Assert.False(Base64Url.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.False(Base64Url.IsValid(utf8CharsWithCharToBeIgnored)); + Assert.False(Base64Url.IsValid(utf8CharsWithCharToBeIgnored, out int decodedLength)); Assert.Equal(0, decodedLength); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index dc87593b2acc6a..97046645c8e061 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -37,7 +37,36 @@ internal static unsafe void AssertWrite(byte* dest, byte* destStart, in } } - internal interface IBase64Encoder + [Conditional("DEBUG")] + internal static unsafe void AssertRead(ushort* src, ushort* srcStart, int srcLength) + { + int vectorElements = Unsafe.SizeOf(); + ushort* readEnd = src + vectorElements; + ushort* srcEnd = srcStart + srcLength; + + if (readEnd > srcEnd) + { + int srcIndex = (int)(src - srcStart); + Debug.Fail($"Read for {typeof(TVector)} is not within safe bounds. srcIndex: {srcIndex}, srcLength: {srcLength}"); + } + } + + [Conditional("DEBUG")] + internal static unsafe void AssertWrite(ushort* dest, ushort* destStart, int destLength) + { + int vectorElements = Unsafe.SizeOf(); + ushort* writeEnd = dest + vectorElements; + ushort* destEnd = destStart + destLength; + + if (writeEnd > destEnd) + { + int destIndex = (int)(dest - destStart); + Debug.Fail($"Write for {typeof(TVector)} is not within safe bounds. destIndex: {destIndex}, destLength: {destLength}"); + } + } + + + internal interface IBase64Encoder where T : unmanaged { static abstract ReadOnlySpan EncodingMap { get; } static abstract sbyte Avx2LutChar62 { get; } @@ -45,13 +74,21 @@ internal interface IBase64Encoder static abstract ReadOnlySpan AdvSimdLut4 { get; } static abstract uint Ssse3AdvSimdLutE3 { get; } static abstract int GetMaxSrcLength(int srcLength, int destLength); - static abstract unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap); - static abstract unsafe uint EncodeTwoOptionallyPadOne(byte* oneByte, ref byte encodingMap); + static abstract int GetMaxEncodedLength(int srcLength); + static abstract uint GetInPlaceDestinationLength(int encodedLength, int leftOver); + static abstract unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, T* dest, ref byte encodingMap); + static abstract unsafe void EncodeTwoOptionallyPadOne(byte* oneByte, T* dest, ref byte encodingMap); + static abstract unsafe void EncodeThreeAndWrite(byte* threeBytes, T* destination, ref byte encodingMap); static abstract int IncrementPadTwo { get; } static abstract int IncrementPadOne { get; } + static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector512 str); + static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector256 str); + static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector128 str); + static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector128 res1, + Vector128 res2, Vector128 res3, Vector128 res4); } - internal interface IBase64Decoder + internal interface IBase64Decoder where T : unmanaged { static abstract ReadOnlySpan DecodingMap { get; } static abstract ReadOnlySpan VbmiLookup0 { get; } @@ -85,7 +122,15 @@ static abstract bool TryDecode256Core( Vector256 lutShift, Vector256 shiftForUnderscore, out Vector256 result); - + static abstract unsafe int Decode(T* encodedBytes, ref sbyte decodingMap); + static abstract unsafe int DecodeRemaining(T* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3); + static abstract int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span); + static abstract OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, + Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TTBase64Decoder : IBase64Decoder; + static abstract unsafe bool TryLoadVector512(T* src, T* srcStart, int sourceLength, out Vector512 str); + static abstract unsafe bool TryLoadAvxVector256(T* src, T* srcStart, int sourceLength, out Vector256 str); + static abstract unsafe bool TryLoadVector128(T* src, T* srcStart, int sourceLength, out Vector128 str); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 4b7a80d3d51249..8a7bd43a51ca83 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -7,6 +7,7 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using static System.Buffers.Text.Base64; namespace System.Buffers.Text { @@ -35,10 +36,12 @@ public static partial class Base64 /// or if the input is incomplete (i.e. not a multiple of 4) and is . /// public static OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - DecodeFromUtf8(utf8, bytes, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + DecodeFrom(utf8, bytes, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) - where TBase64Decoder : IBase64Decoder + internal static unsafe OperationStatus DecodeFrom(ReadOnlySpan utf8, Span bytes, + out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) + where TBase64Decoder : IBase64Decoder + where T : unmanaged { if (utf8.IsEmpty) { @@ -47,7 +50,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp return OperationStatus.Done; } - fixed (byte* srcBytes = &MemoryMarshal.GetReference(utf8)) + fixed (T* srcBytes = &MemoryMarshal.GetReference(utf8)) fixed (byte* destBytes = &MemoryMarshal.GetReference(bytes)) { int srcLength = TBase64Decoder.SrcLength(isFinalBlock, utf8.Length); @@ -62,17 +65,17 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp maxSrcLength = destLength / 3 * 4; } - byte* src = srcBytes; + T* src = srcBytes; byte* dest = destBytes; - byte* srcEnd = srcBytes + (uint)srcLength; - byte* srcMax = srcBytes + (uint)maxSrcLength; + T* srcEnd = srcBytes + (uint)srcLength; + T* srcMax = srcBytes + (uint)maxSrcLength; if (maxSrcLength >= 24) { - byte* end = srcMax - 88; + T* end = srcMax - 88; if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) { - Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -83,7 +86,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp end = srcMax - 45; if (Avx2.IsSupported && (end >= src)) { - Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -94,7 +97,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp end = srcMax - 24; if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) { - Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -125,7 +128,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp while (src < srcMax) { - int result = Decode(src, ref decodingMap); + int result = TBase64Decoder.Decode(src, ref decodingMap); if (result < 0) { @@ -160,44 +163,9 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp } // if isFinalBlock is false, we will never reach this point - - uint t0; - uint t1; - uint t2; - uint t3; // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. long remaining = srcEnd - src; - switch (remaining) - { - case 2: - t0 = srcEnd[-2]; - t1 = srcEnd[-1]; - t2 = EncodingPad; - t3 = EncodingPad; - break; - case 3: - t0 = srcEnd[-3]; - t1 = srcEnd[-2]; - t2 = srcEnd[-1]; - t3 = EncodingPad; - break; - case 4: - t0 = srcEnd[-4]; - t1 = srcEnd[-3]; - t2 = srcEnd[-2]; - t3 = srcEnd[-1]; - break; - default: - goto InvalidDataExit; - } - - int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); - int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); - - i0 <<= 18; - i1 <<= 12; - - i0 |= i1; + int i0 = TBase64Decoder.DecodeRemaining(srcEnd, ref decodingMap, remaining, out uint t2, out uint t3); byte* destMax = destBytes + (uint)destLength; @@ -295,7 +263,7 @@ internal static unsafe OperationStatus DecodeFromUtf8(ReadOnlySp OperationStatus.InvalidData; } - static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) + static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) { utf8 = utf8.Slice(bytesConsumed); bytes = bytes.Slice(bytesWritten); @@ -303,7 +271,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span b OperationStatus status; do { - int localConsumed = IndexOfAnyExceptWhiteSpace(utf8); + int localConsumed = TBase64Decoder.IndexOfAnyExceptWhiteSpace(utf8); if (localConsumed < 0) { // The remainder of the input is all whitespace. Mark it all as having been consumed, @@ -321,7 +289,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span b // Fall back to block-wise decoding. This is very slow, but it's also very non-standard // formatting of the input; whitespace is typically only found between blocks, such as // when Convert.ToBase64String inserts a line break every 76 output characters. - return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + return TBase64Decoder.DecodeWithWhiteSpaceBlockwiseWrapper(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); } // Skip over the starting whitespace and continue. @@ -329,7 +297,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span b utf8 = utf8.Slice(localConsumed); // Try again after consumed whitespace - status = DecodeFromUtf8(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); + status = DecodeFrom(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; if (status is not OperationStatus.InvalidData) @@ -379,10 +347,10 @@ public static int GetMaxDecodedFromUtf8Length(int length) /// hence can only be called once with all the data in the buffer. /// public static OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten) => - DecodeFromUtf8InPlace(buffer, out bytesWritten, ignoreWhiteSpace: true); + DecodeFromUtf8InPlace(buffer, out bytesWritten, ignoreWhiteSpace: true); internal static unsafe OperationStatus DecodeFromUtf8InPlace(Span buffer, out int bytesWritten, bool ignoreWhiteSpace) - where TBase64Decoder : IBase64Decoder + where TBase64Decoder : IBase64Decoder { if (buffer.IsEmpty) { @@ -510,8 +478,8 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa } } - private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) - where TBase64Decoder : IBase64Decoder + internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc byte[BlockSize]; @@ -563,7 +531,7 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(Rea localIsFinalBlock = false; } - status = DecodeFromUtf8(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); + status = DecodeFrom(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; @@ -610,7 +578,7 @@ private static int GetPaddingCount(ref byte ptrToLastElement) } internal static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) - where TBase64Decoder : IBase64Decoder + where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc byte[BlockSize]; @@ -683,8 +651,9 @@ internal static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder + private static unsafe void Avx512Decode(ref T* srcBytes, ref byte* destBytes, T* srcEnd, int sourceLength, int destLength, T* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + where T : unmanaged { // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/decode // If we have AVX512 support, pick off 64 bytes at a time for as long as we can, @@ -692,7 +661,7 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref // string. Also, because we write 16 zeroes at the end of the output, ensure // that there are at least 22 valid bytes of input data remaining to close the // gap. 64 + 2 + 22 = 88 bytes. - byte* src = srcBytes; + T* src = srcBytes; byte* dest = destBytes; // The JIT won't hoist these "constants", so help it @@ -711,8 +680,10 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref // Vbmi was first introduced in CannonLake and is available from IceLake on. do { - AssertRead>(src, srcStart, sourceLength); - Vector512 str = Vector512.Load(src).AsSByte(); + if (!TBase64Decoder.TryLoadVector512(src, srcStart, sourceLength, out Vector512 str)) + { + break; + } // Step 1: Translate encoded Base64 input to their original indices // This step also checks for invalid inputs and exits. @@ -747,8 +718,9 @@ private static unsafe void Avx512Decode(ref byte* srcBytes, ref [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Decode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder + private static unsafe void Avx2Decode(ref T* srcBytes, ref byte* destBytes, T* srcEnd, int sourceLength, int destLength, T* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + where T : unmanaged { // If we have AVX2 support, pick off 32 bytes at a time for as long as we can, // but make sure that we quit before seeing any == markers at the end of the @@ -790,14 +762,16 @@ private static unsafe void Avx2Decode(ref byte* srcBytes, ref by Vector256 mergeConstant0 = Vector256.Create(0x01400140).AsSByte(); Vector256 mergeConstant1 = Vector256.Create(0x00011000).AsInt16(); - byte* src = srcBytes; + T* src = srcBytes; byte* dest = destBytes; //while (remaining >= 45) do { - AssertRead>(src, srcStart, sourceLength); - Vector256 str = Avx.LoadVector256(src).AsSByte(); + if (!TBase64Decoder.TryLoadAvxVector256(src, srcStart, sourceLength, out Vector256 str)) + { + break; + } Vector256 hiNibbles = Avx2.And(Avx2.ShiftRightLogical(str.AsInt32(), 4).AsSByte(), maskSlashOrUnderscore); @@ -864,8 +838,9 @@ internal static Vector128 SimdShuffle(Vector128 left, Vector128(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder + private static unsafe void Vector128Decode(ref T* srcBytes, ref byte* destBytes, T* srcEnd, int sourceLength, int destLength, T* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + where T : unmanaged { Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian); @@ -952,14 +927,16 @@ private static unsafe void Vector128Decode(ref byte* srcBytes, r Vector128 mask2F = Vector128.Create(TBase64Decoder.MaskSlashOrUnderscore); Vector128 mask8F = Vector128.Create((byte)0x8F); Vector128 shiftForUnderscore = Vector128.Create((byte)33); - byte* src = srcBytes; + T* src = srcBytes; byte* dest = destBytes; //while (remaining >= 24) do { - AssertRead>(src, srcStart, sourceLength); - Vector128 str = Vector128.LoadUnsafe(ref *src); + if (!TBase64Decoder.TryLoadVector128(src, srcStart, sourceLength, out Vector128 str)) + { + break; + } // lookup Vector128 hiNibbles = Vector128.ShiftRightLogical(str.AsInt32(), 4).AsByte() & mask2F; @@ -1095,7 +1072,7 @@ internal static bool IsWhiteSpace(int value) return value == 32; } - private readonly struct Base64Decoder : IBase64Decoder + private readonly struct Base64DecoderByte : IBase64Decoder { // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) public static ReadOnlySpan DecodingMap => @@ -1178,6 +1155,7 @@ internal static bool IsWhiteSpace(int value) public static ReadOnlySpan Vector128LutShift => [0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); public static bool IsInValidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 @@ -1248,6 +1226,115 @@ public static bool TryDecode256Core( return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) + { + uint t0 = encodedBytes[0]; + uint t1 = encodedBytes[1]; + uint t2 = encodedBytes[2]; + uint t3 = encodedBytes[3]; + + int i0 = Unsafe.Add(ref decodingMap, t0); + int i1 = Unsafe.Add(ref decodingMap, t1); + int i2 = Unsafe.Add(ref decodingMap, t2); + int i3 = Unsafe.Add(ref decodingMap, t3); + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int DecodeRemaining(byte* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3) + { + uint t0; + uint t1; + t2 = EncodingPad; + t3 = EncodingPad; + switch (remaining) + { + case 2: + t0 = srcEnd[-2]; + t1 = srcEnd[-1]; + break; + case 3: + t0 = srcEnd[-3]; + t1 = srcEnd[-2]; + t2 = srcEnd[-1]; + break; + case 4: + t0 = srcEnd[-4]; + t1 = srcEnd[-3]; + t2 = srcEnd[-2]; + t3 = srcEnd[-1]; + break; + default: + return -1; + } + + int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); + int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + return i0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!IsWhiteSpace(span[i])) + { + return i; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, + Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder + { + return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector512(byte* src, byte* srcStart, int sourceLength, out Vector512 str) + { + AssertRead>(src, srcStart, sourceLength); + str = Vector512.Load(src).AsSByte(); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe bool TryLoadAvxVector256(byte* src, byte* srcStart, int sourceLength, out Vector256 str) + { + AssertRead>(src, srcStart, sourceLength); + str = Avx.LoadVector256(src).AsSByte(); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int sourceLength, out Vector128 str) + { + AssertRead>(src, srcStart, sourceLength); + str = Vector128.LoadUnsafe(ref *src); + return true; + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index 7ebf8d4dacfaaa..e3f2ae29b1677e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -35,10 +35,12 @@ public static partial class Base64 /// It does not return InvalidData since that is not possible for base64 encoding. /// public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - EncodeToUtf8(bytes, utf8, out bytesConsumed, out bytesWritten, isFinalBlock); + EncodeTo(bytes, utf8, out bytesConsumed, out bytesWritten, isFinalBlock); - internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) - where TBase64Encoder : IBase64Encoder + internal static unsafe OperationStatus EncodeTo(ReadOnlySpan bytes, + Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + where TBase64Encoder : IBase64Encoder + where T : unmanaged { if (bytes.IsEmpty) { @@ -48,14 +50,14 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan } fixed (byte* srcBytes = &MemoryMarshal.GetReference(bytes)) - fixed (byte* destBytes = &MemoryMarshal.GetReference(utf8)) + fixed (T* destBytes = &MemoryMarshal.GetReference(utf8)) { int srcLength = bytes.Length; int destLength = utf8.Length; int maxSrcLength = TBase64Encoder.GetMaxSrcLength(srcLength, destLength); byte* src = srcBytes; - byte* dest = destBytes; + T* dest = destBytes; byte* srcEnd = srcBytes + (uint)srcLength; byte* srcMax = srcBytes + (uint)maxSrcLength; @@ -64,7 +66,7 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan byte* end = srcMax - 64; if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) { - Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -73,7 +75,7 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan end = srcMax - 32; if (Avx2.IsSupported && (end >= src)) { - Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -82,7 +84,7 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan end = srcMax - 48; if (AdvSimd.Arm64.IsSupported && (end >= src)) { - AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -91,7 +93,7 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan end = srcMax - 16; if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) { - Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) goto DoneExit; @@ -99,13 +101,11 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan } ref byte encodingMap = ref MemoryMarshal.GetReference(TBase64Encoder.EncodingMap); - uint result = 0; srcMax -= 2; while (src < srcMax) { - result = Encode(src, ref encodingMap); - Unsafe.WriteUnaligned(dest, result); + TBase64Encoder.EncodeThreeAndWrite(src, dest, ref encodingMap); src += 3; dest += 4; } @@ -123,15 +123,13 @@ internal static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan if (src + 1 == srcEnd) { - result = TBase64Encoder.EncodeOneOptionallyPadTwo(src, ref encodingMap); - Unsafe.WriteUnaligned(dest, result); + TBase64Encoder.EncodeOneOptionallyPadTwo(src, dest, ref encodingMap); src += 1; dest += TBase64Encoder.IncrementPadTwo; } else if (src + 2 == srcEnd) { - result = TBase64Encoder.EncodeTwoOptionallyPadOne(src, ref encodingMap); - Unsafe.WriteUnaligned(dest, result); + TBase64Encoder.EncodeTwoOptionallyPadOne(src, dest, ref encodingMap); src += 2; dest += TBase64Encoder.IncrementPadOne; } @@ -183,7 +181,11 @@ public static int GetMaxEncodedToUtf8Length(int length) /// It does not return NeedMoreData since this method tramples the data in the buffer and hence can only be called once with all the data in the buffer. /// It does not return InvalidData since that is not possible for base 64 encoding. /// - public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) + public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) => + EncodeToUtf8InPlace(buffer, dataLength, out bytesWritten); + + internal static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) + where TBase64Encoder : IBase64Encoder { if (buffer.IsEmpty) { @@ -193,40 +195,38 @@ public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int fixed (byte* bufferBytes = &MemoryMarshal.GetReference(buffer)) { - int encodedLength = GetMaxEncodedToUtf8Length(dataLength); + int encodedLength = TBase64Encoder.GetMaxEncodedLength(dataLength); if (buffer.Length < encodedLength) { bytesWritten = 0; return OperationStatus.DestinationTooSmall; } - int leftover = dataLength - (dataLength / 3) * 3; // how many bytes after packs of 3 + int leftover = dataLength % 3; // how many bytes after packs of 3 - uint destinationIndex = (uint)(encodedLength - 4); + uint destinationIndex = TBase64Encoder.GetInPlaceDestinationLength(encodedLength, leftover); uint sourceIndex = (uint)(dataLength - leftover); - uint result = 0; - ref byte encodingMap = ref MemoryMarshal.GetReference(Base64Encoder.EncodingMap); + ref byte encodingMap = ref MemoryMarshal.GetReference(TBase64Encoder.EncodingMap); // encode last pack to avoid conditional in the main loop if (leftover != 0) { if (leftover == 1) { - result = Base64Encoder.EncodeOneOptionallyPadTwo(bufferBytes + sourceIndex, ref encodingMap); + TBase64Encoder.EncodeOneOptionallyPadTwo(bufferBytes + sourceIndex, bufferBytes + destinationIndex, ref encodingMap); } else { - result = Base64Encoder.EncodeTwoOptionallyPadOne(bufferBytes + sourceIndex, ref encodingMap); + TBase64Encoder.EncodeTwoOptionallyPadOne(bufferBytes + sourceIndex, bufferBytes + destinationIndex, ref encodingMap); } - Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); destinationIndex -= 4; } sourceIndex -= 3; while ((int)sourceIndex >= 0) { - result = Encode(bufferBytes + sourceIndex, ref encodingMap); + uint result = Encode(bufferBytes + sourceIndex, ref encodingMap); Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); destinationIndex -= 4; sourceIndex -= 3; @@ -240,8 +240,9 @@ public static unsafe OperationStatus EncodeToUtf8InPlace(Span buffer, int [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx512BW))] [CompExactlyDependsOn(typeof(Avx512Vbmi))] - private static unsafe void Avx512Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Encoder : IBase64Encoder + private static unsafe void Avx512Encode(ref byte* srcBytes, ref T* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, T* destStart) + where TBase64Encoder : IBase64Encoder + where T : unmanaged { // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/encode // If we have AVX512 support, pick off 48 bytes at a time for as long as we can. @@ -249,7 +250,7 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref // full 64-byte read without segfaulting. byte* src = srcBytes; - byte* dest = destBytes; + T* dest = destBytes; // The JIT won't hoist these "constants", so help it Vector512 shuffleVecVbmi = Vector512.Create( @@ -295,8 +296,7 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref // Step 2: Now we have the indices calculated. Next step is to use these indices to translate. str = Avx512Vbmi.PermuteVar64x8(vbmiLookup, str); - AssertWrite>(dest, destStart, destLength); - str.Store((sbyte*)dest); + TBase64Encoder.StoreToDestination(dest, destStart, destLength, str.AsByte()); src += 48; dest += 64; @@ -314,8 +314,9 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, ref [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Encoder : IBase64Encoder + private static unsafe void Avx2Encode(ref byte* srcBytes, ref T* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, T* destStart) + where TBase64Encoder : IBase64Encoder + where T : unmanaged { // If we have AVX2 support, pick off 24 bytes at a time for as long as we can. // But because we read 32 bytes at a time, ensure we have enough room to do a @@ -341,14 +342,14 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref by 10, 9, 11, 10); Vector256 lut = Vector256.Create( - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0, - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0); + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0, + 65, 71, -4, -4, + -4, -4, -4, -4, + -4, -4, -4, -4, + TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0); Vector256 maskAC = Vector256.Create(0x0fc0fc00).AsSByte(); Vector256 maskBB = Vector256.Create(0x003f03f0).AsSByte(); @@ -358,7 +359,7 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref by Vector256 const25 = Vector256.Create((sbyte)25); byte* src = srcBytes; - byte* dest = destBytes; + T* dest = destBytes; // first load is done at c-0 not to get a segfault AssertRead>(src, srcStart, sourceLength); @@ -466,8 +467,7 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref by // Add offsets to input values: str = Avx2.Add(str, Avx2.Shuffle(lut, tmp)); - AssertWrite>(dest, destStart, destLength); - Avx.Store(dest, str.AsByte()); + TBase64Encoder.StoreToDestination(dest, destStart, destLength, str.AsByte()); src += 24; dest += 32; @@ -486,8 +486,9 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref by [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Encoder : IBase64Encoder + private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref T* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, T* destStart) + where TBase64Encoder : IBase64Encoder + where T : unmanaged { // C# implementation of https://github.com/aklomp/base64/blob/3a5add8652076612a8407627a42c768736a4263f/lib/arch/neon64/enc_loop.c Vector128 str1; @@ -502,7 +503,7 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref Vector128 tblEnc3 = Vector128.Create("ghijklmnopqrstuv"u8).AsByte(); Vector128 tblEnc4 = Vector128.Create(TBase64Encoder.AdvSimdLut4).AsByte(); byte* src = srcBytes; - byte* dest = destBytes; + T* dest = destBytes; // If we have Neon support, pick off 48 bytes at a time for as long as we can. do @@ -532,8 +533,7 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref res4 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res4); // Interleave and store result: - AssertWrite>(dest, destStart, destLength); - AdvSimd.Arm64.StoreVector128x4AndZip(dest, (res1, res2, res3, res4)); + TBase64Encoder.StoreToDestination(dest, destStart, destLength, res1, res2, res3, res4); src += 48; dest += 64; @@ -546,8 +546,9 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Ssse3))] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void Vector128Encode(ref byte* srcBytes, ref byte* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, byte* destStart) - where TBase64Encoder : IBase64Encoder + private static unsafe void Vector128Encode(ref byte* srcBytes, ref T* destBytes, byte* srcEnd, int sourceLength, int destLength, byte* srcStart, T* destStart) + where TBase64Encoder : IBase64Encoder + where T : unmanaged { // If we have SSSE3 support, pick off 12 bytes at a time for as long as we can. // But because we read 16 bytes at a time, ensure we have enough room to do a @@ -568,7 +569,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes, r Vector128 mask8F = Vector128.Create((byte)0x8F); byte* src = srcBytes; - byte* dest = destBytes; + T* dest = destBytes; //while (remaining >= 16) do @@ -656,8 +657,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes, r // Add offsets to input values: str += SimdShuffle(lut, tmp.AsByte(), mask8F); - AssertWrite>(dest, destStart, destLength); - str.Store(dest); + TBase64Encoder.StoreToDestination(dest, destStart, destLength, str); src += 12; dest += 16; @@ -696,13 +696,13 @@ internal static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) internal const int MaximumEncodeLength = (int.MaxValue / 4) * 3; // 1610612733 - private readonly struct Base64Encoder : IBase64Encoder + private readonly struct Base64EncoderByte : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8; public static sbyte Avx2LutChar62 => -19; // char '+' diff - public static sbyte Avx2LutChar63 => -16; // char '/' diff + public static sbyte Avx2LutChar63 => -16; // char '/' diff public static ReadOnlySpan AdvSimdLut4 => "wxyz0123456789+/"u8; @@ -713,10 +713,15 @@ internal static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) public static int IncrementPadOne => 4; public static int GetMaxSrcLength(int srcLength, int destLength) => - srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength) ? srcLength : (destLength >> 2) * 3; + srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength) ? + srcLength : (destLength >> 2) * 3; + + public static uint GetInPlaceDestinationLength(int encodedLength, int _) => (uint)(encodedLength - 4); + + public static int GetMaxEncodedLength(int srcLength) => GetMaxEncodedToUtf8Length(srcLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap) + public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, byte* dest, ref byte encodingMap) { uint t0 = oneByte[0]; @@ -727,16 +732,22 @@ public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte enco if (BitConverter.IsLittleEndian) { - return i0 | (i1 << 8) | (EncodingPad << 16) | (EncodingPad << 24); + dest[0] = (byte)i0; + dest[1] = (byte)i1; + dest[2] = (byte)EncodingPad; + dest[3] = (byte)EncodingPad; } else { - return (i0 << 24) | (i1 << 16) | (EncodingPad << 8) | EncodingPad; + dest[3] = (byte)i0; + dest[2] = (byte)i1; + dest[1] = (byte)EncodingPad; + dest[0] = (byte)EncodingPad; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte encodingMap) + public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, ref byte encodingMap) { uint t0 = twoBytes[0]; uint t1 = twoBytes[1]; @@ -749,11 +760,78 @@ public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte enc if (BitConverter.IsLittleEndian) { - return i0 | (i1 << 8) | (i2 << 16) | (EncodingPad << 24); + dest[0] = (byte)i0; + dest[1] = (byte)i1; + dest[2] = (byte)i2; + dest[3] = (byte)EncodingPad; + } + else + { + dest[3] = (byte)i0; + dest[2] = (byte)i1; + dest[1] = (byte)i2; + dest[0] = (byte)EncodingPad; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) + { + AssertWrite>(dest, destStart, destLength); + str.Store(dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) + { + AssertWrite>(dest, destStart, destLength); + Avx.Store(dest, str.AsByte()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) + { + AssertWrite>(dest, destStart, destLength); + str.Store(dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, + Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) + { + AssertWrite>(dest, destStart, destLength); + AdvSimd.Arm64.StoreVector128x4AndZip(dest, (res1, res2, res3, res4)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void EncodeThreeAndWrite(byte* threeBytes, byte* destination, ref byte encodingMap) + { + uint t0 = threeBytes[0]; + uint t1 = threeBytes[1]; + uint t2 = threeBytes[2]; + + uint i = (t0 << 16) | (t1 << 8) | t2; + + byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + destination[0] = i0; + destination[1] = i1; + destination[2] = i2; + destination[3] = i3; } else { - return (i0 << 24) | (i1 << 16) | (i2 << 8) | EncodingPad; + destination[3] = i0; + destination[2] = i1; + destination[1] = i2; + destination[0] = i3; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 43fb86d2e97fc2..4f5ce80a3b0155 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -17,7 +17,7 @@ namespace System.Buffers.Text public static partial class Base64Url { /// - /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "length". + /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "base64Length". /// /// /// Thrown when the specified is less than 0. @@ -36,7 +36,6 @@ public static int GetMaxDecodedLength(int base64Length) /// /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. - /// If the input is not a multiple of 4, it will decode as much as it can, to the closest multiple of 4. /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. @@ -50,31 +49,64 @@ public static int GetMaxDecodedLength(int base64Length) /// - Done - on successful processing of the entire input span /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input /// - NeedMoreData - only if is false and the input is not a multiple of 4 - /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, - /// or if it contains invalid/more than two padding characters and is . + /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, or if it contains invalid/more than two padding characters + /// or if the input is incomplete (i.e. the remainder of % 4 is 1) and is . /// + /// + /// As padding is optional the length not required to be a multiple of 4 even if is . + /// If the length is not a multiple of 4 and is the remainders decoded accordingly: + /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// Remainder of 1 byte - will cause OperationStatus.InvalidData result. + /// public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - DecodeFromUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + DecodeFrom(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); /// /// Decode the span of UTF-8 encoded text in Base64Url (in-place) into binary data. /// The decoded binary output is smaller than the text data contained in the input (the operation deflates the data). - /// TODO: If the input is not a multiple of 4, it will not decode any. /// /// The input span which contains the base 64 text data that needs to be decoded. - /// TODO: It returns the OperationStatus enum values: - /// - Done - on successful processing of the entire input span - /// - InvalidData - if the input contains bytes outside of the expected base 64 range, or if it contains invalid/more than two padding characters. - /// It does not return DestinationTooSmall since that is not possible for base 64 decoding. - /// It does not return NeedMoreData since this method tramples the data in the buffer and - /// hence can only be called once with all the data in the buffer. - /// + /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. + /// contains a invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. + /// + /// As padding is optional the input length not required to be a multiple of 4. + /// If the input length is not a multiple of 4 the remainders decoded accordingly: + /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// Remainder of 1 byte - is invalid input, causes FormatException. + /// public static int DecodeFromUtf8InPlace(Span buffer) { - DecodeFromUtf8InPlace(buffer, out int bytesWritten, ignoreWhiteSpace: true); + OperationStatus status = DecodeFromUtf8InPlace(buffer, out int bytesWritten, ignoreWhiteSpace: true); + + // Base64.DecodeFromUtf8InPlace returns OperationStatus, therefore doesn't throw. + // For the Base64Url case I think it is better to throw to inform that invalid data found. + if (OperationStatus.InvalidData == status) + { + throw new FormatException(SR.Format_BadBase64Char); + } + return bytesWritten; } + /// + /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. + /// Thrown when the encoded output cannot fit in the provided. + /// contains a invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. + /// + /// As padding is optional the input length not required to be a multiple of 4. + /// If the input length is not a multiple of 4 the remainders decoded accordingly: + /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// Remainder of 1 byte - is invalid input, causes FormatException. + /// public static int DecodeFromUtf8(ReadOnlySpan source, Span destination) { OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); @@ -86,12 +118,19 @@ public static int DecodeFromUtf8(ReadOnlySpan source, Span destinati if (OperationStatus.DestinationTooSmall == status) { - throw new ArgumentException("DestinationTooSmall", nameof(destination)); + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } - throw new InvalidOperationException("InvalidData"); + throw new FormatException(SR.Format_BadBase64Char); } + /// + /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// if bytes decoded successfully, otherwise . public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = DecodeFromUtf8(source, destination, out _, out bytesWritten); @@ -99,862 +138,487 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti return status == OperationStatus.Done; } + /// + /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. + /// >A byte array which contains the result of the decoding operation. + /// contains a invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. public static byte[] DecodeFromUtf8(ReadOnlySpan source) { Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : - throw new InvalidOperationException("InvalidData"); + throw new FormatException(SR.Format_BadBase64Char); } + /// + /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of input chars consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// (default) when the input span contains the entire data to encode. + /// Set to when the source buffer contains the entirety of the data to encode. + /// Set to if this method is being called in a loop and if more input data may follow. + /// At the end of the loop, call this (potentially with an empty source buffer) passing . + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input + /// - NeedMoreData - only if is false and the input is not a multiple of 4 + /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, or if it contains invalid/more than two padding characters + /// or if the input is incomplete (i.e. the remainder of % 4 is 1) and is . + /// + /// + /// As padding is optional the length not required to be a multiple of 4 even if is . + /// If the length is not a multiple of 4 and is the remainders decoded accordingly: + /// Remainder of 3 chars - decoded into 2 bytes data, decoding succeeds. + /// Remainder of 2 chars - decoded into 1 byte data. decoding succeeds. + /// Remainder of 1 char - will cause OperationStatus.InvalidData result. + /// public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) => - DecodeFromChars(source, destination, out charsConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); + DecodeFrom(MemoryMarshal.Cast(source), destination, out charsConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - internal static unsafe OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, - out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) - where TBase64Decoder : IBase64Decoder + internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder { - if (source.IsEmpty) - { - bytesConsumed = 0; - bytesWritten = 0; - return OperationStatus.Done; - } + const int BlockSize = 4; + Span buffer = stackalloc ushort[BlockSize]; + OperationStatus status = OperationStatus.Done; - fixed (char* srcBytes = &MemoryMarshal.GetReference(source)) - fixed (byte* destBytes = &MemoryMarshal.GetReference(destination)) + while (!utf8.IsEmpty) { - int srcLength = TBase64Decoder.SrcLength(isFinalBlock, source.Length); - int destLength = destination.Length; - int maxSrcLength = srcLength; - int decodedLength = TBase64Decoder.GetMaxDecodedLength(srcLength); - - // max. 2 padding chars - if (destLength < decodedLength - 2) - { - // For overflow see comment below - maxSrcLength = destLength / 3 * 4; - } - - char* src = srcBytes; - byte* dest = destBytes; - char* srcEnd = srcBytes + (uint)srcLength; - char* srcMax = srcBytes + (uint)maxSrcLength; + int encodedIdx = 0; + int bufferIdx = 0; + int skipped = 0; - if (maxSrcLength >= 24) + for (; encodedIdx < utf8.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) { - char* end = srcMax - 88; - if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) - { - Avx512Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); - - if (src == srcEnd) - { - goto DoneExit; - } - } - - end = srcMax - 45; - if (Avx2.IsSupported && (end >= src)) + if (IsWhiteSpace(utf8[encodedIdx])) { - Avx2Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); - - if (src == srcEnd) - { - goto DoneExit; - } + skipped++; } - - end = srcMax - 24; - if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) + else { - Vector128Decode(ref src, ref dest, end, maxSrcLength, destLength, (ushort*)srcBytes, destBytes); - - if (src == srcEnd) - { - goto DoneExit; - } + buffer[bufferIdx] = utf8[encodedIdx]; + bufferIdx++; } } - // Last bytes could have padding characters, so process them separately and treat them as valid only if isFinalBlock is true - // if isFinalBlock is false, padding characters are considered invalid - int skipLastChunk = isFinalBlock ? 4 : 0; + utf8 = utf8.Slice(encodedIdx); + bytesConsumed += skipped; - if (destLength >= decodedLength) - { - maxSrcLength = srcLength - skipLastChunk; - } - else + if (bufferIdx == 0) { - // This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733) - // Therefore, (destLength / 3) * 4 will always be less than 2147483641 - Debug.Assert(destLength < (int.MaxValue / 4 * 3)); - maxSrcLength = (destLength / 3) * 4; - srcLength &= ~0x3; // Round down to multiple of 4, this only affect Base64UrlDecoder path + continue; } - ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); - srcMax = srcBytes + maxSrcLength; + bool hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + bool localIsFinalBlock = !hasAnotherBlock; - while (src < srcMax) + // If this block contains padding and there's another block, then only whitespace may follow for being valid. + if (hasAnotherBlock) { - int result = Decode(src, ref decodingMap); - - if (result < 0) + int paddingCount = GetPaddingCount(ref buffer[^1]); + if (paddingCount > 0) { - goto InvalidDataExit; + hasAnotherBlock = false; + localIsFinalBlock = true; } - - WriteThreeLowOrderBytes(dest, result); - src += 4; - dest += 3; } - if (maxSrcLength != srcLength - skipLastChunk) - { - goto DestinationTooSmallExit; - } - - // If input is less than 4 bytes, srcLength == sourceIndex == 0 - // If input is not a multiple of 4, sourceIndex == srcLength != 0 - if (src == srcEnd) + if (localIsFinalBlock && !isFinalBlock) { - if (isFinalBlock) - { - goto InvalidDataExit; - } - - if (src == srcBytes + source.Length) - { - goto DoneExit; - } - - goto NeedMoreDataExit; + localIsFinalBlock = false; } - // if isFinalBlock is false, we will never reach this point - - uint t0; - uint t1; - uint t2; - uint t3; - // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. - long remaining = srcEnd - src; - switch (remaining) - { - case 2: - t0 = srcEnd[-2]; - t1 = srcEnd[-1]; - t2 = EncodingPad; - t3 = EncodingPad; - break; - case 3: - t0 = srcEnd[-3]; - t1 = srcEnd[-2]; - t2 = srcEnd[-1]; - t3 = EncodingPad; - break; - case 4: - t0 = srcEnd[-4]; - t1 = srcEnd[-3]; - t2 = srcEnd[-2]; - t3 = srcEnd[-1]; - break; - default: - goto InvalidDataExit; - } + status = DecodeFrom(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); + bytesConsumed += localConsumed; + bytesWritten += localWritten; - if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + if (status != OperationStatus.Done) { - goto InvalidDataExit; + return status; } - int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); - int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); - - i0 <<= 18; - i1 <<= 12; - - i0 |= i1; - - byte* destMax = destBytes + (uint)destLength; - - if (t3 != EncodingPad) + // The remaining data must all be whitespace in order to be valid. + if (!hasAnotherBlock) { - int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); - int i3 = Unsafe.Add(ref decodingMap, (IntPtr)t3); - - i2 <<= 6; - - i0 |= i3; - i0 |= i2; - - if (i0 < 0) - { - goto InvalidDataExit; - } - if (dest + 3 > destMax) + for (int i = 0; i < utf8.Length; ++i) { - goto DestinationTooSmallExit; - } - - WriteThreeLowOrderBytes(dest, i0); - dest += 3; - src += 4; - } - else if (t2 != EncodingPad) - { - int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); - - i2 <<= 6; - - i0 |= i2; + if (!IsWhiteSpace(utf8[i])) + { + // Revert previous dest increment, since an invalid state followed. + bytesConsumed -= localConsumed; + bytesWritten -= localWritten; - if (i0 < 0) - { - goto InvalidDataExit; - } - if (dest + 2 > destMax) - { - goto DestinationTooSmallExit; - } + return OperationStatus.InvalidData; + } - dest[0] = (byte)(i0 >> 16); - dest[1] = (byte)(i0 >> 8); - dest += 2; - src += remaining; - } - else - { - if (i0 < 0) - { - goto InvalidDataExit; - } - if (dest + 1 > destMax) - { - goto DestinationTooSmallExit; + bytesConsumed++; } - dest[0] = (byte)(i0 >> 16); - dest += 1; - src += remaining; - } - - if (srcLength != source.Length) - { - goto InvalidDataExit; - } - - DoneExit: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.Done; - - DestinationTooSmallExit: - if (srcLength != source.Length && isFinalBlock) - { - goto InvalidDataExit; // if input is not a multiple of 4, and there is no more data, return invalid data instead + break; } - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.DestinationTooSmall; - - NeedMoreDataExit: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.NeedMoreData; - - InvalidDataExit: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return ignoreWhiteSpace ? - InvalidDataFallback(source, destination, ref bytesConsumed, ref bytesWritten, isFinalBlock) : - OperationStatus.InvalidData; + bytes = bytes.Slice(localWritten); } - static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) - { - utf8 = utf8.Slice(bytesConsumed); - bytes = bytes.Slice(bytesWritten); + return status; + } - OperationStatus status; - do - { - int localConsumed = IndexOfAnyExceptWhiteSpace(utf8); - if (localConsumed < 0) - { - // The remainder of the input is all whitespace. Mark it all as having been consumed, - // and mark the operation as being done. - bytesConsumed += utf8.Length; - status = OperationStatus.Done; - break; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPaddingCount(ref ushort ptrToLastElement) + { + int padding = 0; - if (localConsumed == 0) - { - // Non-whitespace was found at the beginning of the input. Since it wasn't consumed - // by the previous call to DecodeFromUtf8, it must be part of a Base64 sequence - // that was interrupted by whitespace or something else considered invalid. - // Fall back to block-wise decoding. This is very slow, but it's also very non-standard - // formatting of the input; whitespace is typically only found between blocks, such as - // when Convert.ToBase64String inserts a line break every 76 output characters. - return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); - } + if (ptrToLastElement == EncodingPad) padding++; + if (Unsafe.Subtract(ref ptrToLastElement, 1) == EncodingPad) padding++; - // Skip over the starting whitespace and continue. - bytesConsumed += localConsumed; - utf8 = utf8.Slice(localConsumed); + return padding; + } - // Try again after consumed whitespace - status = DecodeFromChars(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); - bytesConsumed += localConsumed; - bytesWritten += localWritten; - if (status is not OperationStatus.InvalidData) - { - break; - } + /// + /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// Thrown when the encoded output cannot fit in the provided. + /// contains a invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. + public static int DecodeFromChars(ReadOnlySpan source, Span destination) + { + OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); - utf8 = utf8.Slice(localConsumed); - bytes = bytes.Slice(localWritten); - } - while (!utf8.IsEmpty); + if (OperationStatus.Done == status) + { + return bytesWritten; + } - return status; + if (OperationStatus.DestinationTooSmall == status) + { + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } + + throw new FormatException(SR.Format_BadBase64Char); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Avx512BW))] - [CompExactlyDependsOn(typeof(Avx512Vbmi))] - private static unsafe void Avx512Decode(ref char* srcBytes, ref byte* destBytes, char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder + /// + /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// if bytes decoded successfully, otherwise . + public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) { - // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/decode - // If we have AVX512 support, pick off 64 bytes at a time for as long as we can, - // but make sure that we quit before seeing any == markers at the end of the - // string. Also, because we write 16 zeroes at the end of the output, ensure - // that there are at least 22 valid bytes of input data remaining to close the - // gap. 64 + 2 + 22 = 88 bytes. - ushort* src = (ushort*)srcBytes; - byte* dest = destBytes; - - // The JIT won't hoist these "constants", so help it - Vector512 vbmiLookup0 = Vector512.Create(TBase64Decoder.VbmiLookup0).AsSByte(); - Vector512 vbmiLookup1 = Vector512.Create(TBase64Decoder.VbmiLookup1).AsSByte(); - Vector512 vbmiPackedLanesControl = Vector512.Create( - 0x06000102, 0x090a0405, 0x0c0d0e08, 0x16101112, - 0x191a1415, 0x1c1d1e18, 0x26202122, 0x292a2425, - 0x2c2d2e28, 0x36303132, 0x393a3435, 0x3c3d3e38, - 0x00000000, 0x00000000, 0x00000000, 0x00000000).AsByte(); - - Vector512 mergeConstant0 = Vector512.Create(0x01400140).AsSByte(); - Vector512 mergeConstant1 = Vector512.Create(0x00011000).AsInt16(); - - // This algorithm requires AVX512VBMI support. - // Vbmi was first introduced in CannonLake and is available from IceLake on. - do - { - AssertRead>(src, srcStart, sourceLength); - Vector512 utf16VectorLower = Vector512.Load(src); - Vector512 utf16VectorUpper = Vector512.Load(src + 32); - Vector512 str = Vector512.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); - - // Step 1: Translate encoded Base64 input to their original indices - // This step also checks for invalid inputs and exits. - // After this, we have indices which are verified to have upper 2 bits set to 0 in each byte. - // origIndex = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] - Vector512 origIndex = Avx512Vbmi.PermuteVar64x8x2(vbmiLookup0, str, vbmiLookup1); - Vector512 errorVec = (origIndex.AsInt32() | str.AsInt32()).AsSByte(); - if (errorVec.ExtractMostSignificantBits() != 0) - { - break; - } - - // Step 2: Now we need to reshuffle bits to remove the 0 bits. - // multiAdd1: [...|0000cccc|ccdddddd|0000aaaa|aabbbbbb] - Vector512 multiAdd1 = Avx512BW.MultiplyAddAdjacent(origIndex.AsByte(), mergeConstant0); - // multiAdd1: [...|00000000|aaaaaabb|bbbbcccc|ccdddddd] - Vector512 multiAdd2 = Avx512BW.MultiplyAddAdjacent(multiAdd1, mergeConstant1); + OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); - // Step 3: Pack 48 bytes - str = Avx512Vbmi.PermuteVar64x8(multiAdd2.AsByte(), vbmiPackedLanesControl).AsSByte(); + return OperationStatus.Done == status; + } - Base64.AssertWrite>(dest, destStart, destLength); - str.Store((sbyte*)dest); - src += 64; - dest += 48; - } - while (src <= srcEnd); + /// + /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// + /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// A byte array which contains the result of the decoding operation. + /// contains a invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. + public static byte[] DecodeFromChars(ReadOnlySpan source) + { + Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; + OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); - srcBytes = (char*)src; - destBytes = dest; + return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : + throw new FormatException(SR.Format_BadBase64Char); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Decode(ref char* srcBytes, ref byte* destBytes, - char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder + private readonly struct Base64UrlDecoderByte : IBase64Decoder { - // If we have AVX2 support, pick off 32 bytes at a time for as long as we can, - // but make sure that we quit before seeing any == markers at the end of the - // string. Also, because we write 8 zeroes at the end of the output, ensure - // that there are at least 11 valid bytes of input data remaining to close the - // gap. 32 + 2 + 11 = 45 bytes. - - // See SSSE3-version below for an explanation of how the code works. - - // The JIT won't hoist these "constants", so help it - Vector256 lutHi = Vector256.Create(TBase64Decoder.Avx2LutHigh); - - Vector256 lutLo = Vector256.Create(TBase64Decoder.Avx2LutLow); - - Vector256 lutShift = Vector256.Create(TBase64Decoder.Avx2LutShift); - - Vector256 packBytesInLaneMask = Vector256.Create( - 2, 1, 0, 6, - 5, 4, 10, 9, - 8, 14, 13, 12, - -1, -1, -1, -1, - 2, 1, 0, 6, - 5, 4, 10, 9, - 8, 14, 13, 12, - -1, -1, -1, -1); - - Vector256 packLanesControl = Vector256.Create( - 0, 0, 0, 0, - 1, 0, 0, 0, - 2, 0, 0, 0, - 4, 0, 0, 0, - 5, 0, 0, 0, - 6, 0, 0, 0, - -1, -1, -1, -1, - -1, -1, -1, -1).AsInt32(); - - Vector256 maskSlashOrUnderscore = Vector256.Create((sbyte)TBase64Decoder.MaskSlashOrUnderscore); - Vector256 shiftForUnderscore = Vector256.Create((sbyte)33); - Vector256 mergeConstant0 = Vector256.Create(0x01400140).AsSByte(); - Vector256 mergeConstant1 = Vector256.Create(0x00011000).AsInt16(); - - ushort* src = (ushort*)srcBytes; - byte* dest = destBytes; - - //while (remaining >= 45) - do - { - AssertRead>(src, srcStart, sourceLength); - Vector256 utf16VectorLower = Avx.LoadVector256(src); - Vector256 utf16VectorUpper = Avx.LoadVector256(src + 16); - Vector256 str = Vector256.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); + public static ReadOnlySpan DecodingMap => + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, //62 is placed at index 45 (for -), 63 at index 95 (for _) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, //0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; - Vector256 hiNibbles = Avx2.And(Avx2.ShiftRightLogical(str.AsInt32(), 4).AsSByte(), maskSlashOrUnderscore); + public static ReadOnlySpan VbmiLookup0 => + [ + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80808080, + 0x80808080, 0x80808080, 0x80808080, 0x80803e80, + 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080 + ]; - if (!TBase64Decoder.TryDecode256Core(str, hiNibbles, maskSlashOrUnderscore, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) - { - break; - } + public static ReadOnlySpan VbmiLookup1 => + [ + 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, + 0x1211100f, 0x16151413, 0x80191817, 0x3f808080, + 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, + 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080 + ]; - // in, lower lane, bits, upper case are most significant bits, lower case are least significant bits: - // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ - // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG - // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD - // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA - - Vector256 merge_ab_and_bc = Avx2.MultiplyAddAdjacent(str.AsByte(), mergeConstant0); - // 0000kkkk LLllllll 0000JJJJ JJjjKKKK - // 0000hhhh IIiiiiii 0000GGGG GGggHHHH - // 0000eeee FFffffff 0000DDDD DDddEEEE - // 0000bbbb CCcccccc 0000AAAA AAaaBBBB - - Vector256 output = Avx2.MultiplyAddAdjacent(merge_ab_and_bc, mergeConstant1); - // 00000000 JJJJJJjj KKKKkkkk LLllllll - // 00000000 GGGGGGgg HHHHhhhh IIiiiiii - // 00000000 DDDDDDdd EEEEeeee FFffffff - // 00000000 AAAAAAaa BBBBbbbb CCcccccc - - // Pack bytes together in each lane: - output = Avx2.Shuffle(output.AsSByte(), packBytesInLaneMask).AsInt32(); - // 00000000 00000000 00000000 00000000 - // LLllllll KKKKkkkk JJJJJJjj IIiiiiii - // HHHHhhhh GGGGGGgg FFffffff EEEEeeee - // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa - - // Pack lanes - str = Avx2.PermuteVar8x32(output, packLanesControl).AsSByte(); - - Base64.AssertWrite>(dest, destStart, destLength); - Avx.Store(dest, str.AsByte()); - - src += 32; - dest += 24; - } - while (src <= srcEnd); + public static ReadOnlySpan Avx2LutHigh => + [ + 0x00, 0x00, 0x2d, 0x39, + 0x4f, 0x5a, 0x6f, 0x7a, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2d, 0x39, + 0x4f, 0x5a, 0x6f, 0x7a, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]; - srcBytes = (char*)src; - destBytes = dest; - } + public static ReadOnlySpan Avx2LutLow => + [ + 0x01, 0x01, 0x2d, 0x30, + 0x41, 0x50, 0x61, 0x70, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x2d, 0x30, + 0x41, 0x50, 0x61, 0x70, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01 + ]; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - [CompExactlyDependsOn(typeof(Ssse3))] - private static unsafe void Vector128Decode(ref char* srcBytes, ref byte* destBytes, char* srcEnd, int sourceLength, int destLength, ushort* srcStart, byte* destStart) - where TBase64Decoder : IBase64Decoder - { - Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian); - - // If we have Vector128 support, pick off 16 bytes at a time for as long as we can, - // but make sure that we quit before seeing any == markers at the end of the - // string. Also, because we write four zeroes at the end of the output, ensure - // that there are at least 6 valid bytes of input data remaining to close the - // gap. 16 + 2 + 6 = 24 bytes. - - // The input consists of six character sets in the Base64 alphabet, - // which we need to map back to the 6-bit values they represent. - // There are three ranges, two singles, and then there's the rest. - // - // # From To Add Characters - // 1 [43] [62] +19 + - // 2 [47] [63] +16 / - // 3 [48..57] [52..61] +4 0..9 - // 4 [65..90] [0..25] -65 A..Z - // 5 [97..122] [26..51] -71 a..z - // (6) Everything else => invalid input - - // We will use LUTS for character validation & offset computation - // Remember that 0x2X and 0x0X are the same index for _mm_shuffle_epi8, - // this allows to mask with 0x2F instead of 0x0F and thus save one constant declaration (register and/or memory access) - - // For offsets: - // Perfect hash for lut = ((src>>4)&0x2F)+((src==0x2F)?0xFF:0x00) - // 0000 = garbage - // 0001 = / - // 0010 = + - // 0011 = 0-9 - // 0100 = A-Z - // 0101 = A-Z - // 0110 = a-z - // 0111 = a-z - // 1000 >= garbage - - // For validation, here's the table. - // A character is valid if and only if the AND of the 2 lookups equals 0: - - // hi \ lo 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 - // LUT 0x15 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x13 0x1A 0x1B 0x1B 0x1B 0x1A - - // 0000 0X10 char NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI - // andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - - // 0001 0x10 char DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US - // andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - - // 0010 0x01 char ! " # $ % & ' ( ) * + , - . / - // andlut 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x00 0x01 0x01 0x01 0x00 - - // 0011 0x02 char 0 1 2 3 4 5 6 7 8 9 : ; < = > ? - // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x02 0x02 0x02 0x02 0x02 - - // 0100 0x04 char @ A B C D E F G H I J K L M N 0 - // andlut 0x04 0x00 0x00 0x00 0X00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 - - // 0101 0x08 char P Q R S T U V W X Y Z [ \ ] ^ _ - // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08 - - // 0110 0x04 char ` a b c d e f g h i j k l m n o - // andlut 0x04 0x00 0x00 0x00 0X00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 - // 0111 0X08 char p q r s t u v w x y z { | } ~ - // andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08 - - // 1000 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1001 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1010 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1011 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1100 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1101 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1110 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - // 1111 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 - - // The JIT won't hoist these "constants", so help it - Vector128 lutHi = Vector128.Create(TBase64Decoder.Vector128LutHigh).AsByte(); - Vector128 lutLo = Vector128.Create(TBase64Decoder.Vector128LutLow).AsByte(); - Vector128 lutShift = Vector128.Create(TBase64Decoder.Vector128LutShift).AsSByte(); - Vector128 packBytesMask = Vector128.Create(0x06000102, 0x090A0405, 0x0C0D0E08, 0xffffffff).AsSByte(); - Vector128 mergeConstant0 = Vector128.Create(0x01400140).AsByte(); - Vector128 mergeConstant1 = Vector128.Create(0x00011000).AsInt16(); - Vector128 one = Vector128.Create((byte)1); - Vector128 mask2F = Vector128.Create(TBase64Decoder.MaskSlashOrUnderscore); - Vector128 mask8F = Vector128.Create((byte)0x8F); - Vector128 shiftForUnderscore = Vector128.Create((byte)33); - ushort* src = (ushort*)srcBytes; - byte* dest = destBytes; - - //while (remaining >= 24) - do - { - AssertRead>(src, srcStart, sourceLength); - Vector128 utf16VectorLower = Vector128.LoadUnsafe(ref *src); - Vector128 utf16VectorUpper = Vector128.LoadUnsafe(ref *src, 8); - Vector128 str = Vector128.Narrow(utf16VectorLower, utf16VectorUpper); + public static ReadOnlySpan Avx2LutShift => + [ + 0, 0, 17, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 17, 4, + -65, -65, -71, -71, + 0, 0, 0, 0, + 0, 0, 0, 0 + ]; - // lookup - Vector128 hiNibbles = Vector128.ShiftRightLogical(str.AsInt32(), 4).AsByte() & mask2F; + public static byte MaskSlashOrUnderscore => (byte)'_'; // underscore - if (!TBase64Decoder.TryDecode128Core(str, hiNibbles, mask2F, mask8F, lutLo, lutHi, lutShift, shiftForUnderscore, out str)) - { - break; - } + public static ReadOnlySpan Vector128LutHigh => [0x392d0000, 0x7a6f5a4f, 0x00000000, 0x00000000]; - // in, bits, upper case are most significant bits, lower case are least significant bits - // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ - // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG - // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD - // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA + public static ReadOnlySpan Vector128LutLow => [0x302d0101, 0x70615041, 0x01010101, 0x01010101]; - Vector128 merge_ab_and_bc; - if (Ssse3.IsSupported) - { - merge_ab_and_bc = Ssse3.MultiplyAddAdjacent(str.AsByte(), mergeConstant0.AsSByte()); - } - else - { - Vector128 evens = AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(str, one).GetLower(), 6); - Vector128 odds = AdvSimd.Arm64.TransposeOdd(str, Vector128.Zero).AsUInt16(); - merge_ab_and_bc = Vector128.Add(evens, odds).AsInt16(); - } - // 0000kkkk LLllllll 0000JJJJ JJjjKKKK - // 0000hhhh IIiiiiii 0000GGGG GGggHHHH - // 0000eeee FFffffff 0000DDDD DDddEEEE - // 0000bbbb CCcccccc 0000AAAA AAaaBBBB + public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; - Vector128 output; - if (Ssse3.IsSupported) - { - output = Sse2.MultiplyAddAdjacent(merge_ab_and_bc, mergeConstant1); - } - else - { - Vector128 ievens = AdvSimd.ShiftLeftLogicalWideningLower(AdvSimd.Arm64.UnzipEven(merge_ab_and_bc, one.AsInt16()).GetLower(), 12); - Vector128 iodds = AdvSimd.Arm64.TransposeOdd(merge_ab_and_bc, Vector128.Zero).AsInt32(); - output = Vector128.Add(ievens, iodds).AsInt32(); - } - // 00000000 JJJJJJjj KKKKkkkk LLllllll - // 00000000 GGGGGGgg HHHHhhhh IIiiiiii - // 00000000 DDDDDDdd EEEEeeee FFffffff - // 00000000 AAAAAAaa BBBBbbbb CCcccccc - - // Pack bytes together: - str = SimdShuffle(output.AsByte(), packBytesMask.AsByte(), mask8F); - // 00000000 00000000 00000000 00000000 - // LLllllll KKKKkkkk JJJJJJjj IIiiiiii - // HHHHhhhh GGGGGGgg FFffffff EEEEeeee - // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa - - Base64.AssertWrite>(dest, destStart, destLength); - str.Store(dest); - - src += 16; - dest += 12; - } - while (src <= srcEnd); + public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); - srcBytes = (char*)src; - destBytes = dest; - } + public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely - [Conditional("DEBUG")] - private static unsafe void AssertRead(ushort* src, ushort* srcStart, int srcLength) - { - int vectorElements = Unsafe.SizeOf(); - ushort* readEnd = src + vectorElements; - ushort* srcEnd = srcStart + srcLength; + public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; - if (readEnd > srcEnd) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(Ssse3))] + public static bool TryDecode128Core( + Vector128 str, + Vector128 hiNibbles, + Vector128 maskSlashOrUnderscore, + Vector128 mask8F, + Vector128 lutLow, + Vector128 lutHigh, + Vector128 lutShift, + Vector128 shiftForUnderscore, + out Vector128 result) { - int srcIndex = (int)(src - srcStart); - Debug.Fail($"Read for {typeof(TVector)} is not within safe bounds. srcIndex: {srcIndex}, srcLength: {srcLength}"); - } - } + Vector128 lowerBound = SimdShuffle(lutLow, hiNibbles, mask8F); + Vector128 upperBound = SimdShuffle(lutHigh, hiNibbles, mask8F); - internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) - where TBase64Decoder : IBase64Decoder - { - const int BlockSize = 4; - Span buffer = stackalloc char[BlockSize]; - OperationStatus status = OperationStatus.Done; + Vector128 below = Vector128.LessThan(str, lowerBound); + Vector128 above = Vector128.GreaterThan(str, upperBound); + Vector128 eq5F = Vector128.Equals(str, maskSlashOrUnderscore); - while (!utf8.IsEmpty) - { - int encodedIdx = 0; - int bufferIdx = 0; - int skipped = 0; + // Take care as arguments are flipped in order! + Vector128 outside = Vector128.AndNot(below | above, eq5F); - for (; encodedIdx < utf8.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) + if (outside != Vector128.Zero) { - if (IsWhiteSpace(utf8[encodedIdx])) - { - skipped++; - } - else - { - buffer[bufferIdx] = utf8[encodedIdx]; - bufferIdx++; - } + result = default; + return false; } - utf8 = utf8.Slice(encodedIdx); - bytesConsumed += skipped; - - if (bufferIdx == 0) - { - continue; - } + Vector128 shift = SimdShuffle(lutShift.AsByte(), hiNibbles, mask8F); + str += shift; - bool hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; - bool localIsFinalBlock = !hasAnotherBlock; + result = str + (eq5F & shiftForUnderscore); + return true; + } - // If this block contains padding and there's another block, then only whitespace may follow for being valid. - if (hasAnotherBlock) - { - int paddingCount = GetPaddingCount(ref buffer[^1]); - if (paddingCount > 0) - { - hasAnotherBlock = false; - localIsFinalBlock = true; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static bool TryDecode256Core( + Vector256 str, + Vector256 hiNibbles, + Vector256 maskSlashOrUnderscore, + Vector256 lutLow, + Vector256 lutHigh, + Vector256 lutShift, + Vector256 shiftForUnderscore, + out Vector256 result) + { + Vector256 lowerBound = Avx2.Shuffle(lutLow, hiNibbles); + Vector256 upperBound = Avx2.Shuffle(lutHigh, hiNibbles); - if (localIsFinalBlock && !isFinalBlock) - { - localIsFinalBlock = false; - } + Vector256 below = Vector256.LessThan(str, lowerBound); + Vector256 above = Vector256.GreaterThan(str, upperBound); + Vector256 eq5F = Vector256.Equals(str, maskSlashOrUnderscore); - status = DecodeFromChars(buffer.Slice(0, bufferIdx), bytes, out int localConsumed, out int localWritten, localIsFinalBlock, ignoreWhiteSpace: false); - bytesConsumed += localConsumed; - bytesWritten += localWritten; + // Take care as arguments are flipped in order! + Vector256 outside = Vector256.AndNot(below | above, eq5F); - if (status != OperationStatus.Done) + if (outside != Vector256.Zero) { - return status; + result = default; + return false; } - // The remaining data must all be whitespace in order to be valid. - if (!hasAnotherBlock) - { - for (int i = 0; i < utf8.Length; ++i) - { - if (!IsWhiteSpace(utf8[i])) - { - // Revert previous dest increment, since an invalid state followed. - bytesConsumed -= localConsumed; - bytesWritten -= localWritten; + Vector256 shift = Avx2.Shuffle(lutShift, hiNibbles); + str += shift; - return OperationStatus.InvalidData; - } + result = str + (eq5F & shiftForUnderscore); + return true; + } - bytesConsumed++; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) + { + uint t0 = encodedBytes[0]; + uint t1 = encodedBytes[1]; + uint t2 = encodedBytes[2]; + uint t3 = encodedBytes[3]; - break; + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + { + return -1; // One or more chars falls outside the 00..ff range, invalid Base64Url character. } - bytes = bytes.Slice(localWritten); - } - - return status; - } + int i0 = Unsafe.Add(ref decodingMap, t0); + int i1 = Unsafe.Add(ref decodingMap, t1); + int i2 = Unsafe.Add(ref decodingMap, t2); + int i3 = Unsafe.Add(ref decodingMap, t3); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPaddingCount(ref char ptrToLastElement) - { - int padding = 0; + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; - if (ptrToLastElement == EncodingPad) padding++; - if (Unsafe.Subtract(ref ptrToLastElement, 1) == EncodingPad) padding++; + i0 |= i3; + i1 |= i2; - return padding; - } + i0 |= i1; + return i0; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) - { - for (int i = 0; i < span.Length; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int DecodeRemaining(byte* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3) { - if (!IsWhiteSpace(span[i])) + uint t0; + uint t1; + t2 = EncodingPad; + t3 = EncodingPad; + switch (remaining) + { + case 2: + t0 = srcEnd[-2]; + t1 = srcEnd[-1]; + break; + case 3: + t0 = srcEnd[-3]; + t1 = srcEnd[-2]; + t2 = srcEnd[-1]; + break; + case 4: + t0 = srcEnd[-4]; + t1 = srcEnd[-3]; + t2 = srcEnd[-2]; + t3 = srcEnd[-1]; + break; + default: + return -1; + } + + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) { - return i; + return -1; } - } - return -1; - } + int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); + int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int Decode(char* encodedBytes, ref sbyte decodingMap) - { - uint t0 = encodedBytes[0]; - uint t1 = encodedBytes[1]; - uint t2 = encodedBytes[2]; - uint t3 = encodedBytes[3]; + i0 <<= 18; + i1 <<= 12; - if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) - { - return -1; // One or more chars falls outside the 00..ff range, invalid Base64Url character. + i0 |= i1; + return i0; } - int i0 = Unsafe.Add(ref decodingMap, t0); - int i1 = Unsafe.Add(ref decodingMap, t1); - int i2 = Unsafe.Add(ref decodingMap, t2); - int i3 = Unsafe.Add(ref decodingMap, t3); - - i0 <<= 18; - i1 <<= 12; - i2 <<= 6; - - i0 |= i3; - i1 |= i2; - - i0 |= i1; - return i0; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!IsWhiteSpace(span[i])) + { + return i; + } + } - public static int DecodeFromChars(ReadOnlySpan source, Span destination) - { - OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); + return -1; + } - if (OperationStatus.Done == status) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, + ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder { - return bytesWritten; + return Base64.DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); } - if (OperationStatus.DestinationTooSmall == status) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector512(byte* src, byte* srcStart, int sourceLength, out Vector512 str) { - throw new ArgumentException("DestinationTooSmall", nameof(destination)); + Base64.AssertRead>(src, srcStart, sourceLength); + str = Vector512.Load(src).AsSByte(); + return true; } - throw new InvalidOperationException("InvalidData"); - } - - public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) - { - OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); - - return OperationStatus.Done == status; - } - - public static byte[] DecodeFromChars(ReadOnlySpan source) - { - Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; - OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe bool TryLoadAvxVector256(byte* src, byte* srcStart, int sourceLength, out Vector256 str) + { + Base64.AssertRead>(src, srcStart, sourceLength); + str = Avx.LoadVector256(src).AsSByte(); + return true; + } - return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : - throw new InvalidOperationException("InvalidData"); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int sourceLength, out Vector128 str) + { + Base64.AssertRead>(src, srcStart, sourceLength); + str = Vector128.LoadUnsafe(ref *src); + return true; + } } - private readonly struct Base64UrlDecoder : IBase64Decoder + private readonly struct Base64UrlDecoderChar : IBase64Decoder { public static ReadOnlySpan DecodingMap => [ @@ -1030,7 +694,7 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static byte MaskSlashOrUnderscore => (byte)'_'; // underscore - public static ReadOnlySpan Vector128LutHigh => [ 0x392d0000, 0x7a6f5a4f, 0x00000000, 0x00000000 ]; + public static ReadOnlySpan Vector128LutHigh => [0x392d0000, 0x7a6f5a4f, 0x00000000, 0x00000000]; public static ReadOnlySpan Vector128LutLow => [0x302d0101, 0x70615041, 0x01010101, 0x01010101]; @@ -1038,7 +702,7 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); - public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // Should we fail here? One byte cannot be decoded completely + public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; @@ -1113,6 +777,214 @@ public static bool TryDecode256Core( result = str + (eq5F & shiftForUnderscore); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int Decode(ushort* encodedBytes, ref sbyte decodingMap) + { + uint t0 = encodedBytes[0]; + uint t1 = encodedBytes[1]; + uint t2 = encodedBytes[2]; + uint t3 = encodedBytes[3]; + + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + { + return -1; // One or more chars falls outside the 00..ff range, invalid Base64Url character. + } + + int i0 = Unsafe.Add(ref decodingMap, t0); + int i1 = Unsafe.Add(ref decodingMap, t1); + int i2 = Unsafe.Add(ref decodingMap, t2); + int i3 = Unsafe.Add(ref decodingMap, t3); + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int DecodeRemaining(ushort* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3) + { + uint t0; + uint t1; + t2 = EncodingPad; + t3 = EncodingPad; + switch (remaining) + { + case 2: + t0 = srcEnd[-2]; + t1 = srcEnd[-1]; + break; + case 3: + t0 = srcEnd[-3]; + t1 = srcEnd[-2]; + t2 = srcEnd[-1]; + break; + case 4: + t0 = srcEnd[-4]; + t1 = srcEnd[-3]; + t2 = srcEnd[-2]; + t3 = srcEnd[-1]; + break; + default: + return -1; + } + + if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) + { + return -1; + } + + int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); + int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + return i0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!IsWhiteSpace(span[i])) + { + return i; + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, + ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + where TBase64Decoder : IBase64Decoder + { + return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector512(ushort* src, ushort* srcStart, int sourceLength, out Vector512 str) + { + AssertRead>(src, srcStart, sourceLength); + Vector512 utf16VectorLower = Vector512.Load(src); + Vector512 utf16VectorUpper = Vector512.Load(src + 32); + + if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + { + str = default; + return false; + } + + str = Vector512.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe bool TryLoadAvxVector256(ushort* src, ushort* srcStart, int sourceLength, out Vector256 str) + { + AssertRead>(src, srcStart, sourceLength); + Vector256 utf16VectorLower = Avx.LoadVector256(src); + Vector256 utf16VectorUpper = Avx.LoadVector256(src + 16); + + if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + { + str = default; + return false; + } + + str = Vector256.Narrow(utf16VectorLower, utf16VectorUpper).AsSByte(); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool TryLoadVector128(ushort* src, ushort* srcStart, int sourceLength, out Vector128 str) + { + AssertRead>(src, srcStart, sourceLength); + Vector128 utf16VectorLower = Vector128.LoadUnsafe(ref *src); + Vector128 utf16VectorUpper = Vector128.LoadUnsafe(ref *src, 8); + if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + { + str = default; + return false; + } + + str = Vector128.Narrow(utf16VectorLower, utf16VectorUpper); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool VectorContainsNonAsciiChar(Vector512 utf16Vector) + { + const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 + Vector512 zeroIsAscii = utf16Vector & Vector512.Create(asciiMask); + // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. + return zeroIsAscii != Vector512.Zero; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool VectorContainsNonAsciiChar(Vector256 utf16Vector) + { + if (Avx.IsSupported) + { + Vector256 asciiMaskForTestZ = Vector256.Create((ushort)0xFF80); + return !Avx.TestZ(utf16Vector.AsInt16(), asciiMaskForTestZ.AsInt16()); + } + else + { + const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 + Vector256 zeroIsAscii = utf16Vector & Vector256.Create(asciiMask); + // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. + return zeroIsAscii != Vector256.Zero; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) + { + // prefer architecture specific intrinsic as they offer better perf + if (Sse2.IsSupported) + { + if (Sse41.IsSupported) + { + Vector128 asciiMaskForTestZ = Vector128.Create((ushort)0xFF80); + // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. + return !Sse41.TestZ(utf16Vector.AsInt16(), asciiMaskForTestZ.AsInt16()); + } + else + { + Vector128 asciiMaskForAddSaturate = Vector128.Create((ushort)0x7F80); + // The operation below forces the 0x8000 bit of each WORD to be set iff the WORD element + // has value >= 0x0800 (non-ASCII). Then we'll treat the vector as a BYTE vector in order + // to extract the mask. Reminder: the 0x0080 bit of each WORD should be ignored. + return (Sse2.MoveMask(Sse2.AddSaturate(utf16Vector, asciiMaskForAddSaturate).AsByte()) & 0b_1010_1010_1010_1010) != 0; + } + } + else if (AdvSimd.Arm64.IsSupported) + { + // First we pick four chars, a larger one from all four pairs of adjecent chars in the vector. + // If any of those four chars has a non-ASCII bit set, we have seen non-ASCII data. + Vector128 maxChars = AdvSimd.Arm64.MaxPairwise(utf16Vector, utf16Vector); + return (maxChars.AsUInt64().ToScalar() & 0xFF80FF80FF80FF80) != 0; + } + else + { + const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 + Vector128 zeroIsAscii = utf16Vector & Vector128.Create(asciiMask); + // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. + return zeroIsAscii != Vector128.Zero; + } + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 2b2601c23d29c1..fe403659636410 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -34,7 +34,7 @@ public static partial class Base64Url /// The output will not be padded even if the input is not a multiple of 3. public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => - EncodeToUtf8(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + EncodeTo(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); /// /// Returns the length (in bytes) of the result if you were to encode binary data within a byte span of size "length". @@ -60,6 +60,7 @@ public static int GetEncodedLength(int bytesLength) /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. + /// Thrown when the encoded output cannot fit in the provided. /// The output will not be padded even if the input is not a multiple of 3. public static int EncodeToUtf8(ReadOnlySpan source, Span destination) { @@ -72,7 +73,7 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination if (OperationStatus.DestinationTooSmall == status) { - throw new ArgumentException("DestinationTooSmall", nameof(destination)); + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } Debug.Fail("Unreachable code"); @@ -88,10 +89,9 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination public static byte[] EncodeToUtf8(ReadOnlySpan source) { Span destination = stackalloc byte[GetEncodedLength(source.Length)]; - OperationStatus status = EncodeToUtf8(source, destination, out _, out int bytesWritten); + EncodeToUtf8(source, destination, out _, out int bytesWritten); - return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : - throw new InvalidOperationException("InvalidData"); + return destination.Slice(0, bytesWritten).ToArray(); } /// @@ -114,633 +114,7 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) => - EncodeToChars(source, destination, out bytesConsumed, out charsWritten, isFinalBlock); - - internal static unsafe OperationStatus EncodeToChars(ReadOnlySpan bytes, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) - where TBase64Encoder : IBase64Encoder - { - if (bytes.IsEmpty) - { - bytesConsumed = 0; - bytesWritten = 0; - return OperationStatus.Done; - } - - fixed (byte* srcBytes = &MemoryMarshal.GetReference(bytes)) - fixed (char* destBytes = &MemoryMarshal.GetReference(destination)) - { - int srcLength = bytes.Length; - int destLength = destination.Length; - int maxSrcLength = TBase64Encoder.GetMaxSrcLength(srcLength, destLength); - - byte* src = srcBytes; - char* dest = destBytes; - byte* srcEnd = srcBytes + (uint)srcLength; - byte* srcMax = srcBytes + (uint)maxSrcLength; - - if (maxSrcLength >= 16) - { - byte* end = srcMax - 64; - if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported && (end >= src)) - { - Avx512Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); - - if (src == srcEnd) - goto DoneExit; - } - - end = srcMax - 32; - if (Avx2.IsSupported && (end >= src)) - { - Avx2Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); - - if (src == srcEnd) - goto DoneExit; - } - - end = srcMax - 48; - if (AdvSimd.Arm64.IsSupported && (end >= src)) - { - AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); - - if (src == srcEnd) - goto DoneExit; - } - - end = srcMax - 16; - if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian && (end >= src)) - { - Vector128Encode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, (ushort*)destBytes); - - if (src == srcEnd) - goto DoneExit; - } - } - - ref byte encodingMap = ref MemoryMarshal.GetReference(TBase64Encoder.EncodingMap); - uint result = 0; - - srcMax -= 2; - while (src < srcMax) - { - EncodeAndWrite(src, dest, ref encodingMap); - src += 3; - dest += 4; - } - - if (srcMax + 2 != srcEnd) - goto DestinationTooSmallExit; - - if (!isFinalBlock) - { - if (src == srcEnd) - goto DoneExit; - - goto NeedMoreData; - } - - if (src + 1 == srcEnd) - { - result = TBase64Encoder.EncodeOneOptionallyPadTwo(src, ref encodingMap); - if (BitConverter.IsLittleEndian) - { - dest[0] = (char)(result & 0x7F); - dest[1] = (char)(result >> 8); - } - else - { - dest[0] = (char)(result >> 8); - dest[1] = (char)(result & 0x7F); - } - - src += 1; - dest += TBase64Encoder.IncrementPadTwo; - } - else if (src + 2 == srcEnd) - { - result = TBase64Encoder.EncodeTwoOptionallyPadOne(src, ref encodingMap); - if (BitConverter.IsLittleEndian) - { - dest[0] = (char)(result & 0x7F); - dest[1] = (char)((result >> 8) & 0x7F); - dest[2] = (char)(result >> 16); - } - else - { - dest[0] = (char)(result >> 16); - dest[1] = (char)((result >> 8) & 0x7F); - dest[2] = (char)(result & 0x7F); - } - - src += 2; - dest += TBase64Encoder.IncrementPadOne; - } - - DoneExit: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.Done; - - DestinationTooSmallExit: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.DestinationTooSmall; - - NeedMoreData: - bytesConsumed = (int)(src - srcBytes); - bytesWritten = (int)(dest - destBytes); - return OperationStatus.NeedMoreData; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Avx512BW))] - [CompExactlyDependsOn(typeof(Avx512Vbmi))] - private static unsafe void Avx512Encode(ref byte* srcBytes, ref char* destBytes, - byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) - where TBase64Encoder : IBase64Encoder - { - // Reference for VBMI implementation : https://github.com/WojciechMula/base64simd/tree/master/encode - // If we have AVX512 support, pick off 48 bytes at a time for as long as we can. - // But because we read 64 bytes at a time, ensure we have enough room to do a - // full 64-byte read without segfaulting. - - byte* src = srcBytes; - ushort* dest = (ushort*)destBytes; - - // The JIT won't hoist these "constants", so help it - Vector512 shuffleVecVbmi = Vector512.Create( - 0x01020001, 0x04050304, 0x07080607, 0x0a0b090a, - 0x0d0e0c0d, 0x10110f10, 0x13141213, 0x16171516, - 0x191a1819, 0x1c1d1b1c, 0x1f201e1f, 0x22232122, - 0x25262425, 0x28292728, 0x2b2c2a2b, 0x2e2f2d2e).AsSByte(); - Vector512 vbmiLookup = Vector512.Create(TBase64Encoder.EncodingMap).AsSByte(); - - Vector512 maskAC = Vector512.Create((uint)0x0fc0fc00).AsUInt16(); - Vector512 maskBB = Vector512.Create((uint)0x3f003f00); - Vector512 shiftAC = Vector512.Create((uint)0x0006000a).AsUInt16(); - Vector512 shiftBB = Vector512.Create((uint)0x00080004).AsUInt16(); - - Base64.AssertRead>(src, srcStart, sourceLength); - - // This algorithm requires AVX512VBMI support. - // Vbmi was first introduced in CannonLake and is available from IceLake on. - - // str = [...|PONM|LKJI|HGFE|DCBA] - Vector512 str = Vector512.Load(src).AsSByte(); - - while (true) - { - // Step 1 : Split 48 bytes into 64 bytes with each byte using 6-bits from input - // str = [...|KLJK|HIGH|EFDE|BCAB] - str = Avx512Vbmi.PermuteVar64x8(str, shuffleVecVbmi); - - // TO-DO- This can be achieved faster with multishift - // Consider the first 4 bytes - BCAB - // temp1 = [...|0000cccc|cc000000|aaaaaa00|00000000] - Vector512 temp1 = (str.AsUInt16() & maskAC); - - // temp2 = [...|00000000|00cccccc|00000000|00aaaaaa] - Vector512 temp2 = Avx512BW.ShiftRightLogicalVariable(temp1, shiftAC).AsUInt16(); - - // temp3 = [...|ccdddddd|00000000|aabbbbbb|cccc0000] - Vector512 temp3 = Avx512BW.ShiftLeftLogicalVariable(str.AsUInt16(), shiftBB).AsUInt16(); - - // str = [...|00dddddd|00cccccc|00bbbbbb|00aaaaaa] - str = Vector512.ConditionalSelect(maskBB, temp3.AsUInt32(), temp2.AsUInt32()).AsSByte(); - - // Step 2: Now we have the indices calculated. Next step is to use these indices to translate. - str = Avx512Vbmi.PermuteVar64x8(vbmiLookup, str); - - AssertWrite>(dest, destStart, destLength); - (Vector512 utf16LowVector, Vector512 utf16HighVector) = Vector512.Widen(str.AsByte()); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector512.Count); - - src += 48; - dest += 64; - - if (src > srcEnd) - break; - - Base64.AssertRead>(src, srcStart, sourceLength); - str = Vector512.Load(src).AsSByte(); - } - - srcBytes = src; - destBytes = (char*)dest; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Avx2))] - private static unsafe void Avx2Encode(ref byte* srcBytes, ref char* destBytes, - byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) - where TBase64Encoder : IBase64Encoder - { - // If we have AVX2 support, pick off 24 bytes at a time for as long as we can. - // But because we read 32 bytes at a time, ensure we have enough room to do a - // full 32-byte read without segfaulting. - - // translation from SSSE3 into AVX2 of procedure - // This one works with shifted (4 bytes) input in order to - // be able to work efficiently in the 2 128-bit lanes - - // srcBytes, bytes MSB to LSB: - // 0 0 0 0 x w v u t s r q p o n m - // l k j i h g f e d c b a 0 0 0 0 - - // The JIT won't hoist these "constants", so help it - Vector256 shuffleVec = Vector256.Create( - 5, 4, 6, 5, - 8, 7, 9, 8, - 11, 10, 12, 11, - 14, 13, 15, 14, - 1, 0, 2, 1, - 4, 3, 5, 4, - 7, 6, 8, 7, - 10, 9, 11, 10); - - Vector256 lut = Vector256.Create( - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0, - 65, 71, -4, -4, - -4, -4, -4, -4, - -4, -4, -4, -4, - TBase64Encoder.Avx2LutChar62, TBase64Encoder.Avx2LutChar63, 0, 0); - - Vector256 maskAC = Vector256.Create(0x0fc0fc00).AsSByte(); - Vector256 maskBB = Vector256.Create(0x003f03f0).AsSByte(); - Vector256 shiftAC = Vector256.Create(0x04000040).AsUInt16(); - Vector256 shiftBB = Vector256.Create(0x01000010).AsInt16(); - Vector256 const51 = Vector256.Create((byte)51); - Vector256 const25 = Vector256.Create((sbyte)25); - - byte* src = srcBytes; - ushort* dest = (ushort*)destBytes; - - // first load is done at c-0 not to get a segfault - Base64.AssertRead>(src, srcStart, sourceLength); - Vector256 str = Avx.LoadVector256(src).AsSByte(); - - // shift by 4 bytes, as required by Reshuffle - str = Avx2.PermuteVar8x32(str.AsInt32(), Vector256.Create( - 0, 0, 0, 0, - 0, 0, 0, 0, - 1, 0, 0, 0, - 2, 0, 0, 0, - 3, 0, 0, 0, - 4, 0, 0, 0, - 5, 0, 0, 0, - 6, 0, 0, 0).AsInt32()).AsSByte(); - - // Next loads are done at src-4, as required by Reshuffle, so shift it once - src -= 4; - - while (true) - { - // Reshuffle - str = Avx2.Shuffle(str, shuffleVec); - // str, bytes MSB to LSB: - // w x v w - // t u s t - // q r p q - // n o m n - // k l j k - // h i g h - // e f d e - // b c a b - - Vector256 t0 = Avx2.And(str, maskAC); - // bits, upper case are most significant bits, lower case are least significant bits. - // 0000wwww XX000000 VVVVVV00 00000000 - // 0000tttt UU000000 SSSSSS00 00000000 - // 0000qqqq RR000000 PPPPPP00 00000000 - // 0000nnnn OO000000 MMMMMM00 00000000 - // 0000kkkk LL000000 JJJJJJ00 00000000 - // 0000hhhh II000000 GGGGGG00 00000000 - // 0000eeee FF000000 DDDDDD00 00000000 - // 0000bbbb CC000000 AAAAAA00 00000000 - - Vector256 t2 = Avx2.And(str, maskBB); - // 00000000 00xxxxxx 000000vv WWWW0000 - // 00000000 00uuuuuu 000000ss TTTT0000 - // 00000000 00rrrrrr 000000pp QQQQ0000 - // 00000000 00oooooo 000000mm NNNN0000 - // 00000000 00llllll 000000jj KKKK0000 - // 00000000 00iiiiii 000000gg HHHH0000 - // 00000000 00ffffff 000000dd EEEE0000 - // 00000000 00cccccc 000000aa BBBB0000 - - Vector256 t1 = Avx2.MultiplyHigh(t0.AsUInt16(), shiftAC); - // 00000000 00wwwwXX 00000000 00VVVVVV - // 00000000 00ttttUU 00000000 00SSSSSS - // 00000000 00qqqqRR 00000000 00PPPPPP - // 00000000 00nnnnOO 00000000 00MMMMMM - // 00000000 00kkkkLL 00000000 00JJJJJJ - // 00000000 00hhhhII 00000000 00GGGGGG - // 00000000 00eeeeFF 00000000 00DDDDDD - // 00000000 00bbbbCC 00000000 00AAAAAA - - Vector256 t3 = Avx2.MultiplyLow(t2.AsInt16(), shiftBB); - // 00xxxxxx 00000000 00vvWWWW 00000000 - // 00uuuuuu 00000000 00ssTTTT 00000000 - // 00rrrrrr 00000000 00ppQQQQ 00000000 - // 00oooooo 00000000 00mmNNNN 00000000 - // 00llllll 00000000 00jjKKKK 00000000 - // 00iiiiii 00000000 00ggHHHH 00000000 - // 00ffffff 00000000 00ddEEEE 00000000 - // 00cccccc 00000000 00aaBBBB 00000000 - - str = Avx2.Or(t1.AsSByte(), t3.AsSByte()); - // 00xxxxxx 00wwwwXX 00vvWWWW 00VVVVVV - // 00uuuuuu 00ttttUU 00ssTTTT 00SSSSSS - // 00rrrrrr 00qqqqRR 00ppQQQQ 00PPPPPP - // 00oooooo 00nnnnOO 00mmNNNN 00MMMMMM - // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ - // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG - // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD - // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA - - // Translation - // LUT contains Absolute offset for all ranges: - // Translate values 0..63 to the Base64 alphabet. There are five sets: - // # From To Abs Index Characters - // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ - // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz - // 2 [52..61] [48..57] -4 [2..11] 0123456789 - // 3 [62] [43] -19 12 + - // 4 [63] [47] -16 13 / - - // Create LUT indices from input: - // the index for range #0 is right, others are 1 less than expected: - Vector256 indices = Avx2.SubtractSaturate(str.AsByte(), const51); - - // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0: - Vector256 mask = Avx2.CompareGreaterThan(str, const25); - - // subtract -1, so add 1 to indices for range #[1..4], All indices are now correct: - Vector256 tmp = Avx2.Subtract(indices.AsSByte(), mask); - - // Add offsets to input values: - str = Avx2.Add(str, Avx2.Shuffle(lut, tmp)); - - AssertWrite>(dest, destStart, destLength); - (Vector256 utf16LowVector, Vector256 utf16HighVector) = Vector256.Widen(str.AsByte()); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector256.Count); - - src += 24; - dest += 32; - - if (src > srcEnd) - break; - - // Load at src-4, as required by Reshuffle (already shifted by -4) - Base64.AssertRead>(src, srcStart, sourceLength); - str = Avx.LoadVector256(src).AsSByte(); - } - - srcBytes = src + 4; - destBytes = (char*)dest; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void AdvSimdEncode(ref byte* srcBytes, ref char* destBytes, - byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) - where TBase64Encoder : IBase64Encoder - { - // C# implementation of https://github.com/aklomp/base64/blob/3a5add8652076612a8407627a42c768736a4263f/lib/arch/neon64/enc_loop.c - Vector128 str1; - Vector128 str2; - Vector128 str3; - Vector128 res1; - Vector128 res2; - Vector128 res3; - Vector128 res4; - Vector128 tblEnc1 = Vector128.Create("ABCDEFGHIJKLMNOP"u8).AsByte(); - Vector128 tblEnc2 = Vector128.Create("QRSTUVWXYZabcdef"u8).AsByte(); - Vector128 tblEnc3 = Vector128.Create("ghijklmnopqrstuv"u8).AsByte(); - Vector128 tblEnc4 = Vector128.Create(TBase64Encoder.AdvSimdLut4).AsByte(); - byte* src = srcBytes; - ushort* dest = (ushort*)destBytes; - - // If we have Neon support, pick off 48 bytes at a time for as long as we can. - do - { - // Load 48 bytes and deinterleave: - Base64.AssertRead>(src, srcStart, sourceLength); - (str1, str2, str3) = AdvSimd.Arm64.LoadVector128x3AndUnzip(src); - - // Divide bits of three input bytes over four output bytes: - res1 = AdvSimd.ShiftRightLogical(str1, 2); - res2 = AdvSimd.ShiftRightLogical(str2, 4); - res3 = AdvSimd.ShiftRightLogical(str3, 6); - res2 = AdvSimd.ShiftLeftAndInsert(res2, str1, 4); - res3 = AdvSimd.ShiftLeftAndInsert(res3, str2, 2); - - // Clear top two bits: - res2 &= AdvSimd.DuplicateToVector128((byte)0x3F); - res3 &= AdvSimd.DuplicateToVector128((byte)0x3F); - res4 = str3 & AdvSimd.DuplicateToVector128((byte)0x3F); - - // The bits have now been shifted to the right locations; - // translate their values 0..63 to the Base64 alphabet. - // Use a 64-byte table lookup: - res1 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res1); - res2 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res2); - res3 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res3); - res4 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res4); - - // Interleave and store result: - AssertWrite>(dest, destStart, destLength); - (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); - (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); - AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); - (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); - (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); - AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); - - src += 48; - dest += 64; - } while (src <= srcEnd); - - srcBytes = src; - destBytes = (char*)dest; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Ssse3))] - [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - private static unsafe void Vector128Encode(ref byte* srcBytes, ref char* destBytes, - byte* srcEnd, int sourceLength, int destLength, byte* srcStart, ushort* destStart) - where TBase64Encoder : IBase64Encoder - { - // If we have SSSE3 support, pick off 12 bytes at a time for as long as we can. - // But because we read 16 bytes at a time, ensure we have enough room to do a - // full 16-byte read without segfaulting. - - // srcBytes, bytes MSB to LSB: - // 0 0 0 0 l k j i h g f e d c b a - - // The JIT won't hoist these "constants", so help it - Vector128 shuffleVec = Vector128.Create(0x01020001, 0x04050304, 0x07080607, 0x0A0B090A).AsByte(); - Vector128 lut = Vector128.Create(0xFCFC4741, 0xFCFCFCFC, 0xFCFCFCFC, TBase64Encoder.Ssse3AdvSimdLutE3).AsByte(); - Vector128 maskAC = Vector128.Create(0x0fc0fc00).AsByte(); - Vector128 maskBB = Vector128.Create(0x003f03f0).AsByte(); - Vector128 shiftAC = Vector128.Create(0x04000040).AsUInt16(); - Vector128 shiftBB = Vector128.Create(0x01000010).AsInt16(); - Vector128 const51 = Vector128.Create((byte)51); - Vector128 const25 = Vector128.Create((sbyte)25); - Vector128 mask8F = Vector128.Create((byte)0x8F); - - byte* src = srcBytes; - ushort* dest = (ushort*)destBytes; - - //while (remaining >= 16) - do - { - Base64.AssertRead>(src, srcStart, sourceLength); - Vector128 str = Vector128.LoadUnsafe(ref *src); - - // Reshuffle - str = SimdShuffle(str, shuffleVec, mask8F); - // str, bytes MSB to LSB: - // k l j k - // h i g h - // e f d e - // b c a b - - Vector128 t0 = str & maskAC; - // bits, upper case are most significant bits, lower case are least significant bits - // 0000kkkk LL000000 JJJJJJ00 00000000 - // 0000hhhh II000000 GGGGGG00 00000000 - // 0000eeee FF000000 DDDDDD00 00000000 - // 0000bbbb CC000000 AAAAAA00 00000000 - - Vector128 t2 = str & maskBB; - // 00000000 00llllll 000000jj KKKK0000 - // 00000000 00iiiiii 000000gg HHHH0000 - // 00000000 00ffffff 000000dd EEEE0000 - // 00000000 00cccccc 000000aa BBBB0000 - - Vector128 t1; - if (Ssse3.IsSupported) - { - t1 = Sse2.MultiplyHigh(t0.AsUInt16(), shiftAC); - } - else - { - Vector128 odd = Vector128.ShiftRightLogical(AdvSimd.Arm64.UnzipOdd(t0.AsUInt16(), t0.AsUInt16()), 6); - Vector128 even = Vector128.ShiftRightLogical(AdvSimd.Arm64.UnzipEven(t0.AsUInt16(), t0.AsUInt16()), 10); - t1 = AdvSimd.Arm64.ZipLow(even, odd); - } - // 00000000 00kkkkLL 00000000 00JJJJJJ - // 00000000 00hhhhII 00000000 00GGGGGG - // 00000000 00eeeeFF 00000000 00DDDDDD - // 00000000 00bbbbCC 00000000 00AAAAAA - - Vector128 t3 = t2.AsInt16() * shiftBB; - // 00llllll 00000000 00jjKKKK 00000000 - // 00iiiiii 00000000 00ggHHHH 00000000 - // 00ffffff 00000000 00ddEEEE 00000000 - // 00cccccc 00000000 00aaBBBB 00000000 - - str = t1.AsByte() | t3.AsByte(); - // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ - // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG - // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD - // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA - - // Translation - // LUT contains Absolute offset for all ranges: - // Translate values 0..63 to the Base64 alphabet. There are five sets: - // # From To Abs Index Characters - // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ - // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz - // 2 [52..61] [48..57] -4 [2..11] 0123456789 - // 3 [62] [43] -19 12 + - // 4 [63] [47] -16 13 / - - // Create LUT indices from input: - // the index for range #0 is right, others are 1 less than expected: - Vector128 indices; - if (Ssse3.IsSupported) - { - indices = Sse2.SubtractSaturate(str.AsByte(), const51); - } - else - { - indices = AdvSimd.SubtractSaturate(str.AsByte(), const51); - } - - // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0: - Vector128 mask = Vector128.GreaterThan(str.AsSByte(), const25); - - // subtract -1, so add 1 to indices for range #[1..4], All indices are now correct: - Vector128 tmp = indices.AsSByte() - mask; - - // Add offsets to input values: - str += SimdShuffle(lut, tmp.AsByte(), mask8F); - - AssertWrite>(dest, destStart, destLength); - (Vector128 utf16LowVector, Vector128 utf16HighVector) = Vector128.Widen(str); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector128.Count); - - src += 12; - dest += 16; - } - while (src <= srcEnd); - - srcBytes = src; - destBytes = (char*)dest; - } - - private static unsafe void AssertWrite(ushort* dest, ushort* destStart, int destLength) - { - int vectorElements = Unsafe.SizeOf(); - ushort* writeEnd = dest + vectorElements; - ushort* destEnd = destStart + destLength; - - if (writeEnd > destEnd) - { - int destIndex = (int)(dest - destStart); - Debug.Fail($"Write for {typeof(TVector)} is not within safe bounds. destIndex: {destIndex}, destLength: {destLength}"); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void EncodeAndWrite(byte* threeBytes, char* destination, ref byte encodingMap) - { - uint t0 = threeBytes[0]; - uint t1 = threeBytes[1]; - uint t2 = threeBytes[2]; - - uint i = (t0 << 16) | (t1 << 8) | t2; - - byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); - byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); - byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); - byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); - - if (BitConverter.IsLittleEndian) - { - destination[0] = (char)i0; - destination[1] = (char)i1; - destination[2] = (char)i2; - destination[3] = (char)i3; - } - else - { - destination[0] = (char)i3; - destination[1] = (char)i2; - destination[2] = (char)i1; - destination[3] = (char)i0; - } - } + EncodeTo(source, MemoryMarshal.Cast(destination), out bytesConsumed, out charsWritten, isFinalBlock); /// /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. @@ -748,6 +122,7 @@ private static unsafe void EncodeAndWrite(byte* threeBytes, char* destination, r /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. + /// Thrown when the encoded output cannot fit in the provided. /// The output will not be padded even if the input is not a multiple of 3. public static int EncodeToChars(ReadOnlySpan source, Span destination) { @@ -760,7 +135,7 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio if (OperationStatus.DestinationTooSmall == status) { - throw new ArgumentException("DestinationTooSmall", nameof(destination)); + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } Debug.Fail("Unreachable code"); @@ -837,59 +212,155 @@ public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destina /// if bytes encoded successfully, otherwise . public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) { - if (buffer.IsEmpty) + OperationStatus status = EncodeToUtf8InPlace(buffer, dataLength, out bytesWritten); + return status == OperationStatus.Done; + } + + private readonly struct Base64UrlEncoderChar : IBase64Encoder + { + public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; + + public static sbyte Avx2LutChar62 => -17; // char '-' diff + + public static sbyte Avx2LutChar63 => 32; // char '_' diff + + public static ReadOnlySpan AdvSimdLut4 => "wxyz0123456789-_"u8; + + public static uint Ssse3AdvSimdLutE3 => 0x000020EF; + + public static int IncrementPadTwo => 2; + + public static int IncrementPadOne => 3; + + public static int GetMaxSrcLength(int srcLength, int destLength) => + srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? + srcLength : (destLength >> 2) * 3 + destLength % 4; + + public static uint GetInPlaceDestinationLength(int encodedLength, int _) => 0; // not used for char encoding + + public static int GetMaxEncodedLength(int _) => 0; // not used for char encoding + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, ushort* dest, ref byte encodingMap) { - bytesWritten = 0; - return true; + uint t0 = oneByte[0]; + + uint i = t0 << 8; + + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 10)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 4) & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + dest[0] = (ushort)i0; + dest[1] = (ushort)i1; + } + else + { + dest[1] = (ushort)i0; + dest[0] = (ushort)i1; + } } - fixed (byte* bufferBytes = &MemoryMarshal.GetReference(buffer)) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, ushort* dest, ref byte encodingMap) { - int encodedLength = GetEncodedLength(dataLength); - if (buffer.Length < encodedLength) + uint t0 = twoBytes[0]; + uint t1 = twoBytes[1]; + + uint i = (t0 << 16) | (t1 << 8); + + uint i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + uint i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + uint i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + dest[0] = (ushort)i0; + dest[1] = (ushort)i1; + dest[2] = (ushort)i2; + } + else { - bytesWritten = 0; - return false; + dest[2] = (ushort)i0; + dest[1] = (ushort)i1; + dest[0] = (ushort)i2; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector512 str) + { + AssertWrite>(dest, destStart, destLength); + (Vector512 utf16LowVector, Vector512 utf16HighVector) = Vector512.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector512.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector256 str) + { + AssertWrite>(dest, destStart, destLength); + (Vector256 utf16LowVector, Vector256 utf16HighVector) = Vector256.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector256.Count); + } - int leftover = dataLength % 3; // how many bytes left after packs of 3 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector128 str) + { + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector, Vector128 utf16HighVector) = Vector128.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector128.Count); + } - uint destinationIndex = leftover > 0 ? (uint)(encodedLength - leftover - 1) : (uint)(encodedLength - 4); - uint sourceIndex = (uint)(dataLength - leftover); - uint result = 0; - ref byte encodingMap = ref MemoryMarshal.GetReference(Base64UrlEncoder.EncodingMap); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, + Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) + { + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); + (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); + AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); + (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); + (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); + AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void EncodeThreeAndWrite(byte* threeBytes, ushort* destination, ref byte encodingMap) + { + uint t0 = threeBytes[0]; + uint t1 = threeBytes[1]; + uint t2 = threeBytes[2]; - // encode last pack to avoid conditional in the main loop - if (leftover != 0) + uint i = (t0 << 16) | (t1 << 8) | t2; + + byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); + + if (BitConverter.IsLittleEndian) { - if (leftover == 1) - { - result = Base64UrlEncoder.EncodeOneOptionallyPadTwo(bufferBytes + sourceIndex, ref encodingMap); - } - else - { - result = Base64UrlEncoder.EncodeTwoOptionallyPadOne(bufferBytes + sourceIndex, ref encodingMap); - } - - Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); - destinationIndex -= 4; + destination[0] = i0; + destination[1] = i1; + destination[2] = i2; + destination[3] = i3; } - - sourceIndex -= 3; - while ((int)sourceIndex >= 0) + else { - result = Encode(bufferBytes + sourceIndex, ref encodingMap); - Unsafe.WriteUnaligned(bufferBytes + destinationIndex, result); - destinationIndex -= 4; - sourceIndex -= 3; + destination[3] = i0; + destination[2] = i1; + destination[1] = i2; + destination[0] = i3; } - - bytesWritten = encodedLength; - return true; } } - private readonly struct Base64UrlEncoder : IBase64Encoder + private readonly struct Base64UrlEncoderByte : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; @@ -906,10 +377,16 @@ public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLeng public static int IncrementPadOne => 3; public static int GetMaxSrcLength(int srcLength, int destLength) => - srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? srcLength : (destLength >> 2) * 3 + destLength % 4; + srcLength <= MaximumEncodeLength && destLength >= Base64Url.GetEncodedLength(srcLength) ? + srcLength : (destLength >> 2) * 3 + destLength % 4; + + public static uint GetInPlaceDestinationLength(int encodedLength, int leftOver) => + leftOver > 0 ? (uint)(encodedLength - leftOver - 1) : (uint)(encodedLength - 4); + + public static int GetMaxEncodedLength(int srcLength) => GetEncodedLength(srcLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte encodingMap) + public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, byte* dest, ref byte encodingMap) { uint t0 = oneByte[0]; @@ -920,16 +397,18 @@ public static unsafe uint EncodeOneOptionallyPadTwo(byte* oneByte, ref byte enco if (BitConverter.IsLittleEndian) { - return i0 | (i1 << 8); + dest[0] = (byte)i0; + dest[1] = (byte)i1; } else { - return (i0 << 8) | i1; + dest[1] = (byte)i0; + dest[0] = (byte)i1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte encodingMap) + public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, ref byte encodingMap) { uint t0 = twoBytes[0]; uint t1 = twoBytes[1]; @@ -942,11 +421,76 @@ public static unsafe uint EncodeTwoOptionallyPadOne(byte* twoBytes, ref byte enc if (BitConverter.IsLittleEndian) { - return i0 | (i1 << 8) | (i2 << 16); + dest[0] = (byte)i0; + dest[1] = (byte)i1; + dest[2] = (byte)i2; + } + else + { + dest[2] = (byte)i0; + dest[1] = (byte)i1; + dest[0] = (byte)i2; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) + { + Base64.AssertWrite>(dest, destStart, destLength); + str.Store(dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) + { + Base64.AssertWrite>(dest, destStart, destLength); + Avx.Store(dest, str.AsByte()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) + { + Base64.AssertWrite>(dest, destStart, destLength); + str.Store(dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, + Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) + { + Base64.AssertWrite>(dest, destStart, destLength); + AdvSimd.Arm64.StoreVector128x4AndZip(dest, (res1, res2, res3, res4)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void EncodeThreeAndWrite(byte* threeBytes, byte* destination, ref byte encodingMap) + { + uint t0 = threeBytes[0]; + uint t1 = threeBytes[1]; + uint t2 = threeBytes[2]; + + uint i = (t0 << 16) | (t1 << 8) | t2; + + byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); + byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); + byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); + byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); + + if (BitConverter.IsLittleEndian) + { + destination[0] = i0; + destination[1] = i1; + destination[2] = i2; + destination[3] = i3; } else { - return (i0 << 16) | (i1 << 8) | i2; + destination[3] = i0; + destination[2] = i1; + destination[1] = i2; + destination[0] = i3; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 502071837dacb2..510d3bd1b9f8e7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -78,7 +78,7 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int private readonly struct Base64UrlByteValidatable : Base64.IBase64Validatable { - private static readonly SearchValues s_validBase64UrlChars = SearchValues.Create(Base64UrlEncoder.EncodingMap); + private static readonly SearchValues s_validBase64UrlChars = SearchValues.Create(Base64UrlEncoderByte.EncodingMap); public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index 8f8e22ae94263b..d60a8e3c316f47 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -166,7 +166,7 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int private readonly struct Base64ByteValidatable : IBase64Validatable { - private static readonly SearchValues s_validBase64Chars = SearchValues.Create(Base64Encoder.EncodingMap); + private static readonly SearchValues s_validBase64Chars = SearchValues.Create(Base64EncoderByte.EncodingMap); public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); From febb4d9830b6c3b0eadff9391d9e21c632443549 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 16 May 2024 21:17:03 -0700 Subject: [PATCH 08/38] Move ref update to runtime and other cleanup --- .../ref/System.Memory.Forwards.cs | 1 + .../System.Memory/ref/System.Memory.cs | 28 ------------------- .../tests/Base64/Base64DecoderUnitTests.cs | 5 +++- .../tests/Base64/Base64ValidationUnitTests.cs | 13 +++++++++ .../Base64Url/Base64UrlDecoderUnitTests.cs | 7 +++-- .../Base64UrlEncodingAPIsUnitTests.cs | 8 +++--- .../Base64Url/Base64UrlValidationUnitTests.cs | 4 +-- .../src/System/Buffers/Text/Base64Decoder.cs | 20 ++----------- .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Text/Base64Url/Base64UrlDecoder.cs | 2 +- .../Text/Base64Url/Base64UrlValidator.cs | 7 +++-- .../System.Runtime/ref/System.Runtime.cs | 28 +++++++++++++++++++ 12 files changed, 66 insertions(+), 59 deletions(-) diff --git a/src/libraries/System.Memory/ref/System.Memory.Forwards.cs b/src/libraries/System.Memory/ref/System.Memory.Forwards.cs index e79102cfcd350f..5f99062730dc48 100644 --- a/src/libraries/System.Memory/ref/System.Memory.Forwards.cs +++ b/src/libraries/System.Memory/ref/System.Memory.Forwards.cs @@ -11,3 +11,4 @@ [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.MemoryManager<>))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.OperationStatus))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.Text.Base64))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.Text.Base64Url))] diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index c3c261714fb313..8aac4764a1498a 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -639,34 +639,6 @@ public static void WriteUIntPtrLittleEndian(System.Span destination, nuint } namespace System.Buffers.Text { - public static class Base64Url - { - public static byte[] DecodeFromChars(System.ReadOnlySpan source) { throw null; } - public static int DecodeFromChars(System.ReadOnlySpan source, System.Span destination) { throw null; } - public static System.Buffers.OperationStatus DecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } - public static byte[] DecodeFromUtf8(System.ReadOnlySpan source) { throw null; } - public static int DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } - public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } - public static int DecodeFromUtf8InPlace(System.Span buffer) { throw null; } - public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } - public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } - public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } - public static string EncodeToString(System.ReadOnlySpan source) { throw null; } - public static byte[] EncodeToUtf8(System.ReadOnlySpan source) { throw null; } - public static int EncodeToUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } - public static System.Buffers.OperationStatus EncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } - public static int GetEncodedLength(int bytesLength) { throw null; } - public static int GetMaxDecodedLength(int base64Length) { throw null; } - public static bool IsValid(System.ReadOnlySpan base64UrlText) { throw null; } - public static bool IsValid(System.ReadOnlySpan base64UrlText, out int decodedLength) { throw null; } - public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText) { throw null; } - public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText, out int decodedLength) { throw null; } - public static bool TryDecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryDecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } - public static bool TryEncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryEncodeToUtf8InPlace(System.Span buffer, int dataLength, out int bytesWritten) { throw null; } - } public static partial class Utf8Formatter { public static bool TryFormat(bool value, System.Span destination, out int bytesWritten, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index b3aed300f21882..fe919b8667bed5 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -272,6 +272,9 @@ public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, in [Theory] [InlineData("A", 0, 0)] + [InlineData("A===", 0, 0)] + [InlineData("A==", 0, 0)] + [InlineData("A=", 0, 0)] [InlineData("AQ", 0, 0)] [InlineData("AQI", 0, 0)] [InlineData("AQIDBA", 4, 3)] @@ -284,7 +287,7 @@ public void BasicDecodingWithFinalBlockTrueKnownInputInvalid(string inputString, Assert.Equal(OperationStatus.InvalidData, Base64.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); Assert.Equal(expectedConsumed, consumed); Assert.Equal(expectedWritten, decodedByteCount); // expectedWritten == decodedBytes.Length - Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); } [Theory] diff --git a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs index 54149adedc3ae9..62b978c60c5e1a 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64ValidationUnitTests.cs @@ -242,6 +242,19 @@ public void ValidateWithPaddingReturnsCorrectCountChars(string utf8WithByteToBeI Assert.Equal(expectedLength, decodedLength); } + [Theory] + [InlineData("YQ==", 1)] + [InlineData("YWI=", 2)] + [InlineData("YWJj", 3)] + public void DecodeEmptySpan(string utf8WithByteToBeIgnored, int expectedLength) + { + ReadOnlySpan utf8BytesWithByteToBeIgnored = utf8WithByteToBeIgnored.ToArray(); + + Assert.True(Base64.IsValid(utf8BytesWithByteToBeIgnored)); + Assert.True(Base64.IsValid(utf8BytesWithByteToBeIgnored, out int decodedLength)); + Assert.Equal(expectedLength, decodedLength); + } + [Theory] [InlineData("YWJ")] [InlineData("YW")] diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index 1843286032fc39..24ab7b6208eb91 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -271,11 +271,14 @@ public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, in [Theory] [InlineData("A", 0, 0, OperationStatus.InvalidData)] + [InlineData("A===", 0, 0, OperationStatus.InvalidData)] + [InlineData("A==", 0, 0, OperationStatus.InvalidData)] + [InlineData("A=", 0, 0, OperationStatus.InvalidData)] [InlineData("AQ", 2, 1, OperationStatus.Done)] // Padding is optional [InlineData("AQI", 3, 2, OperationStatus.Done)] [InlineData("AQIDBA", 6, 4, OperationStatus.Done)] [InlineData("AQIDBAU", 7, 5, OperationStatus.Done)] - public void BasicDecodingWithFinalBlockTrueInputWithoutPadding(string inputString, int expectedConsumed, int expectedWritten, OperationStatus expectedStatus) + public void BasicDecodingWithFinalBlockTrueInputWithoutPaddingOrInvalidData(string inputString, int expectedConsumed, int expectedWritten, OperationStatus expectedStatus) { Span source = Encoding.ASCII.GetBytes(inputString); Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; @@ -283,7 +286,7 @@ public void BasicDecodingWithFinalBlockTrueInputWithoutPadding(string inputStrin Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); Assert.Equal(expectedConsumed, consumed); Assert.Equal(expectedWritten, decodedByteCount); // expectedWritten == decodedBytes.Length - Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, decodedBytes.Length, source, decodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); } [Theory] diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs index c405836f5a2629..a2a0ffd197fb0e 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs @@ -174,14 +174,14 @@ public static IEnumerable EncodeToStringTests_TestData() } [Theory] - //[InlineData("\u5948cz_T", 0, 0)] // scalar code-path - //[InlineData("z_Ta123\u5948", 4, 3)] + [InlineData("\u5948cz_T", 0, 0)] // scalar code-path + [InlineData("z_Ta123\u5948", 4, 3)] [InlineData("\u5948z_T-H7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path - /*[InlineData("z_T-H7sqEkerqMweH1uSw\u5948==", 20, 15)] + [InlineData("z_T-H7sqEkerqMweH1uSw\u5948==", 20, 15)] [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u5948==", 44, 33)] [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, 0)] // Vector512 / Avx512Vbmi code-path - [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789\u5948==", 92, 69)]*/ + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789\u5948==", 92, 69)] public void BasicDecodingNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten) { Span source = inputString.ToArray(); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs index d94cbbc5ac9f88..af9782be773394 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -56,7 +56,7 @@ public void BasicValidationChars() } } - /*[Fact] TODO: Should we always account bytes length having remainder of 1 as invalid? + [Fact] public void BasicValidationInvalidInputLengthBytes() { var rnd = new Random(42); @@ -95,7 +95,7 @@ public void BasicValidationInvalidInputLengthChars() Assert.False(Base64Url.IsValid(source, out int decodedLength)); Assert.Equal(0, decodedLength); } - }*/ + } [Fact] public void ValidateEmptySpanBytes() diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 8a7bd43a51ca83..7148b87b5389af 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -577,7 +577,7 @@ private static int GetPaddingCount(ref byte ptrToLastElement) return padding; } - internal static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) + private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; @@ -1004,7 +1004,7 @@ private static unsafe void Vector128Decode(ref T* srcBytes, r } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) + private static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) { uint t0 = encodedBytes[0]; uint t1 = encodedBytes[1]; @@ -1028,27 +1028,13 @@ internal static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteThreeLowOrderBytes(byte* destination, int value) + private static unsafe void WriteThreeLowOrderBytes(byte* destination, int value) { destination[0] = (byte)(value >> 16); destination[1] = (byte)(value >> 8); destination[2] = (byte)value; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) - { - for (int i = 0; i < span.Length; i++) - { - if (!IsWhiteSpace(span[i])) - { - return i; - } - } - - return -1; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsWhiteSpace(int value) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index e3f2ae29b1677e..f2604824df815a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -669,7 +669,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) + private static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) { uint t0 = threeBytes[0]; uint t1 = threeBytes[1]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 4f5ce80a3b0155..94a1e36f8b276e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -183,7 +183,7 @@ public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span DecodeFrom(MemoryMarshal.Cast(source), destination, out charsConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 510d3bd1b9f8e7..847ccf5de44430 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -51,14 +51,15 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decoded private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { - if (length == 1) + // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. + int remainder = length % 4; + + if (remainder == 1) { decodedLength = 0; return false; } - // Padding is optional for Base64Url, so need to account remainder. - int remainder = length % 4; decodedLength = (int)((uint)length / 4 * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; return true; } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 0371e3037e2653..7fac639c8bca98 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7487,6 +7487,34 @@ public static partial class Base64 public static bool IsValid(System.ReadOnlySpan base64Text) { throw null; } public static bool IsValid(System.ReadOnlySpan base64Text, out int decodedLength) { throw null; } } + public static class Base64Url + { + public static byte[] DecodeFromChars(System.ReadOnlySpan source) { throw null; } + public static int DecodeFromChars(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus DecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static byte[] DecodeFromUtf8(System.ReadOnlySpan source) { throw null; } + public static int DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static int DecodeFromUtf8InPlace(System.Span buffer) { throw null; } + public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } + public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } + public static string EncodeToString(System.ReadOnlySpan source) { throw null; } + public static byte[] EncodeToUtf8(System.ReadOnlySpan source) { throw null; } + public static int EncodeToUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus EncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } + public static int GetEncodedLength(int bytesLength) { throw null; } + public static int GetMaxDecodedLength(int base64Length) { throw null; } + public static bool IsValid(System.ReadOnlySpan base64UrlText) { throw null; } + public static bool IsValid(System.ReadOnlySpan base64UrlText, out int decodedLength) { throw null; } + public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText) { throw null; } + public static bool IsValid(System.ReadOnlySpan utf8Base64UrlText, out int decodedLength) { throw null; } + public static bool TryDecodeFromChars(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryDecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryEncodeToChars(System.ReadOnlySpan source, System.Span destination, out int charsWritten) { throw null; } + public static bool TryEncodeToUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryEncodeToUtf8InPlace(System.Span buffer, int dataLength, out int bytesWritten) { throw null; } + } } namespace System.CodeDom.Compiler { From 0994bc8591afa3c5c4c4adb9f1c59368b8108a51 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 17 May 2024 09:52:09 -0700 Subject: [PATCH 09/38] Generalize the AdvSimd.Arm64 vectorization added recently --- .../src/System/Buffers/Text/Base64.cs | 4 ++ .../src/System/Buffers/Text/Base64Decoder.cs | 34 +++++++++++--- .../Text/Base64Url/Base64UrlDecoder.cs | 45 +++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index 97046645c8e061..ff24b38913b727 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -100,6 +100,8 @@ internal interface IBase64Decoder where T : unmanaged static abstract ReadOnlySpan Vector128LutHigh { get; } static abstract ReadOnlySpan Vector128LutLow { get; } static abstract ReadOnlySpan Vector128LutShift { get; } + static abstract ReadOnlySpan AdvSimdLutOne3 { get; } + static abstract uint AdvSimdLutTwo3Uint1 { get; } static abstract int SrcLength(bool isFinalBlock, int utf8Length); static abstract int GetMaxDecodedLength(int utf8Length); static abstract bool IsInValidLength(int bufferLength); @@ -131,6 +133,8 @@ static abstract OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper str); static abstract unsafe bool TryLoadAvxVector256(T* src, T* srcStart, int sourceLength, out Vector256 str); static abstract unsafe bool TryLoadVector128(T* src, T* srcStart, int sourceLength, out Vector128 str); + static abstract unsafe bool TryLoadArmVector128x4(T* src, T* srcStart, int sourceLength, + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index d45ce644ce4ea0..631db35612ffdd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -97,7 +97,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa end = srcMax - 66; if (AdvSimd.Arm64.IsSupported && (end >= src)) { - AdvSimdDecode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); + AdvSimdDecode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); if (src == srcEnd) { @@ -848,7 +848,9 @@ internal static Vector128 SimdShuffle(Vector128 left, Vector128(ref T* srcBytes, ref byte* destBytes, T* srcEnd, int sourceLength, int destLength, T* srcStart, byte* destStart) + where TBase64Decoder : IBase64Decoder + where T : unmanaged { // C# implementation of https://github.com/aklomp/base64/blob/3a5add8652076612a8407627a42c768736a4263f/lib/arch/neon64/dec_loop.c // If we have AdvSimd support, pick off 64 bytes at a time for as long as we can, @@ -881,7 +883,7 @@ private static unsafe void AdvSimdDecode(ref byte* srcBytes, ref byte* destBytes // 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 255, 255, 255 var decLutOne = (Vector128.AllBitsSet, Vector128.AllBitsSet, - Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0x3EFFFFFF, 0x3FFFFFFF).AsByte(), + Vector128.Create(TBase64Decoder.AdvSimdLutOne3).AsByte(), Vector128.Create(0x37363534, 0x3B3A3938, 0xFFFF3D3C, 0xFFFFFFFF).AsByte()); // Values in 'decLutTwo' maps input values from 63 to 127. @@ -891,18 +893,21 @@ private static unsafe void AdvSimdDecode(ref byte* srcBytes, ref byte* destBytes // 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255, 255 var decLutTwo = (Vector128.Create(0x0100FF00, 0x05040302, 0x09080706, 0x0D0C0B0A).AsByte(), Vector128.Create(0x11100F0E, 0x15141312, 0x19181716, 0xFFFFFFFF).AsByte(), - Vector128.Create(0x1B1AFFFF, 0x1F1E1D1C, 0x23222120, 0x27262524).AsByte(), + Vector128.Create(TBase64Decoder.AdvSimdLutTwo3Uint1, 0x1F1E1D1C, 0x23222120, 0x27262524).AsByte(), Vector128.Create(0x2B2A2928, 0x2F2E2D2C, 0x33323130, 0xFFFFFFFF).AsByte()); - byte* src = srcBytes; + T* src = srcBytes; byte* dest = destBytes; Vector128 offset = Vector128.Create(63); do { // Step 1: Load 64 bytes and de-interleave. - AssertRead>(src, srcStart, sourceLength); - var (str1, str2, str3, str4) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); + if (!TBase64Decoder.TryLoadArmVector128x4(src, srcStart, sourceLength, + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4)) + { + break; + } // Step 2: Map each valid input to its Base64 value. // We use two look-ups to compute partial results and combine them later. @@ -1287,6 +1292,10 @@ internal static bool IsWhiteSpace(int value) public static ReadOnlySpan Vector128LutShift => [0x04131000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + public static ReadOnlySpan AdvSimdLutOne3 => [0xFFFFFFFF, 0xFFFFFFFF, 0x3EFFFFFF, 0x3FFFFFFF]; + + public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFFFF; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); @@ -1467,6 +1476,17 @@ public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int source str = Vector128.LoadUnsafe(ref *src); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe bool TryLoadArmVector128x4(byte* src, byte* srcStart, int sourceLength, + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) + { + AssertRead>(src, srcStart, sourceLength); + (str1, str2, str3, str4) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); + + return true; + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 94a1e36f8b276e..0b311f8290c9ac 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -420,6 +420,10 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + public static ReadOnlySpan AdvSimdLutOne3 => [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF3EFF]; + + public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFF3F; + public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely @@ -616,6 +620,17 @@ public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int source str = Vector128.LoadUnsafe(ref *src); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe bool TryLoadArmVector128x4(byte* src, byte* srcStart, int sourceLength, + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) + { + AssertRead>(src, srcStart, sourceLength); + (str1, str2, str3, str4) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); + + return true; + } } private readonly struct Base64UrlDecoderChar : IBase64Decoder @@ -700,6 +715,10 @@ public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int source public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + public static ReadOnlySpan AdvSimdLutOne3 => [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF3EFF]; + + public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFF3F; + public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely @@ -923,6 +942,32 @@ public static unsafe bool TryLoadVector128(ushort* src, ushort* srcStart, int so return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, int sourceLength, + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) + { + AssertRead>(src, srcStart, sourceLength); + var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); + var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); + + if (VectorContainsNonAsciiChar(s11) || VectorContainsNonAsciiChar(s12) || + VectorContainsNonAsciiChar(s21) || VectorContainsNonAsciiChar(s22) || + VectorContainsNonAsciiChar(s31) || VectorContainsNonAsciiChar(s32) || + VectorContainsNonAsciiChar(s41) || VectorContainsNonAsciiChar(s42)) + { + str1 = str2 = str3 = str4 = default; + return false; + } + + str1 = Vector128.Narrow(s11, s12); + str2 = Vector128.Narrow(s21, s22); + str3 = Vector128.Narrow(s31, s32); + str4 = Vector128.Narrow(s41, s42); + + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool VectorContainsNonAsciiChar(Vector512 utf16Vector) { From 492694bdc36ed9afe2a424ca0657a3a81f21d4e9 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 17 May 2024 11:00:11 -0700 Subject: [PATCH 10/38] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs | 4 ++-- .../src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs | 2 +- .../src/System/Buffers/Text/Base64Validator.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 0b311f8290c9ac..340a46a2d80bdb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -12,7 +12,7 @@ namespace System.Buffers.Text { - // AVX2 and Vector128 version based on https://github.com/gfoidl/Base64/blob/master/source/gfoidl.Base64/Internal/Encodings/Base64UrlEncoding.cs + // AVX2 and Vector128 version based on https://github.com/gfoidl/Base64/blob/5383320e28cac6c7ac6f86502fb05d23a048a21d/source/gfoidl.Base64/Internal/Encodings/Base64UrlEncoding.cs public static partial class Base64Url { @@ -29,7 +29,7 @@ public static int GetMaxDecodedLength(int base64Length) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); } - int remainder = base64Length % 4; + int remainder = (int)((uint)base64Length % 4); return (base64Length >> 2) * 3 + (remainder > 0 ? remainder - 1 : 0); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 847ccf5de44430..57596397ce9983 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -52,7 +52,7 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decoded private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. - int remainder = length % 4; + int remainder = (int)((uint)length % 4); if (remainder == 1) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index d60a8e3c316f47..a2df9364847691 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -134,7 +134,7 @@ internal static bool IsValid(ReadOnlySpan base64Text, private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { - if (length % 4 == 0) + if ((uint)length % 4 == 0) { // Remove padding to get exact length. decodedLength = (int)((uint)length / 4 * 3) - paddingCount; From c45f58c3288e29d6e37d249403150f86132d4e07 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 17 May 2024 11:17:10 -0700 Subject: [PATCH 11/38] Apply some feedback --- .../ref/System.Memory.Forwards.cs | 1 - .../src/System/Buffers/Text/Base64Decoder.cs | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Memory/ref/System.Memory.Forwards.cs b/src/libraries/System.Memory/ref/System.Memory.Forwards.cs index 5f99062730dc48..e79102cfcd350f 100644 --- a/src/libraries/System.Memory/ref/System.Memory.Forwards.cs +++ b/src/libraries/System.Memory/ref/System.Memory.Forwards.cs @@ -11,4 +11,3 @@ [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.MemoryManager<>))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.OperationStatus))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.Text.Base64))] -[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.Text.Base64Url))] diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 631db35612ffdd..65eb317e4f8d2b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -382,17 +382,20 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); - while (sourceIndex + 4 < bufferLength) + if (bufferLength > 4) { - int result = Decode(bufferBytes + sourceIndex, ref decodingMap); - if (result < 0) + while (sourceIndex < bufferLength - 4) { - goto InvalidExit; - } + int result = Decode(bufferBytes + sourceIndex, ref decodingMap); + if (result < 0) + { + goto InvalidExit; + } - WriteThreeLowOrderBytes(bufferBytes + destIndex, result); - destIndex += 3; - sourceIndex += 4; + WriteThreeLowOrderBytes(bufferBytes + destIndex, result); + destIndex += 3; + sourceIndex += 4; + } } uint t0; From 4bcd58aa2b0baa631366012cf49855e08bfe9917 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 17 May 2024 15:33:30 -0700 Subject: [PATCH 12/38] Try fix ARM failure --- .../System/Buffers/Text/Base64Url/Base64UrlDecoder.cs | 8 ++++---- .../System/Buffers/Text/Base64Url/Base64UrlEncoder.cs | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 340a46a2d80bdb..970f5d19738b1c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -960,10 +960,10 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i return false; } - str1 = Vector128.Narrow(s11, s12); - str2 = Vector128.Narrow(s21, s22); - str3 = Vector128.Narrow(s31, s32); - str4 = Vector128.Narrow(s41, s42); + str1 = AdvSimd.Arm64.UnzipEven(s11.AsByte(), s12.AsByte()); + str2 = AdvSimd.Arm64.UnzipEven(s21.AsByte(), s22.AsByte()); + str3 = AdvSimd.Arm64.UnzipEven(s31.AsByte(), s32.AsByte()); + str4 = AdvSimd.Arm64.UnzipEven(s41.AsByte(), s42.AsByte()); return true; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index fe403659636410..19ce7330930fed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -321,12 +321,11 @@ public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, in Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) { AssertWrite>(dest, destStart, destLength); - (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); - (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); - AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); - (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); - (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); - AdvSimd.Arm64.StoreVector128x4(dest, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); + Vector128 vecWide1 = AdvSimd.Arm64.ZipLow(res1, Vector128.Zero).AsUInt16(); + Vector128 vecWide2 = AdvSimd.Arm64.ZipLow(res2, Vector128.Zero).AsUInt16(); + Vector128 vecWide3 = AdvSimd.Arm64.ZipLow(res3, Vector128.Zero).AsUInt16(); + Vector128 vecWide4 = AdvSimd.Arm64.ZipLow(res4, Vector128.Zero).AsUInt16(); + AdvSimd.Arm64.StoreVector128x4(dest, (vecWide1, vecWide2, vecWide3, vecWide4)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From b0115c58e4ca2dc44be60b664fd1b5d7be37f328 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 17 May 2024 17:42:54 -0700 Subject: [PATCH 13/38] Use array pool whenever applicable --- .../Text/Base64Url/Base64UrlDecoder.cs | 42 ++++++++++++++++++- .../Text/Base64Url/Base64UrlEncoder.cs | 13 +++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 970f5d19738b1c..c1b25371125edb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -147,8 +147,27 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti /// more than two padding characters, or a non-white space-character among the padding characters. public static byte[] DecodeFromUtf8(ReadOnlySpan source) { - Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; + int upperBound = GetMaxDecodedLength(source.Length); + Span destination = stackalloc byte[256]; + byte[]? rented = null; + + if (upperBound <= destination.Length) + { + destination = destination.Slice(0, upperBound); + } + else + { + rented = ArrayPool.Shared.Rent(upperBound); + destination = rented.AsSpan(0, upperBound); + } + OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); + byte[] ret = destination.Slice(0, bytesWritten).ToArray(); + + if (rented is not null) + { + ArrayPool.Shared.Return(rented); + } return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : throw new FormatException(SR.Format_BadBase64Char); @@ -331,8 +350,27 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest /// more than two padding characters, or a non-white space-character among the padding characters. public static byte[] DecodeFromChars(ReadOnlySpan source) { - Span destination = stackalloc byte[GetMaxDecodedLength(source.Length)]; + int upperBound = GetMaxDecodedLength(source.Length); + Span destination = stackalloc byte[256]; + byte[]? rented = null; + + if (upperBound <= destination.Length) + { + destination = destination.Slice(0, upperBound); + } + else + { + rented = ArrayPool.Shared.Rent(upperBound); + destination = rented.AsSpan(0, upperBound); + } + OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); + byte[] ret = destination.Slice(0, bytesWritten).ToArray(); + + if (rented is not null) + { + ArrayPool.Shared.Return(rented); + } return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : throw new FormatException(SR.Format_BadBase64Char); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 19ce7330930fed..0f5f7de1d2b9ce 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -88,8 +88,9 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination /// The output will not be padded even if the input is not a multiple of 3. public static byte[] EncodeToUtf8(ReadOnlySpan source) { - Span destination = stackalloc byte[GetEncodedLength(source.Length)]; + Span destination = new byte[GetEncodedLength(source.Length)]; EncodeToUtf8(source, destination, out _, out int bytesWritten); + Debug.Assert(destination.Length == bytesWritten); return destination.Slice(0, bytesWritten).ToArray(); } @@ -150,8 +151,9 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio /// The output will not be padded even if the input is not a multiple of 3. public static char[] EncodeToChars(ReadOnlySpan source) { - Span destination = stackalloc char[GetEncodedLength(source.Length)]; - EncodeToChars(source, destination, out _, out _); + Span destination = new char[GetEncodedLength(source.Length)]; + EncodeToChars(source, destination, out int bytesWritten, out _); + Debug.Assert(destination.Length == bytesWritten); return destination.ToArray(); } @@ -164,8 +166,9 @@ public static char[] EncodeToChars(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static string EncodeToString(ReadOnlySpan source) { - Span destination = stackalloc char[GetEncodedLength(source.Length)]; - EncodeToChars(source, destination, out _, out _); + Span destination = new char[GetEncodedLength(source.Length)]; + EncodeToChars(source, destination, out int bytesWritten, out _); + Debug.Assert(destination.Length == bytesWritten); return new string(destination); } From d1faf6ac337a76137b45564d3154abeaff9c3727 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 20 May 2024 12:46:00 -0700 Subject: [PATCH 14/38] Handle '%' as url padding, add more tests and fix findings --- .../tests/Base64/Base64DecoderUnitTests.cs | 6 +- .../tests/Base64/Base64TestHelper.cs | 3 +- .../Base64Url/Base64UrlDecoderUnitTests.cs | 85 ++++++++++++++----- .../Base64Url/Base64UrlEncoderUnitTests.cs | 52 ++++++++++++ ...ts.cs => Base64UrlUnicodeAPIsUnitTests.cs} | 29 ++++++- .../tests/System.Memory.Tests.csproj | 2 +- .../src/System/Buffers/Text/Base64.cs | 3 +- .../src/System/Buffers/Text/Base64Decoder.cs | 23 ++--- .../Text/Base64Url/Base64UrlDecoder.cs | 27 +++--- .../Text/Base64Url/Base64UrlEncoder.cs | 6 +- .../System.Runtime/ref/System.Runtime.cs | 4 +- 11 files changed, 186 insertions(+), 54 deletions(-) rename src/libraries/System.Memory/tests/Base64Url/{Base64UrlEncodingAPIsUnitTests.cs => Base64UrlUnicodeAPIsUnitTests.cs} (97%) diff --git a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs index fe919b8667bed5..b31393846c7466 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64DecoderUnitTests.cs @@ -294,9 +294,11 @@ public void BasicDecodingWithFinalBlockTrueKnownInputInvalid(string inputString, [InlineData("\u00ecz/T", 0, 0)] // scalar code-path [InlineData("z/Ta123\u00ec", 4, 3)] [InlineData("\u00ecz/TpH7sqEkerqMweH1uSw==", 0, 0)] // Vector128 code-path - [InlineData("z/TpH7sqEkerqMweH1uSw\u00ec==", 20, 15)] - [InlineData("\u00ecz/TpH7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path + [InlineData("z/TpH7sqEkerqMweH1uSw\u5948==", 20, 15)] + [InlineData("\u5948/TpH7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo==", 0, 0)] // Vector256 / AVX code-path [InlineData("z/TpH7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u00ec==", 44, 33)] + [InlineData("\u5948z+T/H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, 0)] // Vector512 / Avx512Vbmi code-path + [InlineData("z/T+H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789\u5948==", 92, 69)] public void BasicDecodingNonAsciiInputInvalid(string inputString, int expectedConsumed, int expectedWritten) { Span source = Encoding.UTF8.GetBytes(inputString); diff --git a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs index 4637f042aaa0ee..d671d2c5e3cdef 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs @@ -77,6 +77,7 @@ public static class Base64TestHelper public static bool IsByteToBeIgnored(byte charByte) => charByte is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n'; public const byte EncodingPad = (byte)'='; // '=', for padding + public const byte UrlEncodingPad = (byte)'%'; // '%', for url padding public const sbyte InvalidByte = -1; // Designating -1 for invalid bytes in the decoding map public static byte[] InvalidBytes @@ -198,7 +199,7 @@ public static bool VerifyUrlDecodingCorrectness(int expectedConsumed, int expect string sourceString = Encoding.ASCII.GetString(source.Slice(0, expectedConsumed)); string padded = sourceString.Length % 4 == 0 ? sourceString : sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); - string base64 = padded.Replace("_", "/").Replace("-", "+"); + string base64 = padded.Replace('_', '/').Replace('-', '+').Replace('%', '='); byte[] expectedBytes = Convert.FromBase64String(base64); return expectedBytes.AsSpan().SequenceEqual(decodedBytes.Slice(0, expectedWritten)); } diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index 24ab7b6208eb91..5287b32e53be2c 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -32,6 +32,27 @@ public void BasicDecoding() } } + [Fact] + public void BasicDecodingByteArrayReturnOverload() + { + var rnd = new Random(42); + for (int i = 0; i < 5; i++) + { + int numBytes; + do + { + numBytes = rnd.Next(100, 1000 * 1000); + } while (numBytes % 4 == 1); // ensure we have a valid length + + Span source = new byte[numBytes]; + Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + + Span decodedBytes = Base64Url.DecodeFromUtf8(source);; + Assert.Equal(decodedBytes.Length, Base64Url.GetMaxDecodedLength(source.Length)); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(source.Length, decodedBytes.Length, source, decodedBytes)); + } + } + [Fact] public void BasicDecodingInvalidInputLength() { @@ -139,12 +160,10 @@ public void DecodeGuid() { Span source = new byte[22]; // For Base64Url padding ignored Span providedBytes = Guid.NewGuid().ToByteArray(); - Base64Url.EncodeToUtf8(providedBytes, source, out int _, out int _); + Base64Url.EncodeToUtf8(providedBytes, source); Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; - Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); - Assert.Equal(22, consumed); - Assert.Equal(16, decodedByteCount); + Assert.Equal(16, Base64Url.DecodeFromUtf8(source, decodedBytes)); Assert.True(providedBytes.SequenceEqual(decodedBytes)); } @@ -254,7 +273,7 @@ public void DecodingOutputTooSmallRetry(bool isFinalBlock) [InlineData("AQ==", 1)] [InlineData("AQI=", 2)] [InlineData("AQID", 3)] - [InlineData("AQIDBA==", 4)] + [InlineData("AQIDBA%%", 4)] [InlineData("AQIDBAU=", 5)] [InlineData("AQIDBAUG", 6)] public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, int expectedWritten) @@ -262,11 +281,8 @@ public void BasicDecodingWithFinalBlockTrueKnownInputDone(string inputString, in Span source = Encoding.ASCII.GetBytes(inputString); Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; - int expectedConsumed = inputString.Length; - Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); - Assert.Equal(expectedConsumed, consumed); - Assert.Equal(expectedWritten, decodedByteCount); - Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + Assert.Equal(expectedWritten, Base64Url.DecodeFromUtf8(source, decodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(inputString.Length, expectedWritten, source, decodedBytes)); } [Theory] @@ -289,6 +305,35 @@ public void BasicDecodingWithFinalBlockTrueInputWithoutPaddingOrInvalidData(stri Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); } + [Theory] + [InlineData("A", 0, false)] + [InlineData("A===", 0, false)] + [InlineData("A==", 0, false)] + [InlineData("A=", 0, false)] + [InlineData("AQ", 1, true)] // Padding is optional + [InlineData("AQI", 2, true)] + [InlineData("AQID", 3, true)] + [InlineData("AQIDB", 3, false)] + [InlineData("AQIDBA", 4, true)] + [InlineData("AQIDBAU", 5, true)] + [InlineData("AQ==", 1, true)] + [InlineData("AQI%", 2, true)] + [InlineData("AQIDBA%%", 4, true)] + [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u5948==", 33, false)] + [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, false)] + public void TryDecodeFromUtf8VariousInput(string inputString, int expectedWritten, bool expectedStatus) + { + Span source = Encoding.ASCII.GetBytes(inputString); + Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + + Assert.Equal(expectedStatus, Base64Url.TryDecodeFromUtf8(source, decodedBytes, out int bytesWritten)); + Assert.Equal(expectedWritten, bytesWritten); + if (expectedStatus) + { + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(inputString.Length, expectedWritten, source, decodedBytes)); + } + } + [Theory] [InlineData("\u5948cz_T", 0, 0)] // scalar code-path [InlineData("z_Ta123\u5948", 4, 3)] @@ -343,7 +388,7 @@ public void BasicDecodingWithFinalBlockFalseKnownInputNeedMoreData(string inputS [Theory] [InlineData("AQ==", 0, 0)] - [InlineData("AQI=", 0, 0)] + [InlineData("AQI%", 0, 0)] [InlineData("AQIDBA==", 4, 3)] [InlineData("AQIDBAU=", 4, 3)] public void BasicDecodingWithFinalBlockFalseKnownInputInvalid(string inputString, int expectedConsumed, int expectedWritten) @@ -378,9 +423,10 @@ public void DecodingInvalidBytes(bool isFinalBlock) for (int i = 0; i < invalidBytes.Length; i++) { - // Don't test padding (byte 61 i.e. '='), which is tested in DecodingInvalidBytesPadding + // Don't test padding (byte 61 i.e. '=' or '%'), which is tested in DecodingInvalidBytesPadding // Don't test chars to be ignored (spaces: 9, 10, 13, 32 i.e. '\n', '\t', '\r', ' ') if (invalidBytes[i] == Base64TestHelper.EncodingPad || + invalidBytes[i] == Base64TestHelper.UrlEncodingPad || Base64TestHelper.IsByteToBeIgnored(invalidBytes[i])) { continue; @@ -410,9 +456,7 @@ public void DecodingInvalidBytes(bool isFinalBlock) { Span source = "2222PPP"u8.ToArray(); // incomplete input Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; - Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount)); - Assert.Equal(7, consumed); - Assert.Equal(5, decodedByteCount); + Assert.Equal(5, Base64Url.DecodeFromUtf8(source, decodedBytes)); Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(7, 5, source, decodedBytes)); } } @@ -479,18 +523,18 @@ public void DecodingInvalidBytesPadding(bool isFinalBlock) Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out int consumed, out int decodedByteCount, isFinalBlock)); Assert.Equal(expectedConsumed, consumed); Assert.Equal(expectedWritten, decodedByteCount); - Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); source = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; - source[7] = Base64TestHelper.EncodingPad; // valid input - "2222PPP=" + source[7] = Base64TestHelper.UrlEncodingPad; // valid input - "2222PPP=" expectedConsumed = isFinalBlock ? source.Length : 4; expectedWritten = isFinalBlock ? 5 : 3; Assert.Equal(expectedStatus, Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out decodedByteCount, isFinalBlock)); Assert.Equal(expectedConsumed, consumed); Assert.Equal(expectedWritten, decodedByteCount); - Assert.True(Base64TestHelper.VerifyDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); + Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(expectedConsumed, expectedWritten, source, decodedBytes)); } } @@ -526,7 +570,7 @@ private static bool VerifyUrlDecodingCorrectness(string sourceString, Span { string padded = sourceString.Length % 4 == 0 ? sourceString : sourceString.PadRight(sourceString.Length + (4 - sourceString.Length % 4), '='); - byte[] expectedBytes = Convert.FromBase64String(padded.Replace("_", "/").Replace("-", "+")); + byte[] expectedBytes = Convert.FromBase64String(padded.Replace('_', '/').Replace('-', '+').Replace('%', '=')); return expectedBytes.AsSpan().SequenceEqual(decodedBytes); } @@ -585,6 +629,7 @@ public void DecodeInPlaceInvalidBytesThrowsFormatException() // Don't test padding (byte 61 i.e. '='), which is tested in DecodeInPlaceInvalidBytesPadding // Don't test chars to be ignored (spaces: 9, 10, 13, 32 i.e. '\n', '\t', '\r', ' ') if (invalidBytes[i] == Base64TestHelper.EncodingPad || + invalidBytes[i] == Base64TestHelper.UrlEncodingPad || Base64TestHelper.IsByteToBeIgnored(invalidBytes[i])) { continue; @@ -635,7 +680,7 @@ public void DecodeInPlaceInvalidBytesPaddingThrowsFormatException() // The last byte or the last 2 bytes being the padding character is valid { Span buffer = new byte[] { 50, 50, 50, 50, 80, 80, 80, 80 }; - buffer[6] = Base64TestHelper.EncodingPad; + buffer[6] = Base64TestHelper.UrlEncodingPad; buffer[7] = Base64TestHelper.EncodingPad; // valid input - "2222PP==" string sourceString = Encoding.ASCII.GetString(buffer.ToArray()); int bytesWritten = Base64Url.DecodeFromUtf8InPlace(buffer); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs index 78a13298bf7072..bfabcc40d51b02 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncoderUnitTests.cs @@ -286,5 +286,57 @@ public void TryEncodeInPlaceOutputTooSmall() Assert.False(Base64Url.TryEncodeToUtf8InPlace(testBytes, testBytes.Length, out int bytesWritten)); Assert.Equal(0, bytesWritten); } + + [Fact] + public void TryEncodeToUtf8() + { + const int numberOfBytes = 15; + Span testBytes = new byte[numberOfBytes / 3 * 4]; // slack since encoding inflates the data + Base64TestHelper.InitializeBytes(testBytes); + + for (int numberOfBytesToTest = 0; numberOfBytesToTest <= numberOfBytes; numberOfBytesToTest++) + { + ReadOnlySpan source = testBytes.Slice(0, numberOfBytesToTest); + Span destination = new byte[Base64Url.GetEncodedLength(numberOfBytesToTest)]; + Assert.True(Base64Url.TryEncodeToUtf8(source, destination, out int bytesWritten)); + Assert.Equal(destination.Length, bytesWritten); + Assert.True(source.SequenceEqual(Base64Url.DecodeFromUtf8(destination).AsSpan())); + } + } + + [Theory] + [InlineData(1, "AQ")] + [InlineData(2, "AQI")] + [InlineData(3, "AQID")] + [InlineData(4, "AQIDBA")] + [InlineData(5, "AQIDBAU")] + [InlineData(6, "AQIDBAUG")] + [InlineData(7, "AQIDBAUGBw")] + public void TryEncodeToUtf8EncodeUpToDestinationSize(int numBytes, string expectedText) + { + int expectedWritten = expectedText.Length; + + Span source = new byte[numBytes]; + for (int i = 0; i < numBytes; i++) + { + source[i] = (byte)(i + 1); + } + Span destination = new byte[6]; + + if (numBytes < 5) + { + Assert.True(Base64Url.TryEncodeToUtf8(source, destination, out int bytesWritten)); + Assert.Equal(expectedWritten, bytesWritten); + string encodedText = Encoding.ASCII.GetString(destination.Slice(0, expectedWritten).ToArray()); + Assert.Equal(expectedText, encodedText); + } + else + { + Assert.False(Base64Url.TryEncodeToUtf8(source, destination, out int bytesWritten)); + Assert.Equal(4, bytesWritten); + string encodedText = Encoding.ASCII.GetString(destination.Slice(0, 4).ToArray()); + Assert.Equal(expectedText.Substring(0, 4), encodedText); + } + } } } diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs similarity index 97% rename from src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs rename to src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs index a2a0ffd197fb0e..279378778baa49 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlEncodingAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs @@ -8,7 +8,7 @@ namespace System.Buffers.Text.Tests { - public class Base64UrlEncodingAPIsUnitTests + public class Base64UrlUnicodeAPIsUnitTests { [Theory] [InlineData("", 0)] @@ -50,7 +50,7 @@ public static void DecodeEncodeToFromCharsStringRoundTrip(string str, int expect public void EncodingWithLargeSpan() { var rnd = new Random(42); - for (int i = 0; i < 10; i++) + for (int i = 0; i < 5; i++) { int numBytes = rnd.Next(100, 1000 * 1000); Span source = new byte[numBytes]; @@ -70,7 +70,7 @@ public void EncodingWithLargeSpan() public void DecodeWithLargeSpan() { var rnd = new Random(42); - for (int i = 0; i < 10; i++) + for (int i = 0; i < 5; i++) { int numBytes; do @@ -95,6 +95,27 @@ public void DecodeWithLargeSpan() } } + [Fact] + public void RoundTripWithLargeSpan() + { + var rnd = new Random(42); + for (int i = 0; i < 5; i++) + { + int numBytes = rnd.Next(100, 1000 * 1000); + Span source = new byte[numBytes]; + Base64TestHelper.InitializeBytes(source, numBytes); + + int expectedLength = Base64Url.GetEncodedLength(source.Length); + char[] encodedBytes = Base64Url.EncodeToChars(source); + Assert.Equal(expectedLength, encodedBytes.Length); + Assert.Equal(new String(encodedBytes), Base64Url.EncodeToString(source)); + + byte[] decoded = Base64Url.DecodeFromChars(encodedBytes); + Assert.Equal(source, decoded); + } + } + + public static IEnumerable EncodeToStringTests_TestData() { yield return new object[] { Enumerable.Range(0, 0).Select(i => (byte)i).ToArray(), "" }; @@ -379,7 +400,7 @@ private static void VerifyInvalidInput(string input) { char[] inputChars = input.ToCharArray(); - Assert.Throws(() => Base64Url.DecodeFromChars(input)); // or FormatException? + Assert.Throws(() => Base64Url.DecodeFromChars(input)); } private static void Verify(string input, Action action = null) diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index b240ad67413eca..0dfd33b13837db 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index ff24b38913b727..d6592f79e3ce8e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -104,7 +104,8 @@ internal interface IBase64Decoder where T : unmanaged static abstract uint AdvSimdLutTwo3Uint1 { get; } static abstract int SrcLength(bool isFinalBlock, int utf8Length); static abstract int GetMaxDecodedLength(int utf8Length); - static abstract bool IsInValidLength(int bufferLength); + static abstract bool IsInvalidLength(int bufferLength); + static abstract bool IsValidPadding(uint padChar); static abstract bool TryDecode128Core( Vector128 str, Vector128 hiNibbles, diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 65eb317e4f8d2b..76157c51bbb93c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -180,7 +180,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa byte* destMax = destBytes + (uint)destLength; - if (t3 != EncodingPad) + if (!TBase64Decoder.IsValidPadding(t3)) { int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); int i3 = Unsafe.Add(ref decodingMap, (IntPtr)t3); @@ -203,7 +203,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa dest += 3; src += 4; } - else if (t2 != EncodingPad) + else if (!TBase64Decoder.IsValidPadding(t2)) { int i2 = Unsafe.Add(ref decodingMap, (IntPtr)t2); @@ -375,7 +375,7 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa uint sourceIndex = 0; uint destIndex = 0; - if (TBase64Decoder.IsInValidLength(buffer.Length)) + if (TBase64Decoder.IsInvalidLength(buffer.Length)) { goto InvalidExit; } @@ -435,7 +435,7 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa i0 |= i1; - if (t3 != EncodingPad) + if (!TBase64Decoder.IsValidPadding(t3)) { int i2 = Unsafe.Add(ref decodingMap, t2); int i3 = Unsafe.Add(ref decodingMap, t3); @@ -453,7 +453,7 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa WriteThreeLowOrderBytes(bufferBytes + destIndex, i0); destIndex += 3; } - else if (t2 != EncodingPad) + else if (!TBase64Decoder.IsValidPadding(t2)) { int i2 = Unsafe.Add(ref decodingMap, t2); @@ -532,7 +532,7 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re // If this block contains padding and there's another block, then only whitespace may follow for being valid. if (hasAnotherBlock) { - int paddingCount = GetPaddingCount(ref buffer[^1]); + int paddingCount = GetPaddingCount(ref buffer[^1]); if (paddingCount > 0) { hasAnotherBlock = false; @@ -581,12 +581,13 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPaddingCount(ref byte ptrToLastElement) + private static int GetPaddingCount(ref byte ptrToLastElement) + where TBase64Decoder : IBase64Decoder { int padding = 0; - if (ptrToLastElement == EncodingPad) padding++; - if (Unsafe.Subtract(ref ptrToLastElement, 1) == EncodingPad) padding++; + if (TBase64Decoder.IsValidPadding(ptrToLastElement)) padding++; + if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) padding++; return padding; } @@ -1302,7 +1303,9 @@ internal static bool IsWhiteSpace(int value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); - public static bool IsInValidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 + public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 + + public static bool IsValidPadding(uint padChar) => padChar == EncodingPad; public static int SrcLength(bool _, int utf8Length) => utf8Length & ~0x3; // only decode input up to the closest multiple of 4. diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index c1b25371125edb..16920f0ed2788e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -16,6 +16,8 @@ namespace System.Buffers.Text public static partial class Base64Url { + private const int MaxStackallocThreshold = 256; + /// /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "base64Length". /// @@ -148,10 +150,10 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti public static byte[] DecodeFromUtf8(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); - Span destination = stackalloc byte[256]; + Span destination = stackalloc byte[MaxStackallocThreshold]; byte[]? rented = null; - if (upperBound <= destination.Length) + if (upperBound <= MaxStackallocThreshold) { destination = destination.Slice(0, upperBound); } @@ -242,7 +244,7 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(Rea // If this block contains padding and there's another block, then only whitespace may follow for being valid. if (hasAnotherBlock) { - int paddingCount = GetPaddingCount(ref buffer[^1]); + int paddingCount = GetPaddingCount(ref buffer[^1]); if (paddingCount > 0) { hasAnotherBlock = false; @@ -291,12 +293,13 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(Rea } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPaddingCount(ref ushort ptrToLastElement) + private static int GetPaddingCount(ref ushort ptrToLastElement) + where TBase64Decoder : IBase64Decoder { int padding = 0; - if (ptrToLastElement == EncodingPad) padding++; - if (Unsafe.Subtract(ref ptrToLastElement, 1) == EncodingPad) padding++; + if (TBase64Decoder.IsValidPadding(ptrToLastElement)) padding++; + if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) padding++; return padding; } @@ -351,10 +354,10 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest public static byte[] DecodeFromChars(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); - Span destination = stackalloc byte[256]; + Span destination = stackalloc byte[MaxStackallocThreshold]; byte[]? rented = null; - if (upperBound <= destination.Length) + if (upperBound <= MaxStackallocThreshold) { destination = destination.Slice(0, upperBound); } @@ -464,7 +467,9 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); - public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + + public static bool IsValidPadding(uint padChar) => padChar == EncodingPad || padChar == UrlEncodingPad; public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; @@ -759,7 +764,9 @@ public static unsafe bool TryLoadArmVector128x4(byte* src, byte* srcStart, int s public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); - public static bool IsInValidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + + public static bool IsValidPadding(uint padChar) => padChar == EncodingPad || padChar == UrlEncodingPad; public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 0f5f7de1d2b9ce..e92bcdad5c258c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -168,7 +168,7 @@ public static string EncodeToString(ReadOnlySpan source) { Span destination = new char[GetEncodedLength(source.Length)]; EncodeToChars(source, destination, out int bytesWritten, out _); - Debug.Assert(destination.Length == bytesWritten); + Debug.Assert(destination.Length == bytesWritten, $"The source length: {source.Length}, bytes written: {bytesWritten}"); return new string(destination); } @@ -379,8 +379,8 @@ public static unsafe void EncodeThreeAndWrite(byte* threeBytes, ushort* destinat public static int IncrementPadOne => 3; public static int GetMaxSrcLength(int srcLength, int destLength) => - srcLength <= MaximumEncodeLength && destLength >= Base64Url.GetEncodedLength(srcLength) ? - srcLength : (destLength >> 2) * 3 + destLength % 4; + srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? + srcLength : GetMaxDecodedLength(destLength); //(destLength >> 2) * 3 + destLength % 4; public static uint GetInPlaceDestinationLength(int encodedLength, int leftOver) => leftOver > 0 ? (uint)(encodedLength - leftOver - 1) : (uint)(encodedLength - 4); diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 0d9b2b8d3839f2..36f898ba54b07d 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7536,9 +7536,9 @@ public static class Base64Url public static int DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } public static System.Buffers.OperationStatus DecodeFromUtf8(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) { throw null; } public static int DecodeFromUtf8InPlace(System.Span buffer) { throw null; } - public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } - public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } public static char[] EncodeToChars(System.ReadOnlySpan source) { throw null; } + public static int EncodeToChars(System.ReadOnlySpan source, System.Span destination) { throw null; } + public static System.Buffers.OperationStatus EncodeToChars(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) { throw null; } public static string EncodeToString(System.ReadOnlySpan source) { throw null; } public static byte[] EncodeToUtf8(System.ReadOnlySpan source) { throw null; } public static int EncodeToUtf8(System.ReadOnlySpan source, System.Span destination) { throw null; } From c2ff2cbf5b3aac4a44da0b5cd7fe6e19f7ac2258 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 20 May 2024 16:59:08 -0700 Subject: [PATCH 15/38] Fix assertion failure, apply some feedback, try fix ARM failure --- .../Text/Base64Url/Base64UrlDecoder.cs | 44 ++++++------------- .../Text/Base64Url/Base64UrlEncoder.cs | 22 +++++----- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 16920f0ed2788e..d01c6c76532c28 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -150,18 +150,11 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti public static byte[] DecodeFromUtf8(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); - Span destination = stackalloc byte[MaxStackallocThreshold]; byte[]? rented = null; - if (upperBound <= MaxStackallocThreshold) - { - destination = destination.Slice(0, upperBound); - } - else - { - rented = ArrayPool.Shared.Rent(upperBound); - destination = rented.AsSpan(0, upperBound); - } + Span destination = upperBound <= MaxStackallocThreshold + ? stackalloc byte[MaxStackallocThreshold] + : (rented = ArrayPool.Shared.Rent(upperBound)); OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); byte[] ret = destination.Slice(0, bytesWritten).ToArray(); @@ -171,8 +164,7 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) ArrayPool.Shared.Return(rented); } - return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : - throw new FormatException(SR.Format_BadBase64Char); + return OperationStatus.Done == status ? ret : throw new FormatException(SR.Format_BadBase64Char); } /// @@ -354,18 +346,11 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest public static byte[] DecodeFromChars(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); - Span destination = stackalloc byte[MaxStackallocThreshold]; byte[]? rented = null; - if (upperBound <= MaxStackallocThreshold) - { - destination = destination.Slice(0, upperBound); - } - else - { - rented = ArrayPool.Shared.Rent(upperBound); - destination = rented.AsSpan(0, upperBound); - } + Span destination = upperBound <= MaxStackallocThreshold + ? stackalloc byte[MaxStackallocThreshold] + : (rented = ArrayPool.Shared.Rent(upperBound)); OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); byte[] ret = destination.Slice(0, bytesWritten).ToArray(); @@ -375,8 +360,7 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) ArrayPool.Shared.Return(rented); } - return OperationStatus.Done == status ? destination.Slice(0, bytesWritten).ToArray() : - throw new FormatException(SR.Format_BadBase64Char); + return OperationStatus.Done == status ? ret : throw new FormatException(SR.Format_BadBase64Char); } private readonly struct Base64UrlDecoderByte : IBase64Decoder @@ -993,8 +977,8 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) { AssertRead>(src, srcStart, sourceLength); - var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); - var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); + var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4(src); + var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4(src + 32); if (VectorContainsNonAsciiChar(s11) || VectorContainsNonAsciiChar(s12) || VectorContainsNonAsciiChar(s21) || VectorContainsNonAsciiChar(s22) || @@ -1005,10 +989,10 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i return false; } - str1 = AdvSimd.Arm64.UnzipEven(s11.AsByte(), s12.AsByte()); - str2 = AdvSimd.Arm64.UnzipEven(s21.AsByte(), s22.AsByte()); - str3 = AdvSimd.Arm64.UnzipEven(s31.AsByte(), s32.AsByte()); - str4 = AdvSimd.Arm64.UnzipEven(s41.AsByte(), s42.AsByte()); + str1 = Vector128.Narrow(s11, s12); + str2 = Vector128.Narrow(s21, s22); + str3 = Vector128.Narrow(s31, s32); + str4 = Vector128.Narrow(s41, s42); return true; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index e92bcdad5c258c..11c245d92f4de8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -88,11 +88,11 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination /// The output will not be padded even if the input is not a multiple of 3. public static byte[] EncodeToUtf8(ReadOnlySpan source) { - Span destination = new byte[GetEncodedLength(source.Length)]; + byte[] destination = new byte[GetEncodedLength(source.Length)]; EncodeToUtf8(source, destination, out _, out int bytesWritten); Debug.Assert(destination.Length == bytesWritten); - return destination.Slice(0, bytesWritten).ToArray(); + return destination; } /// @@ -151,11 +151,11 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio /// The output will not be padded even if the input is not a multiple of 3. public static char[] EncodeToChars(ReadOnlySpan source) { - Span destination = new char[GetEncodedLength(source.Length)]; - EncodeToChars(source, destination, out int bytesWritten, out _); - Debug.Assert(destination.Length == bytesWritten); + char[] destination = new char[GetEncodedLength(source.Length)]; + EncodeToChars(source, destination, out _, out int charsWritten); + Debug.Assert(destination.Length == charsWritten); - return destination.ToArray(); + return destination; } /// @@ -166,9 +166,9 @@ public static char[] EncodeToChars(ReadOnlySpan source) /// The output will not be padded even if the input is not a multiple of 3. public static string EncodeToString(ReadOnlySpan source) { - Span destination = new char[GetEncodedLength(source.Length)]; - EncodeToChars(source, destination, out int bytesWritten, out _); - Debug.Assert(destination.Length == bytesWritten, $"The source length: {source.Length}, bytes written: {bytesWritten}"); + char[] destination = new char[GetEncodedLength(source.Length)]; + EncodeToChars(source, destination, out _, out int charsWritten); + Debug.Assert(destination.Length == charsWritten, $"The source length: {source.Length}, bytes written: {charsWritten}"); return new string(destination); } @@ -237,7 +237,7 @@ public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLeng public static int GetMaxSrcLength(int srcLength, int destLength) => srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? - srcLength : (destLength >> 2) * 3 + destLength % 4; + srcLength : GetMaxDecodedLength(destLength); public static uint GetInPlaceDestinationLength(int encodedLength, int _) => 0; // not used for char encoding @@ -380,7 +380,7 @@ public static unsafe void EncodeThreeAndWrite(byte* threeBytes, ushort* destinat public static int GetMaxSrcLength(int srcLength, int destLength) => srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? - srcLength : GetMaxDecodedLength(destLength); //(destLength >> 2) * 3 + destLength % 4; + srcLength : GetMaxDecodedLength(destLength); public static uint GetInPlaceDestinationLength(int encodedLength, int leftOver) => leftOver > 0 ? (uint)(encodedLength - leftOver - 1) : (uint)(encodedLength - 4); From b1d4f76322adfc054589378558a47d02deb83940 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 21 May 2024 08:40:12 -0700 Subject: [PATCH 16/38] Update docs, small clean ups --- .../tests/Base64/Base64TestHelper.cs | 2 +- .../Base64UrlUnicodeAPIsUnitTests.cs | 2 +- .../Base64Url/Base64UrlValidationUnitTests.cs | 2 +- .../src/System/Buffers/Text/Base64Decoder.cs | 1 - .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Text/Base64Url/Base64UrlDecoder.cs | 25 +++++++++--------- .../Text/Base64Url/Base64UrlEncoder.cs | 25 +++++++++--------- .../Text/Base64Url/Base64UrlValidator.cs | 26 +++++++++---------- 8 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs index d671d2c5e3cdef..42dca70bd66605 100644 --- a/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs +++ b/src/libraries/System.Memory/tests/Base64/Base64TestHelper.cs @@ -121,7 +121,7 @@ internal static void InitializeDecodableBytes(Span bytes, int seed = 100) } } - internal static void InitializeUrlDecodableBytes(Span bytes, int seed = 100) + internal static void InitializeUrlDecodableChars(Span bytes, int seed = 100) { var rnd = new Random(seed); for (int i = 0; i < bytes.Length; i++) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs index 279378778baa49..b9f16e5d0d97ba 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs @@ -79,7 +79,7 @@ public void DecodeWithLargeSpan() } while (numBytes % 4 == 1); // ensure we have a valid length Span source = new char[numBytes]; - Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + Base64TestHelper.InitializeUrlDecodableChars(source, numBytes); Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; Assert.Equal(OperationStatus.Done, Base64Url.DecodeFromChars(source, decodedBytes, out int consumed, out int decodedByteCount)); diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs index af9782be773394..a415d0098cfe3a 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -89,7 +89,7 @@ public void BasicValidationInvalidInputLengthChars() } while (numBytes % 4 != 1); // ensure we have a invalid length Span source = new char[numBytes]; - Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); + Base64TestHelper.InitializeUrlDecodableChars(source, numBytes); Assert.False(Base64Url.IsValid(source)); Assert.False(Base64Url.IsValid(source, out int decodedLength)); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 76157c51bbb93c..47f25a7c5b6c89 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -7,7 +7,6 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; -using static System.Buffers.Text.Base64; namespace System.Buffers.Text { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index f2604824df815a..20938b9e7d17e4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -28,7 +28,7 @@ public static partial class Base64 /// Set to when the source buffer contains the entirety of the data to encode. /// Set to if this method is being called in a loop and if more input data may follow. /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the OperationStatus enum values: + /// It returns the enum values: /// - Done - on successful processing of the entire input span /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input /// - NeedMoreData - only if is , otherwise the output is padded if the input is not a multiple of 3 diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index d01c6c76532c28..00a27642e6b550 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -19,7 +19,7 @@ public static partial class Base64Url private const int MaxStackallocThreshold = 256; /// - /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size "base64Length". + /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size . /// /// /// Thrown when the specified is less than 0. @@ -70,7 +70,7 @@ public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span /// The input span which contains the base 64 text data that needs to be decoded. /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. - /// contains a invalid Base64Url character, + /// contains an invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. /// /// As padding is optional the input length not required to be a multiple of 4. @@ -100,7 +100,7 @@ public static int DecodeFromUtf8InPlace(Span buffer) /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. /// Thrown when the encoded output cannot fit in the provided. - /// contains a invalid Base64Url character, + /// contains an invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. /// /// As padding is optional the input length not required to be a multiple of 4. @@ -145,7 +145,7 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// >A byte array which contains the result of the decoding operation. - /// contains a invalid Base64Url character, + /// contains an invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. public static byte[] DecodeFromUtf8(ReadOnlySpan source) { @@ -168,9 +168,9 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) } /// - /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. /// - /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The input span which contains unicode ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of input chars consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. @@ -297,9 +297,9 @@ private static int GetPaddingCount(ref ushort ptrToLastElement) } /// - /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. /// - /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// Thrown when the encoded output cannot fit in the provided. @@ -319,13 +319,14 @@ public static int DecodeFromChars(ReadOnlySpan source, Span destinat throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } + Debug.Assert(OperationStatus.InvalidData == status); throw new FormatException(SR.Format_BadBase64Char); } /// - /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. /// - /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// if bytes decoded successfully, otherwise . @@ -337,9 +338,9 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest } /// - /// Decode the span of UTF-8 encoded chars represented as Base64Url into binary data. + /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. /// - /// The input span which contains UTF-8 encoded chars in Base64Url that needs to be decoded. + /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// A byte array which contains the result of the decoding operation. /// contains a invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 11c245d92f4de8..39dfd4d11b6aaa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -25,7 +25,7 @@ public static partial class Base64Url /// Set to when the source buffer contains the entirety of the data to encode. /// Set to if this method is being called in a loop and if more input data may follow. /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the OperationStatus enum values: + /// It returns the enum values: /// - Done - on successful processing of the entire input span /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input /// - NeedMoreData - only if is @@ -37,7 +37,7 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, EncodeTo(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); /// - /// Returns the length (in bytes) of the result if you were to encode binary data within a byte span of size "length". + /// Returns the length (in bytes) of the result if you were to encode binary data within a byte span of size . /// /// /// Thrown when the specified is less than 0 or larger than 1610612733 (since encode inflates the data by 4/3). @@ -96,10 +96,10 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) } /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. - /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// (default) when the input span contains the entire data to encode. @@ -118,10 +118,10 @@ public static OperationStatus EncodeToChars(ReadOnlySpan source, Span(source, MemoryMarshal.Cast(destination), out bytesConsumed, out charsWritten, isFinalBlock); /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. - /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. /// Thrown when the encoded output cannot fit in the provided. /// The output will not be padded even if the input is not a multiple of 3. @@ -144,10 +144,10 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio } /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. - /// A char array which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// A char array which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The output will not be padded even if the input is not a multiple of 3. public static char[] EncodeToChars(ReadOnlySpan source) { @@ -159,10 +159,10 @@ public static char[] EncodeToChars(ReadOnlySpan source) } /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encode the span of binary data into unicode string represented as Base64Url ASCII chars. /// /// The input span which contains binary data that needs to be encoded. - /// A string which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// A string which contains the result of the operation, i.e. the ASCII string in Base64Url. /// The output will not be padded even if the input is not a multiple of 3. public static string EncodeToString(ReadOnlySpan source) { @@ -174,10 +174,10 @@ public static string EncodeToString(ReadOnlySpan source) } /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. - /// The output span which contains the result of the operation, i.e. the UTF-8 encoded chars in Base64Url. + /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// if chars encoded successfully, otherwise . /// The output will not be padded even if the input is not a multiple of 3. @@ -213,6 +213,7 @@ public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destina /// (and needs to be smaller than the buffer length). /// The number of bytes written into the buffer. /// if bytes encoded successfully, otherwise . + /// The output will not be padded even if the input is not a multiple of 3. public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) { OperationStatus status = EncodeToUtf8InPlace(buffer, dataLength, out bytesWritten); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 57596397ce9983..e824a43fa33fd1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -8,11 +8,11 @@ public static partial class Base64Url /// Validates that the specified span of text is comprised of valid base-64 encoded data. /// A span of text to validate. /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . - /// TODO : Update remarks - /// If the method returns , the same text passed to and - /// would successfully decode (in the case - /// of assuming sufficient output space). Any amount of whitespace is allowed anywhere in the input, - /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. + /// + /// If the method returns , the same text passed to and + /// would successfully decode (in the case + /// of assuming sufficient output space). + /// Any amount of whitespace is allowed anywhere in the input, where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. /// public static bool IsValid(ReadOnlySpan base64UrlText) => Base64.IsValid(base64UrlText, out _); @@ -21,11 +21,11 @@ public static bool IsValid(ReadOnlySpan base64UrlText) => /// A span of text to validate. /// If the method returns true, the number of decoded bytes that will result from decoding the input text. /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . - /// TODO : Update remarks - /// If the method returns , the same text passed to and - /// would successfully decode (in the case - /// of assuming sufficient output space). Any amount of whitespace is allowed anywhere in the input, - /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. + /// + /// If the method returns , the same text passed to and + /// would successfully decode (in the case + /// of assuming sufficient output space). + /// Any amount of whitespace is allowed anywhere in the input, where whitespace is defined as the characters ' ', '\t', '\r', or '\n'. /// public static bool IsValid(ReadOnlySpan base64UrlText, out int decodedLength) => Base64.IsValid(base64UrlText, out decodedLength); @@ -33,7 +33,7 @@ public static bool IsValid(ReadOnlySpan base64UrlText, out int decodedLeng /// Validates that the specified span of UTF-8 text is comprised of valid base-64 encoded data. /// A span of UTF-8 text to validate. /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . - /// TODO : Update remarks + /// /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n' (as bytes). /// public static bool IsValid(ReadOnlySpan utf8Base64UrlText) => @@ -43,7 +43,7 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText) => /// A span of UTF-8 text to validate. /// If the method returns true, the number of decoded bytes that will result from decoding the input UTF-8 text. /// if contains a valid, decodable sequence of base-64 encoded data; otherwise, . - /// TODO : Update remarks + /// /// where whitespace is defined as the characters ' ', '\t', '\r', or '\n' (as bytes). /// public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decodedLength) => @@ -64,7 +64,7 @@ private static bool ValidateAndDecodeLength(int length, int paddingCount, out in return true; } - private const uint UrlEncodingPad = '%'; // url padding + private const uint UrlEncodingPad = '%'; // allowed for url padding private readonly struct Base64UrlCharValidatable : Base64.IBase64Validatable { From 0f6fe6ae0343a6078a872765800261ef42cfa4bb Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 21 May 2024 12:32:36 -0700 Subject: [PATCH 17/38] Try fix ARM failure --- .../System/Buffers/Text/Base64Url/Base64UrlDecoder.cs | 6 +++--- .../System/Buffers/Text/Base64Url/Base64UrlEncoder.cs | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 00a27642e6b550..90e71ccacfd324 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -978,8 +978,8 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) { AssertRead>(src, srcStart, sourceLength); - var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4(src); - var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4(src + 32); + var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); + var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); if (VectorContainsNonAsciiChar(s11) || VectorContainsNonAsciiChar(s12) || VectorContainsNonAsciiChar(s21) || VectorContainsNonAsciiChar(s22) || @@ -1047,7 +1047,7 @@ private static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) } else if (AdvSimd.Arm64.IsSupported) { - // First we pick four chars, a larger one from all four pairs of adjecent chars in the vector. + // First we pick four chars, a larger one from all four pairs of adjacent chars in the vector. // If any of those four chars has a non-ASCII bit set, we have seen non-ASCII data. Vector128 maxChars = AdvSimd.Arm64.MaxPairwise(utf16Vector, utf16Vector); return (maxChars.AsUInt64().ToScalar() & 0xFF80FF80FF80FF80) != 0; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 39dfd4d11b6aaa..8b202a4929d56f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -325,11 +325,12 @@ public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, in Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) { AssertWrite>(dest, destStart, destLength); - Vector128 vecWide1 = AdvSimd.Arm64.ZipLow(res1, Vector128.Zero).AsUInt16(); - Vector128 vecWide2 = AdvSimd.Arm64.ZipLow(res2, Vector128.Zero).AsUInt16(); - Vector128 vecWide3 = AdvSimd.Arm64.ZipLow(res3, Vector128.Zero).AsUInt16(); - Vector128 vecWide4 = AdvSimd.Arm64.ZipLow(res4, Vector128.Zero).AsUInt16(); - AdvSimd.Arm64.StoreVector128x4(dest, (vecWide1, vecWide2, vecWide3, vecWide4)); + (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); + (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); + AdvSimd.Arm64.StoreVector128x4AndZip(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); + (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); + (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); + AdvSimd.Arm64.StoreVector128x4AndZip(dest + 32, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 344bbb623ec9e013ace89ddd7dbd9c3ccc173a35 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 22 May 2024 09:30:58 -0700 Subject: [PATCH 18/38] Update src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../Buffers/Text/Base64Url/Base64UrlEncoder.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 8b202a4929d56f..d617563edb935f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -164,13 +164,18 @@ public static char[] EncodeToChars(ReadOnlySpan source) /// The input span which contains binary data that needs to be encoded. /// A string which contains the result of the operation, i.e. the ASCII string in Base64Url. /// The output will not be padded even if the input is not a multiple of 3. - public static string EncodeToString(ReadOnlySpan source) + public static unsafe string EncodeToString(ReadOnlySpan source) { - char[] destination = new char[GetEncodedLength(source.Length)]; - EncodeToChars(source, destination, out _, out int charsWritten); - Debug.Assert(destination.Length == charsWritten, $"The source length: {source.Length}, bytes written: {charsWritten}"); + int encodedLength = GetEncodedLength(source.Length); - return new string(destination); +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + return string.Create(encodedLength, (IntPtr)(&source), static (buffer, spanPtr) => + { + ReadOnlySpan source = *(ReadOnlySpan*)spanPtr; + EncodeToChars(source, buffer, out _, out int charsWritten); + Debug.Assert(buffer.Length == charsWritten, $"The source length: {source.Length}, bytes written: {charsWritten}"); + }); +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type } /// From b657a9981b67e6f1dfc428a91c6d86e481d6705c Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 22 May 2024 10:44:48 -0700 Subject: [PATCH 19/38] Rename StoreToDetionation overloads, reuse some duplicate code --- .../src/System/Buffers/Text/Base64.cs | 8 +- .../src/System/Buffers/Text/Base64Decoder.cs | 8 +- .../src/System/Buffers/Text/Base64Encoder.cs | 18 +- .../Text/Base64Url/Base64UrlDecoder.cs | 301 +++--------------- .../Text/Base64Url/Base64UrlEncoder.cs | 190 +++++------ 5 files changed, 135 insertions(+), 390 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index d6592f79e3ce8e..2e5939bc74078b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -81,10 +81,10 @@ internal interface IBase64Encoder where T : unmanaged static abstract unsafe void EncodeThreeAndWrite(byte* threeBytes, T* destination, ref byte encodingMap); static abstract int IncrementPadTwo { get; } static abstract int IncrementPadOne { get; } - static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector512 str); - static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector256 str); - static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector128 str); - static abstract unsafe void StoreToDestination(T* dest, T* destStart, int destLength, Vector128 res1, + static abstract unsafe void StoreVector512ToDestination(T* dest, T* destStart, int destLength, Vector512 str); + static abstract unsafe void StoreVector256ToDestination(T* dest, T* destStart, int destLength, Vector256 str); + static abstract unsafe void StoreVector128ToDestination(T* dest, T* destStart, int destLength, Vector128 str); + static abstract unsafe void StoreArmVector128x4ToDestination(T* dest, T* destStart, int destLength, Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 47f25a7c5b6c89..39312f90b6c6ab 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -1212,7 +1212,7 @@ internal static bool IsWhiteSpace(int value) return value == 32; } - private readonly struct Base64DecoderByte : IBase64Decoder + internal readonly struct Base64DecoderByte : IBase64Decoder { // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) public static ReadOnlySpan DecodingMap => @@ -1452,10 +1452,8 @@ public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) - where TBase64Decoder : IBase64Decoder - { - return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); - } + where TBase64Decoder : IBase64Decoder => + DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool TryLoadVector512(byte* src, byte* srcStart, int sourceLength, out Vector512 str) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index 20938b9e7d17e4..27873fb67f32f7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -296,7 +296,7 @@ private static unsafe void Avx512Encode(ref byte* srcBytes, r // Step 2: Now we have the indices calculated. Next step is to use these indices to translate. str = Avx512Vbmi.PermuteVar64x8(vbmiLookup, str); - TBase64Encoder.StoreToDestination(dest, destStart, destLength, str.AsByte()); + TBase64Encoder.StoreVector512ToDestination(dest, destStart, destLength, str.AsByte()); src += 48; dest += 64; @@ -467,7 +467,7 @@ private static unsafe void Avx2Encode(ref byte* srcBytes, ref // Add offsets to input values: str = Avx2.Add(str, Avx2.Shuffle(lut, tmp)); - TBase64Encoder.StoreToDestination(dest, destStart, destLength, str.AsByte()); + TBase64Encoder.StoreVector256ToDestination(dest, destStart, destLength, str.AsByte()); src += 24; dest += 32; @@ -533,7 +533,7 @@ private static unsafe void AdvSimdEncode(ref byte* srcBytes, res4 = AdvSimd.Arm64.VectorTableLookup((tblEnc1, tblEnc2, tblEnc3, tblEnc4), res4); // Interleave and store result: - TBase64Encoder.StoreToDestination(dest, destStart, destLength, res1, res2, res3, res4); + TBase64Encoder.StoreArmVector128x4ToDestination(dest, destStart, destLength, res1, res2, res3, res4); src += 48; dest += 64; @@ -657,7 +657,7 @@ private static unsafe void Vector128Encode(ref byte* srcBytes // Add offsets to input values: str += SimdShuffle(lut, tmp.AsByte(), mask8F); - TBase64Encoder.StoreToDestination(dest, destStart, destLength, str); + TBase64Encoder.StoreVector128ToDestination(dest, destStart, destLength, str); src += 12; dest += 16; @@ -696,7 +696,7 @@ private static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) internal const int MaximumEncodeLength = (int.MaxValue / 4) * 3; // 1610612733 - private readonly struct Base64EncoderByte : IBase64Encoder + internal readonly struct Base64EncoderByte : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"u8; @@ -775,7 +775,7 @@ public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) + public static unsafe void StoreVector512ToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) { AssertWrite>(dest, destStart, destLength); str.Store(dest); @@ -783,14 +783,14 @@ public static unsafe void StoreToDestination(byte* dest, byte* destStart, int de [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) + public static unsafe void StoreVector256ToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) { AssertWrite>(dest, destStart, destLength); Avx.Store(dest, str.AsByte()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) + public static unsafe void StoreVector128ToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) { AssertWrite>(dest, destStart, destLength); str.Store(dest); @@ -798,7 +798,7 @@ public static unsafe void StoreToDestination(byte* dest, byte* destStart, int de [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, + public static unsafe void StoreArmVector128x4ToDestination(byte* dest, byte* destStart, int destLength, Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) { AssertWrite>(dest, destStart, destLength); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 90e71ccacfd324..e1997e670ad371 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -531,301 +531,87 @@ public static bool TryDecode256Core( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) - { - uint t0 = encodedBytes[0]; - uint t1 = encodedBytes[1]; - uint t2 = encodedBytes[2]; - uint t3 = encodedBytes[3]; - - if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) - { - return -1; // One or more chars falls outside the 00..ff range, invalid Base64Url character. - } - - int i0 = Unsafe.Add(ref decodingMap, t0); - int i1 = Unsafe.Add(ref decodingMap, t1); - int i2 = Unsafe.Add(ref decodingMap, t2); - int i3 = Unsafe.Add(ref decodingMap, t3); - - i0 <<= 18; - i1 <<= 12; - i2 <<= 6; - - i0 |= i3; - i1 |= i2; - - i0 |= i1; - return i0; - } + public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) => + Base64DecoderByte.Decode(encodedBytes, ref decodingMap); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe int DecodeRemaining(byte* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3) - { - uint t0; - uint t1; - t2 = EncodingPad; - t3 = EncodingPad; - switch (remaining) - { - case 2: - t0 = srcEnd[-2]; - t1 = srcEnd[-1]; - break; - case 3: - t0 = srcEnd[-3]; - t1 = srcEnd[-2]; - t2 = srcEnd[-1]; - break; - case 4: - t0 = srcEnd[-4]; - t1 = srcEnd[-3]; - t2 = srcEnd[-2]; - t3 = srcEnd[-1]; - break; - default: - return -1; - } - - if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) - { - return -1; - } - - int i0 = Unsafe.Add(ref decodingMap, (IntPtr)t0); - int i1 = Unsafe.Add(ref decodingMap, (IntPtr)t1); - - i0 <<= 18; - i1 <<= 12; - - i0 |= i1; - return i0; - } + => Base64DecoderByte.DecodeRemaining(srcEnd, ref decodingMap, remaining, out t2, out t3); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) - { - for (int i = 0; i < span.Length; i++) - { - if (!IsWhiteSpace(span[i])) - { - return i; - } - } - - return -1; - } + public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) => Base64DecoderByte.IndexOfAnyExceptWhiteSpace(span); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, - ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) - where TBase64Decoder : IBase64Decoder - { - return Base64.DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); - } + ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder => + Base64.DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe bool TryLoadVector512(byte* src, byte* srcStart, int sourceLength, out Vector512 str) - { - Base64.AssertRead>(src, srcStart, sourceLength); - str = Vector512.Load(src).AsSByte(); - return true; - } + public static unsafe bool TryLoadVector512(byte* src, byte* srcStart, int sourceLength, out Vector512 str) => + Base64DecoderByte.TryLoadVector512(src, srcStart, sourceLength, out str); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - public static unsafe bool TryLoadAvxVector256(byte* src, byte* srcStart, int sourceLength, out Vector256 str) - { - Base64.AssertRead>(src, srcStart, sourceLength); - str = Avx.LoadVector256(src).AsSByte(); - return true; - } + public static unsafe bool TryLoadAvxVector256(byte* src, byte* srcStart, int sourceLength, out Vector256 str) => + Base64DecoderByte.TryLoadAvxVector256(src, srcStart, sourceLength, out str); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int sourceLength, out Vector128 str) - { - Base64.AssertRead>(src, srcStart, sourceLength); - str = Vector128.LoadUnsafe(ref *src); - return true; - } + public static unsafe bool TryLoadVector128(byte* src, byte* srcStart, int sourceLength, out Vector128 str) => + Base64DecoderByte.TryLoadVector128(src, srcStart, sourceLength, out str); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] public static unsafe bool TryLoadArmVector128x4(byte* src, byte* srcStart, int sourceLength, - out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) - { - AssertRead>(src, srcStart, sourceLength); - (str1, str2, str3, str4) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); - - return true; - } + out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) => + Base64DecoderByte.TryLoadArmVector128x4(src, srcStart, sourceLength, out str1, out str2, out str3, out str4); } private readonly struct Base64UrlDecoderChar : IBase64Decoder { - public static ReadOnlySpan DecodingMap => - [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, //62 is placed at index 45 (for -), 63 at index 95 (for _) - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, //52-61 are placed at index 48-57 (for 0-9) - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, //0-25 are placed at index 65-90 (for A-Z) - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, //26-51 are placed at index 97-122 (for a-z) - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ]; + public static ReadOnlySpan DecodingMap => Base64UrlDecoderByte.DecodingMap; - public static ReadOnlySpan VbmiLookup0 => - [ - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80808080, - 0x80808080, 0x80808080, 0x80808080, 0x80803e80, - 0x37363534, 0x3b3a3938, 0x80803d3c, 0x80808080 - ]; + public static ReadOnlySpan VbmiLookup0 => Base64UrlDecoderByte.VbmiLookup0; - public static ReadOnlySpan VbmiLookup1 => - [ - 0x02010080, 0x06050403, 0x0a090807, 0x0e0d0c0b, - 0x1211100f, 0x16151413, 0x80191817, 0x3f808080, - 0x1c1b1a80, 0x201f1e1d, 0x24232221, 0x28272625, - 0x2c2b2a29, 0x302f2e2d, 0x80333231, 0x80808080 - ]; + public static ReadOnlySpan VbmiLookup1 => Base64UrlDecoderByte.VbmiLookup1; - public static ReadOnlySpan Avx2LutHigh => - [ - 0x00, 0x00, 0x2d, 0x39, - 0x4f, 0x5a, 0x6f, 0x7a, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x2d, 0x39, - 0x4f, 0x5a, 0x6f, 0x7a, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - ]; + public static ReadOnlySpan Avx2LutHigh => Base64UrlDecoderByte.Avx2LutHigh; - public static ReadOnlySpan Avx2LutLow => - [ - 0x01, 0x01, 0x2d, 0x30, - 0x41, 0x50, 0x61, 0x70, - 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x2d, 0x30, - 0x41, 0x50, 0x61, 0x70, - 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x01 - ]; + public static ReadOnlySpan Avx2LutLow => Base64UrlDecoderByte.Avx2LutLow; - public static ReadOnlySpan Avx2LutShift => - [ - 0, 0, 17, 4, - -65, -65, -71, -71, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 17, 4, - -65, -65, -71, -71, - 0, 0, 0, 0, - 0, 0, 0, 0 - ]; + public static ReadOnlySpan Avx2LutShift => Base64UrlDecoderByte.Avx2LutShift; - public static byte MaskSlashOrUnderscore => (byte)'_'; // underscore + public static byte MaskSlashOrUnderscore => Base64UrlDecoderByte.MaskSlashOrUnderscore; - public static ReadOnlySpan Vector128LutHigh => [0x392d0000, 0x7a6f5a4f, 0x00000000, 0x00000000]; + public static ReadOnlySpan Vector128LutHigh => Base64UrlDecoderByte.Vector128LutHigh; - public static ReadOnlySpan Vector128LutLow => [0x302d0101, 0x70615041, 0x01010101, 0x01010101]; + public static ReadOnlySpan Vector128LutLow => Base64UrlDecoderByte.Vector128LutLow; - public static ReadOnlySpan Vector128LutShift => [0x04110000, 0xb9b9bfbf, 0x00000000, 0x00000000]; + public static ReadOnlySpan Vector128LutShift => Base64UrlDecoderByte.Vector128LutShift; - public static ReadOnlySpan AdvSimdLutOne3 => [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF3EFF]; + public static ReadOnlySpan AdvSimdLutOne3 => Base64UrlDecoderByte.AdvSimdLutOne3; - public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFF3F; + public static uint AdvSimdLutTwo3Uint1 => Base64UrlDecoderByte.AdvSimdLutTwo3Uint1; - public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); + public static int GetMaxDecodedLength(int utf8Length) => Base64UrlDecoderByte.GetMaxDecodedLength(utf8Length); - public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + public static bool IsInvalidLength(int bufferLength) => Base64DecoderByte.IsInvalidLength(bufferLength); - public static bool IsValidPadding(uint padChar) => padChar == EncodingPad || padChar == UrlEncodingPad; + public static bool IsValidPadding(uint padChar) => Base64UrlDecoderByte.IsValidPadding(padChar); - public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; + public static int SrcLength(bool isFinalBlock, int utf8Length) => Base64UrlDecoderByte.SrcLength(isFinalBlock, utf8Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] [CompExactlyDependsOn(typeof(Ssse3))] - public static bool TryDecode128Core( - Vector128 str, - Vector128 hiNibbles, - Vector128 maskSlashOrUnderscore, - Vector128 mask8F, - Vector128 lutLow, - Vector128 lutHigh, - Vector128 lutShift, - Vector128 shiftForUnderscore, - out Vector128 result) - { - Vector128 lowerBound = SimdShuffle(lutLow, hiNibbles, mask8F); - Vector128 upperBound = SimdShuffle(lutHigh, hiNibbles, mask8F); - - Vector128 below = Vector128.LessThan(str, lowerBound); - Vector128 above = Vector128.GreaterThan(str, upperBound); - Vector128 eq5F = Vector128.Equals(str, maskSlashOrUnderscore); - - // Take care as arguments are flipped in order! - Vector128 outside = Vector128.AndNot(below | above, eq5F); - - if (outside != Vector128.Zero) - { - result = default; - return false; - } - - Vector128 shift = SimdShuffle(lutShift.AsByte(), hiNibbles, mask8F); - str += shift; - - result = str + (eq5F & shiftForUnderscore); - return true; - } + public static bool TryDecode128Core(Vector128 str, Vector128 hiNibbles, Vector128 maskSlashOrUnderscore, Vector128 mask8F, + Vector128 lutLow, Vector128 lutHigh, Vector128 lutShift, Vector128 shiftForUnderscore, out Vector128 result) => + Base64UrlDecoderByte.TryDecode128Core(str, hiNibbles, maskSlashOrUnderscore, mask8F, lutLow, lutHigh, lutShift, shiftForUnderscore, out result); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Avx2))] - public static bool TryDecode256Core( - Vector256 str, - Vector256 hiNibbles, - Vector256 maskSlashOrUnderscore, - Vector256 lutLow, - Vector256 lutHigh, - Vector256 lutShift, - Vector256 shiftForUnderscore, - out Vector256 result) - { - Vector256 lowerBound = Avx2.Shuffle(lutLow, hiNibbles); - Vector256 upperBound = Avx2.Shuffle(lutHigh, hiNibbles); - - Vector256 below = Vector256.LessThan(str, lowerBound); - Vector256 above = Vector256.GreaterThan(str, upperBound); - Vector256 eq5F = Vector256.Equals(str, maskSlashOrUnderscore); - - // Take care as arguments are flipped in order! - Vector256 outside = Vector256.AndNot(below | above, eq5F); - - if (outside != Vector256.Zero) - { - result = default; - return false; - } - - Vector256 shift = Avx2.Shuffle(lutShift, hiNibbles); - str += shift; - - result = str + (eq5F & shiftForUnderscore); - return true; - } + public static bool TryDecode256Core(Vector256 str, Vector256 hiNibbles, Vector256 maskSlashOrUnderscore, Vector256 lutLow, + Vector256 lutHigh, Vector256 lutShift, Vector256 shiftForUnderscore, out Vector256 result) => + Base64UrlDecoderByte.TryDecode256Core(str, hiNibbles, maskSlashOrUnderscore, lutLow, lutHigh, lutShift, shiftForUnderscore, out result); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe int Decode(ushort* encodedBytes, ref sbyte decodingMap) @@ -915,11 +701,8 @@ public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, - ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) - where TBase64Decoder : IBase64Decoder - { - return DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); - } + ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder => + DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool TryLoadVector512(ushort* src, ushort* srcStart, int sourceLength, out Vector512 str) @@ -977,7 +760,9 @@ public static unsafe bool TryLoadVector128(ushort* src, ushort* srcStart, int so public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, int sourceLength, out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) { - AssertRead>(src, srcStart, sourceLength); + str1 = str2 = str3 = str4 = default; + /* TODO: Fix interleaving of the 4 vectors + * AssertRead>(src, srcStart, sourceLength); var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); @@ -993,9 +778,9 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i str1 = Vector128.Narrow(s11, s12); str2 = Vector128.Narrow(s21, s22); str3 = Vector128.Narrow(s31, s32); - str4 = Vector128.Narrow(s41, s42); + str4 = Vector128.Narrow(s41, s42);*/ - return true; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index d617563edb935f..f24bdeada06a6f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -222,10 +222,11 @@ public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destina public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) { OperationStatus status = EncodeToUtf8InPlace(buffer, dataLength, out bytesWritten); + return status == OperationStatus.Done; } - private readonly struct Base64UrlEncoderChar : IBase64Encoder + private readonly struct Base64UrlEncoderByte : IBase64Encoder { public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; @@ -245,12 +246,13 @@ public static int GetMaxSrcLength(int srcLength, int destLength) => srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? srcLength : GetMaxDecodedLength(destLength); - public static uint GetInPlaceDestinationLength(int encodedLength, int _) => 0; // not used for char encoding + public static uint GetInPlaceDestinationLength(int encodedLength, int leftOver) => + leftOver > 0 ? (uint)(encodedLength - leftOver - 1) : (uint)(encodedLength - 4); - public static int GetMaxEncodedLength(int _) => 0; // not used for char encoding + public static int GetMaxEncodedLength(int srcLength) => GetEncodedLength(srcLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, ushort* dest, ref byte encodingMap) + public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, byte* dest, ref byte encodingMap) { uint t0 = oneByte[0]; @@ -261,18 +263,18 @@ public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, ushort* dest, if (BitConverter.IsLittleEndian) { - dest[0] = (ushort)i0; - dest[1] = (ushort)i1; + dest[0] = (byte)i0; + dest[1] = (byte)i1; } else { - dest[1] = (ushort)i0; - dest[0] = (ushort)i1; + dest[1] = (byte)i0; + dest[0] = (byte)i1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, ushort* dest, ref byte encodingMap) + public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, ref byte encodingMap) { uint t0 = twoBytes[0]; uint t1 = twoBytes[1]; @@ -285,117 +287,67 @@ public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, ushort* dest if (BitConverter.IsLittleEndian) { - dest[0] = (ushort)i0; - dest[1] = (ushort)i1; - dest[2] = (ushort)i2; + dest[0] = (byte)i0; + dest[1] = (byte)i1; + dest[2] = (byte)i2; } else { - dest[2] = (ushort)i0; - dest[1] = (ushort)i1; - dest[0] = (ushort)i2; + dest[2] = (byte)i0; + dest[1] = (byte)i1; + dest[0] = (byte)i2; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector512 str) - { - AssertWrite>(dest, destStart, destLength); - (Vector512 utf16LowVector, Vector512 utf16HighVector) = Vector512.Widen(str); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector512.Count); - } + public static unsafe void StoreVector512ToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) => + Base64EncoderByte.StoreVector512ToDestination(dest, destStart, destLength, str); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector256 str) - { - AssertWrite>(dest, destStart, destLength); - (Vector256 utf16LowVector, Vector256 utf16HighVector) = Vector256.Widen(str); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector256.Count); - } + [CompExactlyDependsOn(typeof(Avx2))] + public static unsafe void StoreVector256ToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) => + Base64EncoderByte.StoreVector256ToDestination(dest, destStart, destLength, str); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, Vector128 str) - { - AssertWrite>(dest, destStart, destLength); - (Vector128 utf16LowVector, Vector128 utf16HighVector) = Vector128.Widen(str); - utf16LowVector.Store(dest); - utf16HighVector.Store(dest + Vector128.Count); - } + public static unsafe void StoreVector128ToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) => + Base64EncoderByte.StoreVector128ToDestination(dest, destStart, destLength, str); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - public static unsafe void StoreToDestination(ushort* dest, ushort* destStart, int destLength, - Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) - { - AssertWrite>(dest, destStart, destLength); - (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); - (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); - AdvSimd.Arm64.StoreVector128x4AndZip(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); - (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); - (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); - AdvSimd.Arm64.StoreVector128x4AndZip(dest + 32, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); - } + public static unsafe void StoreArmVector128x4ToDestination(byte* dest, byte* destStart, int destLength, + Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) => + Base64EncoderByte.StoreArmVector128x4ToDestination(dest, destStart, destLength, res1, res2, res3, res4); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeThreeAndWrite(byte* threeBytes, ushort* destination, ref byte encodingMap) - { - uint t0 = threeBytes[0]; - uint t1 = threeBytes[1]; - uint t2 = threeBytes[2]; - - uint i = (t0 << 16) | (t1 << 8) | t2; - - byte i0 = Unsafe.Add(ref encodingMap, (IntPtr)(i >> 18)); - byte i1 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 12) & 0x3F)); - byte i2 = Unsafe.Add(ref encodingMap, (IntPtr)((i >> 6) & 0x3F)); - byte i3 = Unsafe.Add(ref encodingMap, (IntPtr)(i & 0x3F)); - - if (BitConverter.IsLittleEndian) - { - destination[0] = i0; - destination[1] = i1; - destination[2] = i2; - destination[3] = i3; - } - else - { - destination[3] = i0; - destination[2] = i1; - destination[1] = i2; - destination[0] = i3; - } - } + public static unsafe void EncodeThreeAndWrite(byte* threeBytes, byte* destination, ref byte encodingMap) => + Base64EncoderByte.EncodeThreeAndWrite(threeBytes, destination, ref encodingMap); } - private readonly struct Base64UrlEncoderByte : IBase64Encoder + private readonly struct Base64UrlEncoderChar : IBase64Encoder { - public static ReadOnlySpan EncodingMap => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; + public static ReadOnlySpan EncodingMap => Base64UrlEncoderByte.EncodingMap; - public static sbyte Avx2LutChar62 => -17; // char '-' diff + public static sbyte Avx2LutChar62 => Base64UrlEncoderByte.Avx2LutChar62; - public static sbyte Avx2LutChar63 => 32; // char '_' diff + public static sbyte Avx2LutChar63 => Base64UrlEncoderByte.Avx2LutChar63; - public static ReadOnlySpan AdvSimdLut4 => "wxyz0123456789-_"u8; + public static ReadOnlySpan AdvSimdLut4 => Base64UrlEncoderByte.AdvSimdLut4; - public static uint Ssse3AdvSimdLutE3 => 0x000020EF; + public static uint Ssse3AdvSimdLutE3 => Base64UrlEncoderByte.Ssse3AdvSimdLutE3; - public static int IncrementPadTwo => 2; + public static int IncrementPadTwo => Base64UrlEncoderByte.IncrementPadTwo; - public static int IncrementPadOne => 3; + public static int IncrementPadOne => Base64UrlEncoderByte.IncrementPadOne; public static int GetMaxSrcLength(int srcLength, int destLength) => - srcLength <= MaximumEncodeLength && destLength >= GetEncodedLength(srcLength) ? - srcLength : GetMaxDecodedLength(destLength); + Base64UrlEncoderByte.GetMaxSrcLength(srcLength, destLength); - public static uint GetInPlaceDestinationLength(int encodedLength, int leftOver) => - leftOver > 0 ? (uint)(encodedLength - leftOver - 1) : (uint)(encodedLength - 4); + public static uint GetInPlaceDestinationLength(int encodedLength, int _) => 0; // not used for char encoding - public static int GetMaxEncodedLength(int srcLength) => GetEncodedLength(srcLength); + public static int GetMaxEncodedLength(int _) => 0; // not used for char encoding [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, byte* dest, ref byte encodingMap) + public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, ushort* dest, ref byte encodingMap) { uint t0 = oneByte[0]; @@ -406,18 +358,18 @@ public static unsafe void EncodeOneOptionallyPadTwo(byte* oneByte, byte* dest, r if (BitConverter.IsLittleEndian) { - dest[0] = (byte)i0; - dest[1] = (byte)i1; + dest[0] = (ushort)i0; + dest[1] = (ushort)i1; } else { - dest[1] = (byte)i0; - dest[0] = (byte)i1; + dest[1] = (ushort)i0; + dest[0] = (ushort)i1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, ref byte encodingMap) + public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, ushort* dest, ref byte encodingMap) { uint t0 = twoBytes[0]; uint t1 = twoBytes[1]; @@ -430,51 +382,61 @@ public static unsafe void EncodeTwoOptionallyPadOne(byte* twoBytes, byte* dest, if (BitConverter.IsLittleEndian) { - dest[0] = (byte)i0; - dest[1] = (byte)i1; - dest[2] = (byte)i2; + dest[0] = (ushort)i0; + dest[1] = (ushort)i1; + dest[2] = (ushort)i2; } else { - dest[2] = (byte)i0; - dest[1] = (byte)i1; - dest[0] = (byte)i2; + dest[2] = (ushort)i0; + dest[1] = (ushort)i1; + dest[0] = (ushort)i2; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector512 str) + public static unsafe void StoreVector512ToDestination(ushort* dest, ushort* destStart, int destLength, Vector512 str) { - Base64.AssertWrite>(dest, destStart, destLength); - str.Store(dest); + AssertWrite>(dest, destStart, destLength); + (Vector512 utf16LowVector, Vector512 utf16HighVector) = Vector512.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector512.Count); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - [CompExactlyDependsOn(typeof(Avx2))] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector256 str) + public static unsafe void StoreVector256ToDestination(ushort* dest, ushort* destStart, int destLength, Vector256 str) { - Base64.AssertWrite>(dest, destStart, destLength); - Avx.Store(dest, str.AsByte()); + AssertWrite>(dest, destStart, destLength); + (Vector256 utf16LowVector, Vector256 utf16HighVector) = Vector256.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector256.Count); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, Vector128 str) + public static unsafe void StoreVector128ToDestination(ushort* dest, ushort* destStart, int destLength, Vector128 str) { - Base64.AssertWrite>(dest, destStart, destLength); - str.Store(dest); + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector, Vector128 utf16HighVector) = Vector128.Widen(str); + utf16LowVector.Store(dest); + utf16HighVector.Store(dest + Vector128.Count); } [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] - public static unsafe void StoreToDestination(byte* dest, byte* destStart, int destLength, + public static unsafe void StoreArmVector128x4ToDestination(ushort* dest, ushort* destStart, int destLength, Vector128 res1, Vector128 res2, Vector128 res3, Vector128 res4) { - Base64.AssertWrite>(dest, destStart, destLength); - AdvSimd.Arm64.StoreVector128x4AndZip(dest, (res1, res2, res3, res4)); + AssertWrite>(dest, destStart, destLength); + (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); + (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); + AdvSimd.Arm64.StoreVector128x4AndZip(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); + (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); + (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); + AdvSimd.Arm64.StoreVector128x4AndZip(dest + 32, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void EncodeThreeAndWrite(byte* threeBytes, byte* destination, ref byte encodingMap) + public static unsafe void EncodeThreeAndWrite(byte* threeBytes, ushort* destination, ref byte encodingMap) { uint t0 = threeBytes[0]; uint t1 = threeBytes[1]; From 140663d98e49037e4b872f9db1ce9d6d02dee108 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 22 May 2024 14:20:29 -0700 Subject: [PATCH 20/38] Improve perf for Base.IsValid() oveerloads, exclude ARM vectorization for char(ushort) overload --- .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Text/Base64Url/Base64UrlValidator.cs | 37 ++++++++++--------- .../System/Buffers/Text/Base64Validator.cs | 33 +++++++++-------- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index 27873fb67f32f7..f60b19604ac9ee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -82,7 +82,7 @@ internal static unsafe OperationStatus EncodeTo(ReadOnlySpan< } end = srcMax - 48; - if (AdvSimd.Arm64.IsSupported && (end >= src)) + if (AdvSimd.Arm64.IsSupported && (end >= src) && typeof(T) == typeof(byte)) { AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index e824a43fa33fd1..7b8dccf80e924f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace System.Buffers.Text { public static partial class Base64Url @@ -49,21 +51,6 @@ public static bool IsValid(ReadOnlySpan utf8Base64UrlText) => public static bool IsValid(ReadOnlySpan utf8Base64UrlText, out int decodedLength) => Base64.IsValid(utf8Base64UrlText, out decodedLength); - private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) - { - // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. - int remainder = (int)((uint)length % 4); - - if (remainder == 1) - { - decodedLength = 0; - return false; - } - - decodedLength = (int)((uint)length / 4 * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; - return true; - } - private const uint UrlEncodingPad = '%'; // allowed for url padding private readonly struct Base64UrlCharValidatable : Base64.IBase64Validatable @@ -73,8 +60,9 @@ private static bool ValidateAndDecodeLength(int length, int paddingCount, out in public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == Base64.EncodingPad || value == UrlEncodingPad; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => - Base64Url.ValidateAndDecodeLength(length, paddingCount, out decodedLength); + Base64UrlByteValidatable.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } private readonly struct Base64UrlByteValidatable : Base64.IBase64Validatable @@ -84,8 +72,21 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64UrlChars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(byte value) => value == Base64.EncodingPad || value == UrlEncodingPad; - public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => - Base64Url.ValidateAndDecodeLength(length, paddingCount, out decodedLength); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) + { + // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. + int remainder = (int)((uint)length % 4); + + if (remainder == 1) + { + decodedLength = 0; + return false; + } + + decodedLength = (int)((uint)length / 4 * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; + return true; + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index a2df9364847691..b7bb7308552c4f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace System.Buffers.Text { public static partial class Base64 @@ -132,19 +134,6 @@ internal static bool IsValid(ReadOnlySpan base64Text, return false; } - private static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) - { - if ((uint)length % 4 == 0) - { - // Remove padding to get exact length. - decodedLength = (int)((uint)length / 4 * 3) - paddingCount; - return true; - } - - decodedLength = 0; - return false; - } - internal interface IBase64Validatable { static abstract int IndexOfAnyExcept(ReadOnlySpan span); @@ -160,8 +149,9 @@ internal interface IBase64Validatable public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == EncodingPad; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => - Base64.ValidateAndDecodeLength(length, paddingCount, out decodedLength); + Base64ByteValidatable.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } private readonly struct Base64ByteValidatable : IBase64Validatable @@ -171,8 +161,19 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(byte value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(byte value) => value == EncodingPad; - public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => - Base64.ValidateAndDecodeLength(length, paddingCount, out decodedLength); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) + { + if (length % 4 == 0) + { + // Remove padding to get exact length. + decodedLength = (int)((uint)length / 4 * 3) - paddingCount; + return true; + } + + decodedLength = 0; + return false; + } } } } From c4c605dbb766e978c5f0506738e0be1008a47fe0 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 23 May 2024 18:03:15 -0700 Subject: [PATCH 21/38] Apply feedbacks --- .../Base64Url/Base64UrlDecoderUnitTests.cs | 18 +++++++++++------- .../Base64Url/Base64UrlUnicodeAPIsUnitTests.cs | 6 +++--- .../src/System/Buffers/Text/Base64Decoder.cs | 11 ++++++++--- .../Buffers/Text/Base64Url/Base64UrlDecoder.cs | 18 ++++++++++++++++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index 5287b32e53be2c..8aad4d4530cd05 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -177,7 +177,7 @@ public void DecodingOutputTooSmall() Span decodedBytes = new byte[3]; int consumed, written; - if (numBytes >= 8) + if (numBytes >= 6) { Assert.True(OperationStatus.DestinationTooSmall == Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written), "Number of Input Bytes: " + numBytes); @@ -321,17 +321,21 @@ public void BasicDecodingWithFinalBlockTrueInputWithoutPaddingOrInvalidData(stri [InlineData("AQIDBA%%", 4, true)] [InlineData("z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo\u5948==", 33, false)] [InlineData("\u5948z_T-H7sqEkerqMweH1uSw1a5ebaAF9xa8B0ze1wet4epo01234567890123456789012345678901234567890123456789==", 0, false)] - public void TryDecodeFromUtf8VariousInput(string inputString, int expectedWritten, bool expectedStatus) + public void TryDecodeFromUtf8VariousInput(string inputString, int expectedWritten, bool succeeds) { - Span source = Encoding.ASCII.GetBytes(inputString); - Span decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; + byte[] source = Encoding.ASCII.GetBytes(inputString); + byte[] decodedBytes = new byte[Base64Url.GetMaxDecodedLength(source.Length)]; - Assert.Equal(expectedStatus, Base64Url.TryDecodeFromUtf8(source, decodedBytes, out int bytesWritten)); - Assert.Equal(expectedWritten, bytesWritten); - if (expectedStatus) + if (succeeds) { + Assert.True(Base64Url.TryDecodeFromUtf8(source, decodedBytes, out int bytesWritten)); + Assert.Equal(expectedWritten, bytesWritten); Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(inputString.Length, expectedWritten, source, decodedBytes)); } + else + { + Assert.Throws(() => Base64Url.TryDecodeFromUtf8(source, decodedBytes, out _)); + } } [Theory] diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs index b9f16e5d0d97ba..adbf17eae10047 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlUnicodeAPIsUnitTests.cs @@ -449,11 +449,11 @@ public static void Base64_AllMethodsRoundtripConsistently() [MemberData(nameof(Base64TestData))] public static void TryDecodeFromChars(string encodedAsString, byte[] expected) { - ReadOnlySpan encoded = encodedAsString; + char[] encoded = encodedAsString.ToCharArray(); if (expected == null) { - Span actual = new byte[Base64Url.GetMaxDecodedLength(encodedAsString.Length)]; - Assert.False(Base64Url.TryDecodeFromChars(encoded, actual, out int bytesWritten)); + byte[] actual = new byte[Base64Url.GetMaxDecodedLength(encodedAsString.Length)]; + Assert.Throws(() => Base64Url.TryDecodeFromChars(encoded, actual, out _)); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 39312f90b6c6ab..1000000dff3405 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -130,7 +130,11 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa // Therefore, (destLength / 3) * 4 will always be less than 2147483641 Debug.Assert(destLength < (int.MaxValue / 4 * 3)); maxSrcLength = (destLength / 3) * 4; - srcLength &= ~0x3; // Round down to multiple of 4, this only affect Base64UrlDecoder path + int remainder = destLength % 3; + if (isFinalBlock && remainder > 0) + { + srcLength &= ~0x3; // In case of Base64UrlDecoder source can be not a multiple of 4, round down to multiple of 4 + } } ref sbyte decodingMap = ref MemoryMarshal.GetReference(TBase64Decoder.DecodingMap); @@ -175,6 +179,8 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa // if isFinalBlock is false, we will never reach this point // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. long remaining = srcEnd - src; + Debug.Assert(typeof(TBase64Decoder) == typeof(Base64DecoderByte) ? remaining == 4 : remaining <= 4); + int i0 = TBase64Decoder.DecodeRemaining(srcEnd, ref decodingMap, remaining, out uint t2, out uint t3); byte* destMax = destBytes + (uint)destLength; @@ -330,7 +336,6 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span byte /// /// Thrown when the specified is less than 0. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedFromUtf8Length(int length) { if (length < 0) @@ -1300,7 +1305,7 @@ internal static bool IsWhiteSpace(int value) public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFFFF; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetMaxDecodedLength(int utf8Length) => Base64.GetMaxDecodedFromUtf8Length(utf8Length); + public static int GetMaxDecodedLength(int utf8Length) => GetMaxDecodedFromUtf8Length(utf8Length); public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index e1997e670ad371..80155320277f0c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -28,7 +28,7 @@ public static int GetMaxDecodedLength(int base64Length) { if (base64Length < 0) { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + ArgumentOutOfRangeException.ThrowIfNegative(base64Length); } int remainder = (int)((uint)base64Length % 4); @@ -84,7 +84,7 @@ public static int DecodeFromUtf8InPlace(Span buffer) OperationStatus status = DecodeFromUtf8InPlace(buffer, out int bytesWritten, ignoreWhiteSpace: true); // Base64.DecodeFromUtf8InPlace returns OperationStatus, therefore doesn't throw. - // For the Base64Url case I think it is better to throw to inform that invalid data found. + // For Base64Url, this is not an OperationStatus API and thus throws. if (OperationStatus.InvalidData == status) { throw new FormatException(SR.Format_BadBase64Char); @@ -133,10 +133,17 @@ public static int DecodeFromUtf8(ReadOnlySpan source, Span destinati /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// if bytes decoded successfully, otherwise . + /// contains an invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = DecodeFromUtf8(source, destination, out _, out bytesWritten); + if (OperationStatus.InvalidData == status) + { + throw new FormatException(SR.Format_BadBase64Char); + } + return status == OperationStatus.Done; } @@ -330,10 +337,17 @@ public static int DecodeFromChars(ReadOnlySpan source, Span destinat /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. /// if bytes decoded successfully, otherwise . + /// contains an invalid Base64Url character, + /// more than two padding characters, or a non-white space-character among the padding characters. public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); + if (OperationStatus.InvalidData == status) + { + throw new FormatException(SR.Format_BadBase64Char); + } + return OperationStatus.Done == status; } From 61095ddc3ccbf87bf41d1ad5cc92255307ef0fcd Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 23 May 2024 22:11:41 -0700 Subject: [PATCH 22/38] Revert Assert --- .../src/System/Buffers/Text/Base64Decoder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 1000000dff3405..339dc6d82ba7d7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -178,9 +178,8 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa // if isFinalBlock is false, we will never reach this point // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. + // If more than 4 bytes remained it means destination is smaller and would exit with InvalidDataExit long remaining = srcEnd - src; - Debug.Assert(typeof(TBase64Decoder) == typeof(Base64DecoderByte) ? remaining == 4 : remaining <= 4); - int i0 = TBase64Decoder.DecodeRemaining(srcEnd, ref decodingMap, remaining, out uint t2, out uint t3); byte* destMax = destBytes + (uint)destLength; From b338a9485470ec41782dcc44471c5c64ebe9832b Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 24 May 2024 12:05:06 -0700 Subject: [PATCH 23/38] Fix ARM vectorization failure for char overload --- .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Buffers/Text/Base64Url/Base64UrlDecoder.cs | 14 ++++++-------- .../Buffers/Text/Base64Url/Base64UrlEncoder.cs | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index f60b19604ac9ee..27873fb67f32f7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -82,7 +82,7 @@ internal static unsafe OperationStatus EncodeTo(ReadOnlySpan< } end = srcMax - 48; - if (AdvSimd.Arm64.IsSupported && (end >= src) && typeof(T) == typeof(byte)) + if (AdvSimd.Arm64.IsSupported && (end >= src)) { AdvSimdEncode(ref src, ref dest, end, maxSrcLength, destLength, srcBytes, destBytes); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 80155320277f0c..a32947a303ed5f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -774,9 +774,7 @@ public static unsafe bool TryLoadVector128(ushort* src, ushort* srcStart, int so public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, int sourceLength, out Vector128 str1, out Vector128 str2, out Vector128 str3, out Vector128 str4) { - str1 = str2 = str3 = str4 = default; - /* TODO: Fix interleaving of the 4 vectors - * AssertRead>(src, srcStart, sourceLength); + AssertRead> (src, srcStart, sourceLength); var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); @@ -789,12 +787,12 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i return false; } - str1 = Vector128.Narrow(s11, s12); - str2 = Vector128.Narrow(s21, s22); - str3 = Vector128.Narrow(s31, s32); - str4 = Vector128.Narrow(s41, s42);*/ + str1 = Vector128.Narrow(s11, s31); + str2 = Vector128.Narrow(s12, s32); + str3 = Vector128.Narrow(s21, s41); + str4 = Vector128.Narrow(s22, s42); - return false; + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index f24bdeada06a6f..05a0d005909614 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -429,10 +429,10 @@ public static unsafe void StoreArmVector128x4ToDestination(ushort* dest, ushort* AssertWrite>(dest, destStart, destLength); (Vector128 utf16LowVector1, Vector128 utf16HighVector1) = Vector128.Widen(res1); (Vector128 utf16LowVector2, Vector128 utf16HighVector2) = Vector128.Widen(res2); - AdvSimd.Arm64.StoreVector128x4AndZip(dest, (utf16LowVector1, utf16HighVector1, utf16LowVector2, utf16HighVector2)); (Vector128 utf16LowVector3, Vector128 utf16HighVector3) = Vector128.Widen(res3); (Vector128 utf16LowVector4, Vector128 utf16HighVector4) = Vector128.Widen(res4); - AdvSimd.Arm64.StoreVector128x4AndZip(dest + 32, (utf16LowVector3, utf16HighVector3, utf16LowVector4, utf16HighVector4)); + AdvSimd.Arm64.StoreVector128x4AndZip(dest, (utf16LowVector1, utf16LowVector2, utf16LowVector3, utf16LowVector4)); + AdvSimd.Arm64.StoreVector128x4AndZip(dest + 32, (utf16HighVector1, utf16HighVector2, utf16HighVector3, utf16HighVector4)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 7d4242d9910b2bc00a1d370f960f8e93849738e8 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 24 May 2024 14:28:23 -0700 Subject: [PATCH 24/38] Apply suggestions from code review Co-authored-by: Jeremy Barton --- .../System.Private.CoreLib/src/System/Buffers/Text/Base64.cs | 1 - .../src/System/Buffers/Text/Base64Decoder.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index 2e5939bc74078b..eacb54b9a326ad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -65,7 +65,6 @@ internal static unsafe void AssertWrite(ushort* dest, ushort* destStart } } - internal interface IBase64Encoder where T : unmanaged { static abstract ReadOnlySpan EncodingMap { get; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 339dc6d82ba7d7..5f024aaf6f9a8b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -129,8 +129,8 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa // This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733) // Therefore, (destLength / 3) * 4 will always be less than 2147483641 Debug.Assert(destLength < (int.MaxValue / 4 * 3)); - maxSrcLength = (destLength / 3) * 4; - int remainder = destLength % 3; + (maxSrcLength, int remainder) = int.DivRem(destLength, 3); + maxSrcLength *= 4; if (isFinalBlock && remainder > 0) { srcLength &= ~0x3; // In case of Base64UrlDecoder source can be not a multiple of 4, round down to multiple of 4 From f6c4d93cb1f7fab8d931708337fec6e6f9f6428c Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 28 May 2024 09:03:05 -0700 Subject: [PATCH 25/38] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../src/System/Buffers/Text/Base64Encoder.cs | 2 +- .../Text/Base64Url/Base64UrlDecoder.cs | 22 +++++++++---------- .../Text/Base64Url/Base64UrlEncoder.cs | 10 ++++----- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index 27873fb67f32f7..b5748c52f63fbd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -202,7 +202,7 @@ internal static unsafe OperationStatus EncodeToUtf8InPlace(Span< return OperationStatus.DestinationTooSmall; } - int leftover = dataLength % 3; // how many bytes after packs of 3 + int leftover = (int)((uint)dataLength % 3); // how many bytes after packs of 3 uint destinationIndex = TBase64Encoder.GetInPlaceDestinationLength(encodedLength, leftover); uint sourceIndex = (uint)(dataLength - leftover); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index a32947a303ed5f..3da5a8710391e1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -85,7 +85,7 @@ public static int DecodeFromUtf8InPlace(Span buffer) // Base64.DecodeFromUtf8InPlace returns OperationStatus, therefore doesn't throw. // For Base64Url, this is not an OperationStatus API and thus throws. - if (OperationStatus.InvalidData == status) + if (status == OperationStatus.InvalidData) { throw new FormatException(SR.Format_BadBase64Char); } @@ -113,12 +113,12 @@ public static int DecodeFromUtf8(ReadOnlySpan source, Span destinati { OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); - if (OperationStatus.Done == status) + if (status == OperationStatus.Done) { return bytesWritten; } - if (OperationStatus.DestinationTooSmall == status) + if (status == OperationStatus.DestinationTooSmall) { throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } @@ -139,7 +139,7 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti { OperationStatus status = DecodeFromUtf8(source, destination, out _, out bytesWritten); - if (OperationStatus.InvalidData == status) + if (status == OperationStatus.InvalidData) { throw new FormatException(SR.Format_BadBase64Char); } @@ -171,7 +171,7 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) ArrayPool.Shared.Return(rented); } - return OperationStatus.Done == status ? ret : throw new FormatException(SR.Format_BadBase64Char); + return status == OperationStatus.Done ? ret : throw new FormatException(SR.Format_BadBase64Char); } /// @@ -316,17 +316,17 @@ public static int DecodeFromChars(ReadOnlySpan source, Span destinat { OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); - if (OperationStatus.Done == status) + if (status == OperationStatus.Done) { return bytesWritten; } - if (OperationStatus.DestinationTooSmall == status) + if (status == OperationStatus.DestinationTooSmall) { throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } - Debug.Assert(OperationStatus.InvalidData == status); + Debug.Assert(status == OperationStatus.InvalidData); throw new FormatException(SR.Format_BadBase64Char); } @@ -343,12 +343,12 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest { OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); - if (OperationStatus.InvalidData == status) + if (status == OperationStatus.InvalidData) { throw new FormatException(SR.Format_BadBase64Char); } - return OperationStatus.Done == status; + return status == OperationStatus.Done; } /// @@ -375,7 +375,7 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) ArrayPool.Shared.Return(rented); } - return OperationStatus.Done == status ? ret : throw new FormatException(SR.Format_BadBase64Char); + return status == OperationStatus.Done ? ret : throw new FormatException(SR.Format_BadBase64Char); } private readonly struct Base64UrlDecoderByte : IBase64Decoder diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 05a0d005909614..e95122957efae9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -49,7 +49,7 @@ public static int GetEncodedLength(int bytesLength) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); } - int remainder = bytesLength % 3; + int remainder = (int)((uint)bytesLength % 3); return (bytesLength / 3) * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. } @@ -66,12 +66,12 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination { OperationStatus status = EncodeToUtf8(source, destination, out _, out int bytesWritten); - if (OperationStatus.Done == status) + if (status == OperationStatus.Done) { return bytesWritten; } - if (OperationStatus.DestinationTooSmall == status) + if (status == OperationStatus.DestinationTooSmall) { throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } @@ -129,12 +129,12 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio { OperationStatus status = EncodeToChars(source, destination, out _, out int charsWritten); - if (OperationStatus.Done == status) + if (status == OperationStatus.Done) { return charsWritten; } - if (OperationStatus.DestinationTooSmall == status) + if (status == OperationStatus.DestinationTooSmall) { throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } From 6c1035df5347a2d5de20cb60504c2548388eb892 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 28 May 2024 11:17:42 -0700 Subject: [PATCH 26/38] Apply more feedback --- .../src/System/Buffers/Text/Base64Decoder.cs | 6 ++---- .../src/System/Buffers/Text/Base64Encoder.cs | 3 +-- .../System/Buffers/Text/Base64Url/Base64UrlDecoder.cs | 5 +---- .../System/Buffers/Text/Base64Url/Base64UrlEncoder.cs | 9 +++------ .../System/Buffers/Text/Base64Url/Base64UrlValidator.cs | 5 ++--- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 5f024aaf6f9a8b..1f57fb1418ef80 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -180,6 +180,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. // If more than 4 bytes remained it means destination is smaller and would exit with InvalidDataExit long remaining = srcEnd - src; + Debug.Assert(remaining > 0 && remaining <= 8); int i0 = TBase64Decoder.DecodeRemaining(srcEnd, ref decodingMap, remaining, out uint t2, out uint t3); byte* destMax = destBytes + (uint)destLength; @@ -337,10 +338,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span byte /// public static int GetMaxDecodedFromUtf8Length(int length) { - if (length < 0) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); - } + ArgumentOutOfRangeException.ThrowIfNegative(length); return (length >> 2) * 3; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index b5748c52f63fbd..fadb0276df267f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -160,8 +160,7 @@ internal static unsafe OperationStatus EncodeTo(ReadOnlySpan< [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxEncodedToUtf8Length(int length) { - if ((uint)length > MaximumEncodeLength) - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)length, MaximumEncodeLength); return ((length + 2) / 3) * 4; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 3da5a8710391e1..e439d141b5b840 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -26,10 +26,7 @@ public static partial class Base64Url /// public static int GetMaxDecodedLength(int base64Length) { - if (base64Length < 0) - { - ArgumentOutOfRangeException.ThrowIfNegative(base64Length); - } + ArgumentOutOfRangeException.ThrowIfNegative(base64Length); int remainder = (int)((uint)base64Length % 4); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index e95122957efae9..6a1268b44991b3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -44,14 +44,11 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, /// public static int GetEncodedLength(int bytesLength) { - if ((uint)bytesLength > Base64.MaximumEncodeLength) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); - } + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)bytesLength, Base64.MaximumEncodeLength); - int remainder = (int)((uint)bytesLength % 3); + (int whole, int remainder) = int.DivRem(bytesLength, 3); - return (bytesLength / 3) * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. + return whole * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 7b8dccf80e924f..8adfeb80e4eacc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -76,15 +76,14 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. - int remainder = (int)((uint)length % 4); - + (int whole, int remainder) = int.DivRem(length, 4); if (remainder == 1) { decodedLength = 0; return false; } - decodedLength = (int)((uint)length / 4 * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; + decodedLength = (whole * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; return true; } } From 22430607f2830d81a2976137bedaaa3df531b358 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 28 May 2024 11:58:02 -0700 Subject: [PATCH 27/38] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs | 4 ++-- .../src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 6a1268b44991b3..2fb1a8e7cc9334 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -46,9 +46,9 @@ public static int GetEncodedLength(int bytesLength) { ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)bytesLength, Base64.MaximumEncodeLength); - (int whole, int remainder) = int.DivRem(bytesLength, 3); + (uint whole, uint remainder) = uint.DivRem((uint)bytesLength, 3); - return whole * 4 + (remainder > 0 ? remainder + 1 : 0); // if remainder is 1 or 2, the encoded length will be 1 byte longer. + return (int)(whole * 4 + (remainder > 0 ? remainder + 1 : 0)); // if remainder is 1 or 2, the encoded length will be 1 byte longer. } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index 8adfeb80e4eacc..c3495c1c94c15f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -76,14 +76,14 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) { // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. - (int whole, int remainder) = int.DivRem(length, 4); + (uint whole, uint remainder) = uint.DivRem((uint)length, 4); if (remainder == 1) { decodedLength = 0; return false; } - decodedLength = (whole * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount; + decodedLength = (int)((whole * 3) + (remainder > 0 ? remainder - 1 : 0) - paddingCount); return true; } } From 42543e8169c1a9a10e2a4ad7f94257583e1f4123 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 30 May 2024 11:14:01 -0700 Subject: [PATCH 28/38] Apply review comment left overs --- .../src/System/Buffers/Text/Base64Decoder.cs | 10 ++++------ .../src/System/Buffers/Text/Base64Encoder.cs | 1 + .../System/Buffers/Text/Base64Url/Base64UrlDecoder.cs | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 1f57fb1418ef80..fa4f3d3bc0e8ad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -159,8 +159,6 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa goto DestinationTooSmallExit; } - // If input is less than 4 bytes, srcLength == sourceIndex == 0 - // If input is not a multiple of 4, sourceIndex == srcLength != 0 if (src == srcEnd) { if (isFinalBlock) @@ -177,10 +175,10 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa } // if isFinalBlock is false, we will never reach this point - // Handle remaining, for Base64 its always 4 bytes, for Base64Url it could be 2, 3, or 4 bytes left. - // If more than 4 bytes remained it means destination is smaller and would exit with InvalidDataExit + // Handle remaining bytes, for Base64 its always 4 bytes, for Base64Url up to 8 bytes left. + // If more than 4 bytes remained it will end up in DestinationTooSmallExit or InvalidDataExit (might succeed after whitespace removed) long remaining = srcEnd - src; - Debug.Assert(remaining > 0 && remaining <= 8); + Debug.Assert(typeof(TBase64Decoder) == typeof(Base64DecoderByte) ? remaining == 4 : remaining < 8); int i0 = TBase64Decoder.DecodeRemaining(srcEnd, ref decodingMap, remaining, out uint t2, out uint t3); byte* destMax = destBytes + (uint)destLength; @@ -336,6 +334,7 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span byte /// /// Thrown when the specified is less than 0. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedFromUtf8Length(int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); @@ -1301,7 +1300,6 @@ internal static bool IsWhiteSpace(int value) public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFFFF; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxDecodedLength(int utf8Length) => GetMaxDecodedFromUtf8Length(utf8Length); public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 != 0; // only decode input if it is a multiple of 4 diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs index fadb0276df267f..a3f409a1b74a58 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Encoder.cs @@ -711,6 +711,7 @@ private static unsafe uint Encode(byte* threeBytes, ref byte encodingMap) public static int IncrementPadOne => 4; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetMaxSrcLength(int srcLength, int destLength) => srcLength <= MaximumEncodeLength && destLength >= GetMaxEncodedToUtf8Length(srcLength) ? srcLength : (destLength >> 2) * 3; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index e439d141b5b840..419be7bfdcf44a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -28,9 +28,9 @@ public static int GetMaxDecodedLength(int base64Length) { ArgumentOutOfRangeException.ThrowIfNegative(base64Length); - int remainder = (int)((uint)base64Length % 4); + (uint whole, uint remainder) = uint.DivRem((uint)base64Length, 4); - return (base64Length >> 2) * 3 + (remainder > 0 ? remainder - 1 : 0); + return (int)(whole * 3 + (remainder > 0 ? remainder - 1 : 0)); } /// From bdbdbab4d686a591bebb890e7ce5c07aca6b6753 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 30 May 2024 15:40:18 -0700 Subject: [PATCH 29/38] Apply suggestions from code review Co-authored-by: Miha Zupan --- .../Buffers/Text/Base64Url/Base64UrlDecoder.cs | 13 +++++-------- .../Buffers/Text/Base64Url/Base64UrlEncoder.cs | 18 ++++-------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 419be7bfdcf44a..fb5beee100a517 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -775,19 +775,16 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); - if (VectorContainsNonAsciiChar(s11) || VectorContainsNonAsciiChar(s12) || - VectorContainsNonAsciiChar(s21) || VectorContainsNonAsciiChar(s22) || - VectorContainsNonAsciiChar(s31) || VectorContainsNonAsciiChar(s32) || - VectorContainsNonAsciiChar(s41) || VectorContainsNonAsciiChar(s42)) + if (VectorContainsNonAsciiChar(s11 | s12 | s21 | s22 | s31 | s32 | s41 | s42)) { str1 = str2 = str3 = str4 = default; return false; } - str1 = Vector128.Narrow(s11, s31); - str2 = Vector128.Narrow(s12, s32); - str3 = Vector128.Narrow(s21, s41); - str4 = Vector128.Narrow(s22, s42); + str1 = Ascii.ExtractAsciiVector(s11, s31); + str2 = Ascii.ExtractAsciiVector(s12, s32); + str3 = Ascii.ExtractAsciiVector(s21, s41); + str4 = Ascii.ExtractAsciiVector(s22, s42); return true; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 2fb1a8e7cc9334..68054debbbde65 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -68,13 +68,8 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination return bytesWritten; } - if (status == OperationStatus.DestinationTooSmall) - { - throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); - } - - Debug.Fail("Unreachable code"); - return 0; + Debug.Assert(status == OperationStatus.DestinationTooSmall); + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } /// @@ -131,13 +126,8 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio return charsWritten; } - if (status == OperationStatus.DestinationTooSmall) - { - throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); - } - - Debug.Fail("Unreachable code"); - return 0; + Debug.Assert(status == OperationStatus.DestinationTooSmall); + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } /// From b871d6db15d65a3180094f7b5eeeeefbab0ccfb3 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 30 May 2024 16:16:36 -0700 Subject: [PATCH 30/38] Apply remaining feedback --- .../Base64Url/Base64UrlDecoderUnitTests.cs | 8 +- .../Text/Base64Url/Base64UrlDecoder.cs | 74 ++----------------- .../System/Buffers/Text/Base64Validator.cs | 1 - .../src/System/Text/Ascii.Utility.cs | 8 +- 4 files changed, 15 insertions(+), 76 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index 8aad4d4530cd05..b98d7ce8448894 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -222,8 +222,10 @@ public void DecodingOutputTooSmall() } } - [Fact] - public void DecodingOutputTooSmallWithFinalBlockFalse() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodingOutputTooSmallWithFinalBlockTrueFalse(bool isFinalBlock) { for (int numBytes = 8; numBytes < 20; numBytes++) { @@ -233,7 +235,7 @@ public void DecodingOutputTooSmallWithFinalBlockFalse() Span decodedBytes = new byte[4]; int consumed, written; Assert.True(OperationStatus.DestinationTooSmall == - Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written, isFinalBlock: false), "Number of Input Bytes: " + numBytes); + Base64Url.DecodeFromUtf8(source, decodedBytes, out consumed, out written, isFinalBlock: isFinalBlock), "Number of Input Bytes: " + numBytes); int expectedConsumed = 4; int expectedWritten = 3; Assert.Equal(expectedConsumed, consumed); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index fb5beee100a517..479d649e5a22ab 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -7,6 +7,7 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using System.Text; using System.Text.Unicode; using static System.Buffers.Text.Base64; @@ -722,7 +723,7 @@ public static unsafe bool TryLoadVector512(ushort* src, ushort* srcStart, int so Vector512 utf16VectorLower = Vector512.Load(src); Vector512 utf16VectorUpper = Vector512.Load(src + 32); - if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + if (Ascii.VectorContainsNonAsciiChar(utf16VectorLower | utf16VectorUpper)) { str = default; return false; @@ -740,7 +741,7 @@ public static unsafe bool TryLoadAvxVector256(ushort* src, ushort* srcStart, int Vector256 utf16VectorLower = Avx.LoadVector256(src); Vector256 utf16VectorUpper = Avx.LoadVector256(src + 16); - if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + if (Ascii.VectorContainsNonAsciiChar(utf16VectorLower | utf16VectorUpper)) { str = default; return false; @@ -756,13 +757,13 @@ public static unsafe bool TryLoadVector128(ushort* src, ushort* srcStart, int so AssertRead>(src, srcStart, sourceLength); Vector128 utf16VectorLower = Vector128.LoadUnsafe(ref *src); Vector128 utf16VectorUpper = Vector128.LoadUnsafe(ref *src, 8); - if (VectorContainsNonAsciiChar(utf16VectorLower) || VectorContainsNonAsciiChar(utf16VectorUpper)) + if (Ascii.VectorContainsNonAsciiChar(utf16VectorLower | utf16VectorUpper)) { str = default; return false; } - str = Vector128.Narrow(utf16VectorLower, utf16VectorUpper); + str = Ascii.ExtractAsciiVector(utf16VectorLower, utf16VectorUpper); return true; } @@ -775,7 +776,7 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i var (s11, s12, s21, s22) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src); var (s31, s32, s41, s42) = AdvSimd.Arm64.LoadVector128x4AndUnzip(src + 32); - if (VectorContainsNonAsciiChar(s11 | s12 | s21 | s22 | s31 | s32 | s41 | s42)) + if (Ascii.VectorContainsNonAsciiChar(s11 | s12 | s21 | s22 | s31 | s32 | s41 | s42)) { str1 = str2 = str3 = str4 = default; return false; @@ -788,69 +789,6 @@ public static unsafe bool TryLoadArmVector128x4(ushort* src, ushort* srcStart, i return true; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector512 utf16Vector) - { - const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 - Vector512 zeroIsAscii = utf16Vector & Vector512.Create(asciiMask); - // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. - return zeroIsAscii != Vector512.Zero; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector256 utf16Vector) - { - if (Avx.IsSupported) - { - Vector256 asciiMaskForTestZ = Vector256.Create((ushort)0xFF80); - return !Avx.TestZ(utf16Vector.AsInt16(), asciiMaskForTestZ.AsInt16()); - } - else - { - const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 - Vector256 zeroIsAscii = utf16Vector & Vector256.Create(asciiMask); - // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. - return zeroIsAscii != Vector256.Zero; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) - { - // prefer architecture specific intrinsic as they offer better perf - if (Sse2.IsSupported) - { - if (Sse41.IsSupported) - { - Vector128 asciiMaskForTestZ = Vector128.Create((ushort)0xFF80); - // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. - return !Sse41.TestZ(utf16Vector.AsInt16(), asciiMaskForTestZ.AsInt16()); - } - else - { - Vector128 asciiMaskForAddSaturate = Vector128.Create((ushort)0x7F80); - // The operation below forces the 0x8000 bit of each WORD to be set iff the WORD element - // has value >= 0x0800 (non-ASCII). Then we'll treat the vector as a BYTE vector in order - // to extract the mask. Reminder: the 0x0080 bit of each WORD should be ignored. - return (Sse2.MoveMask(Sse2.AddSaturate(utf16Vector, asciiMaskForAddSaturate).AsByte()) & 0b_1010_1010_1010_1010) != 0; - } - } - else if (AdvSimd.Arm64.IsSupported) - { - // First we pick four chars, a larger one from all four pairs of adjacent chars in the vector. - // If any of those four chars has a non-ASCII bit set, we have seen non-ASCII data. - Vector128 maxChars = AdvSimd.Arm64.MaxPairwise(utf16Vector, utf16Vector); - return (maxChars.AsUInt64().ToScalar() & 0xFF80FF80FF80FF80) != 0; - } - else - { - const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 - Vector128 zeroIsAscii = utf16Vector & Vector128.Create(asciiMask); - // If a non-ASCII bit is set in any WORD of the vector, we have seen non-ASCII data. - return zeroIsAscii != Vector128.Zero; - } - } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs index b7bb7308552c4f..3012bf6f4a0a5e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Validator.cs @@ -149,7 +149,6 @@ internal interface IBase64Validatable public static int IndexOfAnyExcept(ReadOnlySpan span) => span.IndexOfAnyExcept(s_validBase64Chars); public static bool IsWhiteSpace(char value) => Base64.IsWhiteSpace(value); public static bool IsEncodingPad(char value) => value == EncodingPad; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidateAndDecodeLength(int length, int paddingCount, out int decodedLength) => Base64ByteValidatable.ValidateAndDecodeLength(length, paddingCount, out decodedLength); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Ascii.Utility.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Ascii.Utility.cs index 76d2701b0eaabc..57346e0861e6ff 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Ascii.Utility.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Ascii.Utility.cs @@ -1517,7 +1517,7 @@ private static bool VectorContainsNonAsciiChar(Vector128 asciiVector) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) + internal static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) { // prefer architecture specific intrinsic as they offer better perf if (Sse2.IsSupported) @@ -1554,7 +1554,7 @@ private static bool VectorContainsNonAsciiChar(Vector128 utf16Vector) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector256 utf16Vector) + internal static bool VectorContainsNonAsciiChar(Vector256 utf16Vector) { if (Avx.IsSupported) { @@ -1571,7 +1571,7 @@ private static bool VectorContainsNonAsciiChar(Vector256 utf16Vector) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool VectorContainsNonAsciiChar(Vector512 utf16Vector) + internal static bool VectorContainsNonAsciiChar(Vector512 utf16Vector) { const ushort asciiMask = ushort.MaxValue - 127; // 0xFF80 Vector512 zeroIsAscii = utf16Vector & Vector512.Create(asciiMask); @@ -1651,7 +1651,7 @@ private static bool AllCharsInVectorAreAscii(Vector512 vector) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ExtractAsciiVector(Vector128 vectorFirst, Vector128 vectorSecond) + internal static Vector128 ExtractAsciiVector(Vector128 vectorFirst, Vector128 vectorSecond) { // Narrows two vectors of words [ w7 w6 w5 w4 w3 w2 w1 w0 ] and [ w7' w6' w5' w4' w3' w2' w1' w0' ] // to a vector of bytes [ b7 ... b0 b7' ... b0']. From 73bd7df34f52fe81d67dbeb5db8a2e8745901440 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 7 Jun 2024 13:41:31 -0700 Subject: [PATCH 31/38] Apply suggestions from code review Co-authored-by: Jeremy Barton --- .../Text/Base64Url/Base64UrlDecoder.cs | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 479d649e5a22ab..b2a87c56161b0f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -35,25 +35,16 @@ public static int GetMaxDecodedLength(int base64Length) } /// - /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// Decodes the span of UTF-8 encoded text represented as Base64Url into binary data. /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. - /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. - /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. - /// (default) when the input span contains the entire data to encode. - /// Set to when the source buffer contains the entirety of the data to encode. - /// Set to if this method is being called in a loop and if more input data may follow. - /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the OperationStatus enum values: - /// - Done - on successful processing of the entire input span - /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input - /// - NeedMoreData - only if is false and the input is not a multiple of 4 - /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, or if it contains invalid/more than two padding characters - /// or if the input is incomplete (i.e. the remainder of % 4 is 1) and is . - /// + /// When this method returns, contains the number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// when the input span contains the entirety of data to encode; when more data may follow, such as when calling in a loop. The default is . + /// One of the enumeration values that indicates the success or failure of the operation. /// - /// As padding is optional the length not required to be a multiple of 4 even if is . + /// As padding is optional for Base64Url the length not required to be a multiple of 4 even if is . /// If the length is not a multiple of 4 and is the remainders decoded accordingly: /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. @@ -63,11 +54,11 @@ public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); /// - /// Decode the span of UTF-8 encoded text in Base64Url (in-place) into binary data. + /// Decodes the span of UTF-8 encoded text in Base64Url into binary data, in-place. /// The decoded binary output is smaller than the text data contained in the input (the operation deflates the data). /// /// The input span which contains the base 64 text data that needs to be decoded. - /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. + /// The number of bytes written into . This can be used to slice the output for subsequent calls, if necessary. /// contains an invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. /// @@ -92,12 +83,12 @@ public static int DecodeFromUtf8InPlace(Span buffer) } /// - /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// Decodes the span of UTF-8 encoded text represented as Base64Url into binary data. /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. - /// The number of bytes written into the . This can be used to slice the output for subsequent calls, if necessary. - /// Thrown when the encoded output cannot fit in the provided. + /// The number of bytes written into . This can be used to slice the output for subsequent calls, if necessary. + /// The buffer in is too small to hold the encoded output. /// contains an invalid Base64Url character, /// more than two padding characters, or a non-white space-character among the padding characters. /// From 070f68560505c08e86e6a3b5c4efb381827557ec Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 7 Jun 2024 14:55:12 -0700 Subject: [PATCH 32/38] Apply the doc feedback for other API docs --- .../Text/Base64Url/Base64UrlDecoder.cs | 88 +++++++++---------- .../Text/Base64Url/Base64UrlEncoder.cs | 64 ++++++-------- 2 files changed, 65 insertions(+), 87 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index b2a87c56161b0f..e4f6904ae38bde 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -22,8 +22,7 @@ public static partial class Base64Url /// /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size . /// - /// - /// Thrown when the specified is less than 0. + /// The specified is less than 0. /// public static int GetMaxDecodedLength(int base64Length) { @@ -41,14 +40,15 @@ public static int GetMaxDecodedLength(int base64Length) /// The output span which contains the result of the operation, i.e. the decoded binary data. /// When this method returns, contains the number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. - /// when the input span contains the entirety of data to encode; when more data may follow, such as when calling in a loop. The default is . + /// when the input span contains the entirety of data to encode; when more data may follow, + /// such as when calling in a loop, subsequent calls with should end with call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. /// /// As padding is optional for Base64Url the length not required to be a multiple of 4 even if is . /// If the length is not a multiple of 4 and is the remainders decoded accordingly: - /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. - /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. - /// Remainder of 1 byte - will cause OperationStatus.InvalidData result. + /// - Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// - Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// - Remainder of 1 byte - will cause OperationStatus.InvalidData result. /// public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => DecodeFrom(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); @@ -60,13 +60,13 @@ public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, SpanThe input span which contains the base 64 text data that needs to be decoded. /// The number of bytes written into . This can be used to slice the output for subsequent calls, if necessary. /// contains an invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. /// - /// As padding is optional the input length not required to be a multiple of 4. - /// If the input length is not a multiple of 4 the remainders decoded accordingly: - /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. - /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. - /// Remainder of 1 byte - is invalid input, causes FormatException. + /// As padding is optional for Base64Url the length not required to be a multiple of 4. + /// If the length is not a multiple of 4 the remainders decoded accordingly: + /// - Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// - Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// - Remainder of 1 byte - is invalid input, causes FormatException. /// public static int DecodeFromUtf8InPlace(Span buffer) { @@ -90,13 +90,13 @@ public static int DecodeFromUtf8InPlace(Span buffer) /// The number of bytes written into . This can be used to slice the output for subsequent calls, if necessary. /// The buffer in is too small to hold the encoded output. /// contains an invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. /// - /// As padding is optional the input length not required to be a multiple of 4. - /// If the input length is not a multiple of 4 the remainders decoded accordingly: - /// Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. - /// Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. - /// Remainder of 1 byte - is invalid input, causes FormatException. + /// As padding is optional the length not required to be a multiple of 4. + /// If the length is not a multiple of 4 the remainders decoded accordingly: + /// - Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. + /// - Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. + /// - Remainder of 1 byte - is invalid input, causes FormatException. /// public static int DecodeFromUtf8(ReadOnlySpan source, Span destination) { @@ -116,14 +116,14 @@ public static int DecodeFromUtf8(ReadOnlySpan source, Span destinati } /// - /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// Decodes the span of UTF-8 encoded text represented as Base64Url into binary data. /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. - /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if bytes decoded successfully, otherwise . /// contains an invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = DecodeFromUtf8(source, destination, out _, out bytesWritten); @@ -137,12 +137,12 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti } /// - /// Decode the span of UTF-8 encoded text represented as Base64Url into binary data. + /// Decodes the span of UTF-8 encoded text represented as Base64Url into binary data. /// /// The input span which contains UTF-8 encoded text in Base64Url that needs to be decoded. /// >A byte array which contains the result of the decoding operation. /// contains an invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. public static byte[] DecodeFromUtf8(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); @@ -164,29 +164,21 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) } /// - /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. + /// Decodes the span of unicode ASCII chars represented as Base64Url into binary data. /// /// The input span which contains unicode ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. - /// The number of input chars consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. - /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. - /// (default) when the input span contains the entire data to encode. - /// Set to when the source buffer contains the entirety of the data to encode. - /// Set to if this method is being called in a loop and if more input data may follow. - /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the OperationStatus enum values: - /// - Done - on successful processing of the entire input span - /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input - /// - NeedMoreData - only if is false and the input is not a multiple of 4 - /// - InvalidData - if the input contains bytes outside of the expected Base64Url range, or if it contains invalid/more than two padding characters - /// or if the input is incomplete (i.e. the remainder of % 4 is 1) and is . - /// + /// When this method returns, contains the number of input chars consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// when the input span contains the entirety of data to encode; when more data may follow, + /// such as when calling in a loop, subsequent calls with should end with call. The default is . + /// One of the enumeration values that indicates the success or failure of the operation. /// /// As padding is optional the length not required to be a multiple of 4 even if is . /// If the length is not a multiple of 4 and is the remainders decoded accordingly: - /// Remainder of 3 chars - decoded into 2 bytes data, decoding succeeds. - /// Remainder of 2 chars - decoded into 1 byte data. decoding succeeds. - /// Remainder of 1 char - will cause OperationStatus.InvalidData result. + /// - Remainder of 3 chars - decoded into 2 bytes data, decoding succeeds. + /// - Remainder of 2 chars - decoded into 1 byte data. decoding succeeds. + /// - Remainder of 1 char - will cause OperationStatus.InvalidData result. /// public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span destination, out int charsConsumed, out int bytesWritten, bool isFinalBlock = true) => @@ -293,14 +285,14 @@ private static int GetPaddingCount(ref ushort ptrToLastElement) } /// - /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. + /// Decodes the span of unicode ASCII chars represented as Base64Url into binary data. /// /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. - /// Thrown when the encoded output cannot fit in the provided. + /// The buffer in is too small to hold the encoded output. /// contains a invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. public static int DecodeFromChars(ReadOnlySpan source, Span destination) { OperationStatus status = DecodeFromChars(source, destination, out _, out int bytesWritten); @@ -320,14 +312,14 @@ public static int DecodeFromChars(ReadOnlySpan source, Span destinat } /// - /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. + /// Decodes the span of unicode ASCII chars represented as Base64Url into binary data. /// /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// The output span which contains the result of the operation, i.e. the decoded binary data. - /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if bytes decoded successfully, otherwise . /// contains an invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. public static bool TryDecodeFromChars(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = DecodeFromChars(source, destination, out _, out bytesWritten); @@ -341,12 +333,12 @@ public static bool TryDecodeFromChars(ReadOnlySpan source, Span dest } /// - /// Decode the span of unicode ASCII chars represented as Base64Url into binary data. + /// Decodes the span of unicode ASCII chars represented as Base64Url into binary data. /// /// The input span which contains ASCII chars in Base64Url that needs to be decoded. /// A byte array which contains the result of the decoding operation. /// contains a invalid Base64Url character, - /// more than two padding characters, or a non-white space-character among the padding characters. + /// more than two padding characters, or a non white space character among the padding characters. public static byte[] DecodeFromChars(ReadOnlySpan source) { int upperBound = GetMaxDecodedLength(source.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 68054debbbde65..b03de3dd295709 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -15,22 +15,15 @@ namespace System.Buffers.Text public static partial class Base64Url { /// - /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// Encodes the span of binary data into UTF-8 encoded text represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. - /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. - /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. - /// (default) when the input span contains the entire data to encode. - /// Set to when the source buffer contains the entirety of the data to encode. - /// Set to if this method is being called in a loop and if more input data may follow. - /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the enum values: - /// - Done - on successful processing of the entire input span - /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input - /// - NeedMoreData - only if is - /// It does not return InvalidData since that is not possible for base64 encoding. - /// + /// When this method returns, contains the number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// when the input span contains the entirety of data to encode; when more data may follow, + /// such as when calling in a loop, subsequent calls with should end with call. The default is . + /// One of the enumeration values that indicates the success or failure of the operation. /// The output will not be padded even if the input is not a multiple of 3. public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => @@ -52,12 +45,12 @@ public static int GetEncodedLength(int bytesLength) } /// - /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// Encodes the span of binary data into UTF-8 encoded text represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. - /// Thrown when the encoded output cannot fit in the provided. + /// The buffer in is too small to hold the encoded output. /// The output will not be padded even if the input is not a multiple of 3. public static int EncodeToUtf8(ReadOnlySpan source, Span destination) { @@ -73,7 +66,7 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination } /// - /// Encode the span of binary data into UTF-8 encoded text represented as Base64Url. + /// Encodes the span of binary data into UTF-8 encoded text represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output byte array which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. @@ -88,34 +81,27 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) } /// - /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. + /// Encodes the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. - /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. - /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. - /// (default) when the input span contains the entire data to encode. - /// Set to when the source buffer contains the entirety of the data to encode. - /// Set to if this method is being called in a loop and if more input data may follow. - /// At the end of the loop, call this (potentially with an empty source buffer) passing . - /// It returns the OperationStatus enum values: - /// - Done - on successful processing of the entire input span - /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input - /// - NeedMoreData - only if is - /// It does not return InvalidData since that is not possible for base64 encoding. - /// + /// >When this method returns, contains the number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// >When this method returns, contains the number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. + /// when the input span contains the entirety of data to encode; when more data may follow, + /// such as when calling in a loop, subsequent calls with should end with call. The default is . + /// One of the enumeration values that indicates the success or failure of the operation. /// The output will not be padded even if the input is not a multiple of 3. public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) => EncodeTo(source, MemoryMarshal.Cast(destination), out bytesConsumed, out charsWritten, isFinalBlock); /// - /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. + /// Encodes the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. - /// Thrown when the encoded output cannot fit in the provided. + /// The buffer in is too small to hold the encoded output. /// The output will not be padded even if the input is not a multiple of 3. public static int EncodeToChars(ReadOnlySpan source, Span destination) { @@ -131,7 +117,7 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio } /// - /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. + /// Encodes the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// A char array which contains the result of the operation, i.e. the ASCII chars in Base64Url. @@ -146,7 +132,7 @@ public static char[] EncodeToChars(ReadOnlySpan source) } /// - /// Encode the span of binary data into unicode string represented as Base64Url ASCII chars. + /// Encodes the span of binary data into unicode string represented as Base64Url ASCII chars. /// /// The input span which contains binary data that needs to be encoded. /// A string which contains the result of the operation, i.e. the ASCII string in Base64Url. @@ -166,11 +152,11 @@ public static unsafe string EncodeToString(ReadOnlySpan source) } /// - /// Encode the span of binary data into unicode ASCII chars represented as Base64Url. + /// Encodes the span of binary data into unicode ASCII chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. - /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// When this method returns, contains the number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if chars encoded successfully, otherwise . /// The output will not be padded even if the input is not a multiple of 3. public static bool TryEncodeToChars(ReadOnlySpan source, Span destination, out int charsWritten) @@ -181,11 +167,11 @@ public static bool TryEncodeToChars(ReadOnlySpan source, Span destin } /// - /// Encode the span of binary data into UTF-8 encoded chars represented as Base64Url. + /// Encodes the span of binary data into UTF-8 encoded chars represented as Base64Url. /// /// The input span which contains binary data that needs to be encoded. /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. - /// The number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// When this method returns, contains the number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if bytes encoded successfully, otherwise . /// The output will not be padded even if the input is not a multiple of 3. public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) @@ -196,14 +182,14 @@ public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destina } /// - /// Encode the span of binary data (in-place) into UTF-8 encoded text represented as base 64. + /// Encodes the span of binary data (in-place) into UTF-8 encoded text represented as base 64. /// The encoded text output is larger than the binary data contained in the input (the operation inflates the data). /// /// The input span which contains binary data that needs to be encoded. /// It needs to be large enough to fit the result of the operation. /// The amount of binary data contained within the buffer that needs to be encoded /// (and needs to be smaller than the buffer length). - /// The number of bytes written into the buffer. + /// When this method returns, contains the number of bytes written into the buffer. This parameter is treated as uninitialized. /// if bytes encoded successfully, otherwise . /// The output will not be padded even if the input is not a multiple of 3. public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) From 00139b2192c2281514f9331fb369309c3c6cab85 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 10 Jun 2024 17:27:08 -0700 Subject: [PATCH 33/38] Fix Base64Url fuzzer findings --- .../Base64Url/Base64UrlDecoderUnitTests.cs | 51 ++++++++++++++++++- .../Base64Url/Base64UrlValidationUnitTests.cs | 16 ++++++ .../src/System/Buffers/Text/Base64Decoder.cs | 37 ++++++++++---- .../Text/Base64Url/Base64UrlValidator.cs | 4 +- 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs index b98d7ce8448894..23a16f6bb07a37 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlDecoderUnitTests.cs @@ -47,7 +47,7 @@ public void BasicDecodingByteArrayReturnOverload() Span source = new byte[numBytes]; Base64TestHelper.InitializeUrlDecodableBytes(source, numBytes); - Span decodedBytes = Base64Url.DecodeFromUtf8(source);; + Span decodedBytes = Base64Url.DecodeFromUtf8(source); Assert.Equal(decodedBytes.Length, Base64Url.GetMaxDecodedLength(source.Length)); Assert.True(Base64TestHelper.VerifyUrlDecodingCorrectness(source.Length, decodedBytes.Length, source, decodedBytes)); } @@ -774,6 +774,55 @@ public void DecodingInPlaceWithOnlyCharsToBeIgnored(string utf8WithCharsToBeIgno Assert.Equal(0, bytesWritten); } + [Theory] + [InlineData(new byte[] { 0xa, 0xa, 0x2d, 0x2d }, 251)] + [InlineData(new byte[] { 0xa, 0x5f, 0xa, 0x2d }, 255)] + [InlineData(new byte[] { 0x5f, 0x5f, 0xa, 0xa }, 255)] + [InlineData(new byte[] { 0x70, 0xa, 0x61, 0xa }, 165)] + [InlineData(new byte[] { 0xa, 0x70, 0xa, 0x61, 0xa }, 165)] + [InlineData(new byte[] { 0x70, 0xa, 0x61, 0xa, 0x3d, 0x3d }, 165)] + public void DecodingLessThan4BytesWithWhiteSpaces(byte[] utf8Bytes, byte decoded) + { + Assert.True(Base64Url.IsValid(utf8Bytes, out int decodedLength)); + Assert.Equal(1, decodedLength); + Span decodedSpan = new byte[decodedLength]; + OperationStatus status = Base64Url.DecodeFromUtf8(utf8Bytes, decodedSpan, out int bytesRead, out int bytesDecoded); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(utf8Bytes.Length, bytesRead); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, decodedSpan[0]); + decodedSpan.Clear(); + Assert.True(Base64Url.TryDecodeFromUtf8(utf8Bytes, decodedSpan, out bytesDecoded)); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, decodedSpan[0]); + + bytesDecoded = Base64Url.DecodeFromUtf8InPlace(utf8Bytes); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, utf8Bytes[0]); + } + + [Theory] + [InlineData(new byte[] { 0x4a, 0x74, 0xa, 0x4a, 0x4a, 0x74, 0xa, 0x4a }, new byte[] { 38, 210, 73, 180 })] + [InlineData(new byte[] { 0xa, 0x2d, 0x56, 0xa, 0xa, 0xa, 0x2d, 0x4a, 0x4a, 0x4a, }, new byte[] { 249, 95, 137, 36 })] + public void DecodingNotMultipleOf4WithWhiteSpace(byte[] utf8Bytes, byte[] decoded) + { + Assert.True(Base64Url.IsValid(utf8Bytes, out int decodedLength)); + Assert.Equal(4, decodedLength); + Span decodedSpan = new byte[decodedLength]; + OperationStatus status = Base64Url.DecodeFromUtf8(utf8Bytes, decodedSpan, out int bytesRead, out int bytesDecoded); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(utf8Bytes.Length, bytesRead); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, decodedSpan); + decodedSpan.Clear(); + Assert.True(Base64Url.TryDecodeFromUtf8(utf8Bytes, decodedSpan, out bytesDecoded)); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, decodedSpan); + bytesDecoded = Base64Url.DecodeFromUtf8InPlace(utf8Bytes); + Assert.Equal(decodedLength, bytesDecoded); + Assert.Equal(decoded, utf8Bytes.AsSpan().Slice(0, bytesDecoded)); + } + [Theory] [MemberData(nameof(BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes_MemberData))] public void BasicDecodingWithExtraWhitespaceShouldBeCountedInConsumedBytes(string inputString, int expectedConsumed, int expectedWritten) diff --git a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs index a415d0098cfe3a..b2cb650fa6e3f6 100644 --- a/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs +++ b/src/libraries/System.Memory/tests/Base64Url/Base64UrlValidationUnitTests.cs @@ -9,6 +9,22 @@ namespace System.Buffers.Text.Tests { public class Base64UrlValidationUnitTests : Base64TestBase { + [Theory] + [InlineData("==")] + [InlineData("-%")] + [InlineData("A=")] + [InlineData("A==")] + [InlineData("4%%")] + [InlineData(" A==")] + [InlineData("AAAAA ==")] + [InlineData("\tLLLL\t=\r")] + [InlineData("6066=")] + public void BasicValidationEdgeCaseScenario(string base64UrlText) + { + Assert.False(Base64Url.IsValid(base64UrlText, out int decodedLength)); + Assert.Equal(0, decodedLength); + } + [Fact] public void BasicValidationBytes() { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index fa4f3d3bc0e8ad..ace20aac21e58d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -526,7 +526,17 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re continue; } - bool hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + bool hasAnotherBlock; + + if (typeof(TBase64Decoder) == typeof(Base64DecoderByte) || bufferIdx == 1) + { + hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + } + else + { + hasAnotherBlock = utf8.Length > 1; + } + bool localIsFinalBlock = !hasAnotherBlock; // If this block contains padding and there's another block, then only whitespace may follow for being valid. @@ -595,7 +605,7 @@ private static int GetPaddingCount(ref byte ptrToLastElement) private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) where TBase64Decoder : IBase64Decoder { - const int BlockSize = 4; + int BlockSize = Math.Min(utf8.Length - (int)sourceIndex, 4); Span buffer = stackalloc byte[BlockSize]; OperationStatus status = OperationStatus.Done; @@ -607,13 +617,8 @@ private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace= (uint)utf8.Length) // TODO https://github.com/dotnet/runtime/issues/83349: move into the while condition once fixed - { - break; - } - if (!IsWhiteSpace(utf8[(int)sourceIndex])) { buffer[bufferIdx] = utf8[(int)sourceIndex]; @@ -630,8 +635,20 @@ private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace 1 && (remainder - paddingCount == 1 || paddingCount == remainder)) { decodedLength = 0; return false; From dea9006ff953b2c42cadf5d0bcd31fcc2ec1e757 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 10 Jun 2024 21:23:45 -0700 Subject: [PATCH 34/38] Apply suggestions from code review Co-authored-by: Jeremy Barton --- .../Text/Base64Url/Base64UrlDecoder.cs | 19 ++++++++++++++----- .../Text/Base64Url/Base64UrlEncoder.cs | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index e4f6904ae38bde..60ae1e32789bed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -20,7 +20,7 @@ public static partial class Base64Url private const int MaxStackallocThreshold = 256; /// - /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text within a byte span of size . + /// Returns the maximum length (in bytes) of the result if you were to decode base 64 encoded text from a span of size . /// /// The specified is less than 0. /// @@ -133,6 +133,7 @@ public static bool TryDecodeFromUtf8(ReadOnlySpan source, Span desti throw new FormatException(SR.Format_BadBase64Char); } + Debug.Assert(status is OperationStatus.Done or OperationStatus.DestinationTooSmall); return status == OperationStatus.Done; } @@ -153,6 +154,7 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) : (rented = ArrayPool.Shared.Rent(upperBound)); OperationStatus status = DecodeFromUtf8(source, destination, out _, out int bytesWritten); + Debug.Assert(status is OperationStatus.Done or OperationStatus.InvalidData); byte[] ret = destination.Slice(0, bytesWritten).ToArray(); if (rented is not null) @@ -171,7 +173,7 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) /// When this method returns, contains the number of input chars consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// when the input span contains the entirety of data to encode; when more data may follow, - /// such as when calling in a loop, subsequent calls with should end with call. The default is . + /// such as when calling in a loop. Calls with should be followed up with another call where this parameter is call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. /// /// As padding is optional the length not required to be a multiple of 4 even if is . @@ -278,8 +280,15 @@ private static int GetPaddingCount(ref ushort ptrToLastElement) { int padding = 0; - if (TBase64Decoder.IsValidPadding(ptrToLastElement)) padding++; - if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) padding++; + if (TBase64Decoder.IsValidPadding(ptrToLastElement)) + { + padding++; + } + + if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) + { + padding++; + } return padding; } @@ -447,7 +456,7 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); - public static bool IsInvalidLength(int bufferLength) => bufferLength % 4 == 1; // One byte cannot be decoded completely + public static bool IsInvalidLength(int bufferLength) => (bufferLength & 3) == 1; // One byte cannot be decoded completely public static bool IsValidPadding(uint padChar) => padChar == EncodingPad || padChar == UrlEncodingPad; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index b03de3dd295709..390da1a3e9aa72 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -24,7 +24,7 @@ public static partial class Base64Url /// when the input span contains the entirety of data to encode; when more data may follow, /// such as when calling in a loop, subsequent calls with should end with call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => EncodeTo(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); @@ -33,7 +33,7 @@ public static unsafe OperationStatus EncodeToUtf8(ReadOnlySpan source, /// Returns the length (in bytes) of the result if you were to encode binary data within a byte span of size . /// /// - /// Thrown when the specified is less than 0 or larger than 1610612733 (since encode inflates the data by 4/3). + /// is less than 0 or greater than 1610612733. /// public static int GetEncodedLength(int bytesLength) { From e617b8f47390727e6c211797051eac63a70881f5 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 10 Jun 2024 22:12:45 -0700 Subject: [PATCH 35/38] Rename utf8 -> source/destintion --- .../src/System/Buffers/Text/Base64.cs | 6 +- .../src/System/Buffers/Text/Base64Decoder.cs | 63 ++++++++++--------- .../src/System/Buffers/Text/Base64Encoder.cs | 14 ++--- .../Text/Base64Url/Base64UrlDecoder.cs | 30 ++++----- 4 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index eacb54b9a326ad..ef3bcc96dd1881 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -101,8 +101,8 @@ internal interface IBase64Decoder where T : unmanaged static abstract ReadOnlySpan Vector128LutShift { get; } static abstract ReadOnlySpan AdvSimdLutOne3 { get; } static abstract uint AdvSimdLutTwo3Uint1 { get; } - static abstract int SrcLength(bool isFinalBlock, int utf8Length); - static abstract int GetMaxDecodedLength(int utf8Length); + static abstract int SrcLength(bool isFinalBlock, int sourceLength); + static abstract int GetMaxDecodedLength(int sourceLength); static abstract bool IsInvalidLength(int bufferLength); static abstract bool IsValidPadding(uint padChar); static abstract bool TryDecode128Core( @@ -127,7 +127,7 @@ static abstract bool TryDecode256Core( static abstract unsafe int Decode(T* encodedBytes, ref sbyte decodingMap); static abstract unsafe int DecodeRemaining(T* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3); static abstract int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span); - static abstract OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, + static abstract OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan source, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TTBase64Decoder : IBase64Decoder; static abstract unsafe bool TryLoadVector512(T* src, T* srcStart, int sourceLength, out Vector512 str); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index ace20aac21e58d..967445f58e055d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -37,22 +37,22 @@ public static partial class Base64 public static OperationStatus DecodeFromUtf8(ReadOnlySpan utf8, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => DecodeFrom(utf8, bytes, out bytesConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - internal static unsafe OperationStatus DecodeFrom(ReadOnlySpan utf8, Span bytes, + internal static unsafe OperationStatus DecodeFrom(ReadOnlySpan source, Span bytes, out int bytesConsumed, out int bytesWritten, bool isFinalBlock, bool ignoreWhiteSpace) where TBase64Decoder : IBase64Decoder where T : unmanaged { - if (utf8.IsEmpty) + if (source.IsEmpty) { bytesConsumed = 0; bytesWritten = 0; return OperationStatus.Done; } - fixed (T* srcBytes = &MemoryMarshal.GetReference(utf8)) + fixed (T* srcBytes = &MemoryMarshal.GetReference(source)) fixed (byte* destBytes = &MemoryMarshal.GetReference(bytes)) { - int srcLength = TBase64Decoder.SrcLength(isFinalBlock, utf8.Length); + int srcLength = TBase64Decoder.SrcLength(isFinalBlock, source.Length); int destLength = bytes.Length; int maxSrcLength = srcLength; int decodedLength = TBase64Decoder.GetMaxDecodedLength(srcLength); @@ -166,7 +166,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa goto InvalidDataExit; } - if (src == srcBytes + utf8.Length) + if (src == srcBytes + source.Length) { goto DoneExit; } @@ -244,7 +244,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa src += remaining; } - if (srcLength != utf8.Length) + if (srcLength != source.Length) { goto InvalidDataExit; } @@ -255,7 +255,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa return OperationStatus.Done; DestinationTooSmallExit: - if (srcLength != utf8.Length && isFinalBlock) + if (srcLength != source.Length && isFinalBlock) { goto InvalidDataExit; // if input is not a multiple of 4, and there is no more data, return invalid data instead } @@ -273,7 +273,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa bytesConsumed = (int)(src - srcBytes); bytesWritten = (int)(dest - destBytes); return ignoreWhiteSpace ? - InvalidDataFallback(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock) : + InvalidDataFallback(source, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock) : OperationStatus.InvalidData; } @@ -492,33 +492,33 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa } } - internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + internal static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan source, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc byte[BlockSize]; OperationStatus status = OperationStatus.Done; - while (!utf8.IsEmpty) + while (!source.IsEmpty) { int encodedIdx = 0; int bufferIdx = 0; int skipped = 0; - for (; encodedIdx < utf8.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) + for (; encodedIdx < source.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) { - if (IsWhiteSpace(utf8[encodedIdx])) + if (IsWhiteSpace(source[encodedIdx])) { skipped++; } else { - buffer[bufferIdx] = utf8[encodedIdx]; + buffer[bufferIdx] = source[encodedIdx]; bufferIdx++; } } - utf8 = utf8.Slice(encodedIdx); + source = source.Slice(encodedIdx); bytesConsumed += skipped; if (bufferIdx == 0) @@ -530,11 +530,11 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re if (typeof(TBase64Decoder) == typeof(Base64DecoderByte) || bufferIdx == 1) { - hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + hasAnotherBlock = source.Length >= BlockSize && bufferIdx == BlockSize; } else { - hasAnotherBlock = utf8.Length > 1; + hasAnotherBlock = source.Length > 1; } bool localIsFinalBlock = !hasAnotherBlock; @@ -567,9 +567,9 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re // The remaining data must all be whitespace in order to be valid. if (!hasAnotherBlock) { - for (int i = 0; i < utf8.Length; ++i) + for (int i = 0; i < source.Length; ++i) { - if (!IsWhiteSpace(utf8[i])) + if (!IsWhiteSpace(source[i])) { // Revert previous dest increment, since an invalid state followed. bytesConsumed -= localConsumed; @@ -596,16 +596,23 @@ private static int GetPaddingCount(ref byte ptrToLastElement) { int padding = 0; - if (TBase64Decoder.IsValidPadding(ptrToLastElement)) padding++; - if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) padding++; + if (TBase64Decoder.IsValidPadding(ptrToLastElement)) + { + padding++; + } + + if (TBase64Decoder.IsValidPadding(Unsafe.Subtract(ref ptrToLastElement, 1))) + { + padding++; + } return padding; } - private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span utf8, ref int destIndex, uint sourceIndex) + private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace(Span source, ref int destIndex, uint sourceIndex) where TBase64Decoder : IBase64Decoder { - int BlockSize = Math.Min(utf8.Length - (int)sourceIndex, 4); + int BlockSize = Math.Min(source.Length - (int)sourceIndex, 4); Span buffer = stackalloc byte[BlockSize]; OperationStatus status = OperationStatus.Done; @@ -613,15 +620,15 @@ private static OperationStatus DecodeWithWhiteSpaceFromUtf8InPlace bytes, Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) => EncodeTo(bytes, utf8, out bytesConsumed, out bytesWritten, isFinalBlock); - internal static unsafe OperationStatus EncodeTo(ReadOnlySpan bytes, - Span utf8, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + internal static unsafe OperationStatus EncodeTo(ReadOnlySpan source, + Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) where TBase64Encoder : IBase64Encoder where T : unmanaged { - if (bytes.IsEmpty) + if (source.IsEmpty) { bytesConsumed = 0; bytesWritten = 0; return OperationStatus.Done; } - fixed (byte* srcBytes = &MemoryMarshal.GetReference(bytes)) - fixed (T* destBytes = &MemoryMarshal.GetReference(utf8)) + fixed (byte* srcBytes = &MemoryMarshal.GetReference(source)) + fixed (T* destBytes = &MemoryMarshal.GetReference(destination)) { - int srcLength = bytes.Length; - int destLength = utf8.Length; + int srcLength = source.Length; + int destLength = destination.Length; int maxSrcLength = TBase64Encoder.GetMaxSrcLength(srcLength, destLength); byte* src = srcBytes; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 60ae1e32789bed..52ea3160e1730b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -186,33 +186,33 @@ public static OperationStatus DecodeFromChars(ReadOnlySpan source, Span DecodeFrom(MemoryMarshal.Cast(source), destination, out charsConsumed, out bytesWritten, isFinalBlock, ignoreWhiteSpace: true); - private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) + private static OperationStatus DecodeWithWhiteSpaceBlockwise(ReadOnlySpan source, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder { const int BlockSize = 4; Span buffer = stackalloc ushort[BlockSize]; OperationStatus status = OperationStatus.Done; - while (!utf8.IsEmpty) + while (!source.IsEmpty) { int encodedIdx = 0; int bufferIdx = 0; int skipped = 0; - for (; encodedIdx < utf8.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) + for (; encodedIdx < source.Length && (uint)bufferIdx < (uint)buffer.Length; ++encodedIdx) { - if (IsWhiteSpace(utf8[encodedIdx])) + if (IsWhiteSpace(source[encodedIdx])) { skipped++; } else { - buffer[bufferIdx] = utf8[encodedIdx]; + buffer[bufferIdx] = source[encodedIdx]; bufferIdx++; } } - utf8 = utf8.Slice(encodedIdx); + source = source.Slice(encodedIdx); bytesConsumed += skipped; if (bufferIdx == 0) @@ -220,7 +220,7 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(Rea continue; } - bool hasAnotherBlock = utf8.Length >= BlockSize && bufferIdx == BlockSize; + bool hasAnotherBlock = source.Length >= BlockSize && bufferIdx == BlockSize; bool localIsFinalBlock = !hasAnotherBlock; // If this block contains padding and there's another block, then only whitespace may follow for being valid. @@ -251,9 +251,9 @@ private static OperationStatus DecodeWithWhiteSpaceBlockwise(Rea // The remaining data must all be whitespace in order to be valid. if (!hasAnotherBlock) { - for (int i = 0; i < utf8.Length; ++i) + for (int i = 0; i < source.Length; ++i) { - if (!IsWhiteSpace(utf8[i])) + if (!IsWhiteSpace(source[i])) { // Revert previous dest increment, since an invalid state followed. bytesConsumed -= localConsumed; @@ -454,13 +454,13 @@ public static byte[] DecodeFromChars(ReadOnlySpan source) public static uint AdvSimdLutTwo3Uint1 => 0x1B1AFF3F; - public static int GetMaxDecodedLength(int utf8Length) => Base64Url.GetMaxDecodedLength(utf8Length); + public static int GetMaxDecodedLength(int sourceLength) => Base64Url.GetMaxDecodedLength(sourceLength); public static bool IsInvalidLength(int bufferLength) => (bufferLength & 3) == 1; // One byte cannot be decoded completely public static bool IsValidPadding(uint padChar) => padChar == EncodingPad || padChar == UrlEncodingPad; - public static int SrcLength(bool isFinalBlock, int utf8Length) => isFinalBlock ? utf8Length : utf8Length & ~0x3; + public static int SrcLength(bool isFinalBlock, int sourceLength) => isFinalBlock ? sourceLength : sourceLength & ~0x3; [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] @@ -596,13 +596,13 @@ public static unsafe bool TryLoadArmVector128x4(byte* src, byte* srcStart, int s public static uint AdvSimdLutTwo3Uint1 => Base64UrlDecoderByte.AdvSimdLutTwo3Uint1; - public static int GetMaxDecodedLength(int utf8Length) => Base64UrlDecoderByte.GetMaxDecodedLength(utf8Length); + public static int GetMaxDecodedLength(int sourceLength) => Base64UrlDecoderByte.GetMaxDecodedLength(sourceLength); public static bool IsInvalidLength(int bufferLength) => Base64DecoderByte.IsInvalidLength(bufferLength); public static bool IsValidPadding(uint padChar) => Base64UrlDecoderByte.IsValidPadding(padChar); - public static int SrcLength(bool isFinalBlock, int utf8Length) => Base64UrlDecoderByte.SrcLength(isFinalBlock, utf8Length); + public static int SrcLength(bool isFinalBlock, int sourceLength) => Base64UrlDecoderByte.SrcLength(isFinalBlock, sourceLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] @@ -704,9 +704,9 @@ public static int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan utf8, Span bytes, + public static OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan source, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock = true) where TBase64Decoder : IBase64Decoder => - DecodeWithWhiteSpaceBlockwise(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + DecodeWithWhiteSpaceBlockwise(source, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool TryLoadVector512(ushort* src, ushort* srcStart, int sourceLength, out Vector512 str) From c3ff207d342b09ae3fff3d8f8a32c37ca2469dad Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Tue, 11 Jun 2024 23:26:01 -0700 Subject: [PATCH 36/38] Apply feedbacks --- .../src/System/Buffers/Text/Base64.cs | 2 +- .../src/System/Buffers/Text/Base64Decoder.cs | 39 ++++--------------- .../Text/Base64Url/Base64UrlDecoder.cs | 15 +++---- .../Text/Base64Url/Base64UrlValidator.cs | 2 +- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs index ef3bcc96dd1881..c362220d308049 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64.cs @@ -124,7 +124,7 @@ static abstract bool TryDecode256Core( Vector256 lutShift, Vector256 shiftForUnderscore, out Vector256 result); - static abstract unsafe int Decode(T* encodedBytes, ref sbyte decodingMap); + static abstract unsafe int DecodeFourElements(T* source, ref sbyte decodingMap); static abstract unsafe int DecodeRemaining(T* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3); static abstract int IndexOfAnyExceptWhiteSpace(ReadOnlySpan span); static abstract OperationStatus DecodeWithWhiteSpaceBlockwiseWrapper(ReadOnlySpan source, diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 967445f58e055d..89d851cbe676d4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -142,7 +142,7 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa while (src < srcMax) { - int result = TBase64Decoder.Decode(src, ref decodingMap); + int result = TBase64Decoder.DecodeFourElements(src, ref decodingMap); if (result < 0) { @@ -386,7 +386,7 @@ internal static unsafe OperationStatus DecodeFromUtf8InPlace(Spa { while (sourceIndex < bufferLength - 4) { - int result = Decode(bufferBytes + sourceIndex, ref decodingMap); + int result = Base64DecoderByte.DecodeFourElements(bufferBytes + sourceIndex, ref decodingMap); if (result < 0) { goto InvalidExit; @@ -1182,30 +1182,6 @@ private static unsafe void Vector128Decode(ref T* srcBytes, r destBytes = dest; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) - { - uint t0 = encodedBytes[0]; - uint t1 = encodedBytes[1]; - uint t2 = encodedBytes[2]; - uint t3 = encodedBytes[3]; - - int i0 = Unsafe.Add(ref decodingMap, t0); - int i1 = Unsafe.Add(ref decodingMap, t1); - int i2 = Unsafe.Add(ref decodingMap, t2); - int i3 = Unsafe.Add(ref decodingMap, t3); - - i0 <<= 18; - i1 <<= 12; - i2 <<= 6; - - i0 |= i3; - i1 |= i2; - - i0 |= i1; - return i0; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void WriteThreeLowOrderBytes(byte* destination, int value) { @@ -1398,12 +1374,13 @@ public static bool TryDecode256Core( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) + public static unsafe int DecodeFourElements(byte* source, ref sbyte decodingMap) { - uint t0 = encodedBytes[0]; - uint t1 = encodedBytes[1]; - uint t2 = encodedBytes[2]; - uint t3 = encodedBytes[3]; + // The 'source' span expected to have at least 4 elements, and the 'decodingMap' consists 256 sbytes + uint t0 = source[0]; + uint t1 = source[1]; + uint t2 = source[2]; + uint t3 = source[3]; int i0 = Unsafe.Add(ref decodingMap, t0); int i1 = Unsafe.Add(ref decodingMap, t1); diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index 52ea3160e1730b..f8b5ce605a0265 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -535,8 +535,8 @@ public static bool TryDecode256Core( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Decode(byte* encodedBytes, ref sbyte decodingMap) => - Base64DecoderByte.Decode(encodedBytes, ref decodingMap); + public static unsafe int DecodeFourElements(byte* source, ref sbyte decodingMap) => + Base64DecoderByte.DecodeFourElements(source, ref decodingMap); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe int DecodeRemaining(byte* srcEnd, ref sbyte decodingMap, long remaining, out uint t2, out uint t3) @@ -618,12 +618,13 @@ public static bool TryDecode256Core(Vector256 str, Vector256 hiNib Base64UrlDecoderByte.TryDecode256Core(str, hiNibbles, maskSlashOrUnderscore, lutLow, lutHigh, lutShift, shiftForUnderscore, out result); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Decode(ushort* encodedBytes, ref sbyte decodingMap) + public static unsafe int DecodeFourElements(ushort* source, ref sbyte decodingMap) { - uint t0 = encodedBytes[0]; - uint t1 = encodedBytes[1]; - uint t2 = encodedBytes[2]; - uint t3 = encodedBytes[3]; + // The 'source' span expected to have at least 4 elements, and the 'decodingMap' consists 256 sbytes + uint t0 = source[0]; + uint t1 = source[1]; + uint t2 = source[2]; + uint t3 = source[3]; if (((t0 | t1 | t2 | t3) & 0xffffff00) != 0) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs index e0005fb2d15bf9..20eb0e926d62aa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlValidator.cs @@ -77,7 +77,7 @@ public static bool ValidateAndDecodeLength(int length, int paddingCount, out int { // Padding is optional for Base64Url, so need to account remainder. If remainder is 1, then it's invalid. (uint whole, uint remainder) = uint.DivRem((uint)(length), 4); - if (remainder == 1 || remainder > 1 && (remainder - paddingCount == 1 || paddingCount == remainder)) + if (remainder == 1 || (remainder > 1 && (remainder - paddingCount == 1 || paddingCount == remainder))) { decodedLength = 0; return false; From 154b4003974ea471e84f01d0f225c783beaef06d Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 12 Jun 2024 15:56:23 -0700 Subject: [PATCH 37/38] Apply feedback --- .../src/System/Buffers/Text/Base64Decoder.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs index 89d851cbe676d4..9d3f35b3a00382 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Decoder.cs @@ -277,20 +277,20 @@ internal static unsafe OperationStatus DecodeFrom(ReadOnlySpa OperationStatus.InvalidData; } - static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) + static OperationStatus InvalidDataFallback(ReadOnlySpan source, Span bytes, ref int bytesConsumed, ref int bytesWritten, bool isFinalBlock) { - utf8 = utf8.Slice(bytesConsumed); + source = source.Slice(bytesConsumed); bytes = bytes.Slice(bytesWritten); OperationStatus status; do { - int localConsumed = TBase64Decoder.IndexOfAnyExceptWhiteSpace(utf8); + int localConsumed = TBase64Decoder.IndexOfAnyExceptWhiteSpace(source); if (localConsumed < 0) { // The remainder of the input is all whitespace. Mark it all as having been consumed, // and mark the operation as being done. - bytesConsumed += utf8.Length; + bytesConsumed += source.Length; status = OperationStatus.Done; break; } @@ -303,15 +303,15 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span byte // Fall back to block-wise decoding. This is very slow, but it's also very non-standard // formatting of the input; whitespace is typically only found between blocks, such as // when Convert.ToBase64String inserts a line break every 76 output characters. - return TBase64Decoder.DecodeWithWhiteSpaceBlockwiseWrapper(utf8, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); + return TBase64Decoder.DecodeWithWhiteSpaceBlockwiseWrapper(source, bytes, ref bytesConsumed, ref bytesWritten, isFinalBlock); } // Skip over the starting whitespace and continue. bytesConsumed += localConsumed; - utf8 = utf8.Slice(localConsumed); + source = source.Slice(localConsumed); // Try again after consumed whitespace - status = DecodeFrom(utf8, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); + status = DecodeFrom(source, bytes, out localConsumed, out int localWritten, isFinalBlock, ignoreWhiteSpace: false); bytesConsumed += localConsumed; bytesWritten += localWritten; if (status is not OperationStatus.InvalidData) @@ -319,10 +319,10 @@ static OperationStatus InvalidDataFallback(ReadOnlySpan utf8, Span byte break; } - utf8 = utf8.Slice(localConsumed); + source = source.Slice(localConsumed); bytes = bytes.Slice(localWritten); } - while (!utf8.IsEmpty); + while (!source.IsEmpty); return status; } @@ -528,9 +528,9 @@ internal static OperationStatus DecodeWithWhiteSpaceBlockwise(Re bool hasAnotherBlock; - if (typeof(TBase64Decoder) == typeof(Base64DecoderByte) || bufferIdx == 1) + if (typeof(TBase64Decoder) == typeof(Base64DecoderByte)) { - hasAnotherBlock = source.Length >= BlockSize && bufferIdx == BlockSize; + hasAnotherBlock = source.Length >= BlockSize; } else { From 4ccdde82bdef81b5a0f154cc21d7e90506485786 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Thu, 13 Jun 2024 11:40:38 -0700 Subject: [PATCH 38/38] Apply left out feedbacks --- .../Text/Base64Url/Base64UrlDecoder.cs | 9 +++++---- .../Text/Base64Url/Base64UrlEncoder.cs | 19 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs index f8b5ce605a0265..6e7d97f79426c5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlDecoder.cs @@ -8,7 +8,6 @@ using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; using System.Text; -using System.Text.Unicode; using static System.Buffers.Text.Base64; namespace System.Buffers.Text @@ -41,7 +40,7 @@ public static int GetMaxDecodedLength(int base64Length) /// When this method returns, contains the number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. This parameter is treated as uninitialized. /// When this method returns, contains the number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// when the input span contains the entirety of data to encode; when more data may follow, - /// such as when calling in a loop, subsequent calls with should end with call. The default is . + /// such as when calling in a loop. Calls with should be followed up with another call where this parameter is call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. /// /// As padding is optional for Base64Url the length not required to be a multiple of 4 even if is . @@ -79,6 +78,7 @@ public static int DecodeFromUtf8InPlace(Span buffer) throw new FormatException(SR.Format_BadBase64Char); } + Debug.Assert(status is OperationStatus.Done); return bytesWritten; } @@ -92,7 +92,7 @@ public static int DecodeFromUtf8InPlace(Span buffer) /// contains an invalid Base64Url character, /// more than two padding characters, or a non white space character among the padding characters. /// - /// As padding is optional the length not required to be a multiple of 4. + /// As padding is optional for Base64Url the length not required to be a multiple of 4. /// If the length is not a multiple of 4 the remainders decoded accordingly: /// - Remainder of 3 bytes - decoded into 2 bytes data, decoding succeeds. /// - Remainder of 2 bytes - decoded into 1 byte data. decoding succeeds. @@ -112,6 +112,7 @@ public static int DecodeFromUtf8(ReadOnlySpan source, Span destinati throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); } + Debug.Assert(status is OperationStatus.InvalidData); throw new FormatException(SR.Format_BadBase64Char); } @@ -176,7 +177,7 @@ public static byte[] DecodeFromUtf8(ReadOnlySpan source) /// such as when calling in a loop. Calls with should be followed up with another call where this parameter is call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. /// - /// As padding is optional the length not required to be a multiple of 4 even if is . + /// As padding is optional for Base64Url the length not required to be a multiple of 4 even if is . /// If the length is not a multiple of 4 and is the remainders decoded accordingly: /// - Remainder of 3 chars - decoded into 2 bytes data, decoding succeeds. /// - Remainder of 2 chars - decoded into 1 byte data. decoding succeeds. diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs index 390da1a3e9aa72..13d210638fee37 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Url/Base64UrlEncoder.cs @@ -7,7 +7,6 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; -using System.Text; using static System.Buffers.Text.Base64; namespace System.Buffers.Text @@ -51,7 +50,7 @@ public static int GetEncodedLength(int bytesLength) /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. /// The buffer in is too small to hold the encoded output. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static int EncodeToUtf8(ReadOnlySpan source, Span destination) { OperationStatus status = EncodeToUtf8(source, destination, out _, out int bytesWritten); @@ -70,7 +69,7 @@ public static int EncodeToUtf8(ReadOnlySpan source, Span destination /// /// The input span which contains binary data that needs to be encoded. /// The output byte array which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static byte[] EncodeToUtf8(ReadOnlySpan source) { byte[] destination = new byte[GetEncodedLength(source.Length)]; @@ -90,7 +89,7 @@ public static byte[] EncodeToUtf8(ReadOnlySpan source) /// when the input span contains the entirety of data to encode; when more data may follow, /// such as when calling in a loop, subsequent calls with should end with call. The default is . /// One of the enumeration values that indicates the success or failure of the operation. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static OperationStatus EncodeToChars(ReadOnlySpan source, Span destination, out int bytesConsumed, out int charsWritten, bool isFinalBlock = true) => EncodeTo(source, MemoryMarshal.Cast(destination), out bytesConsumed, out charsWritten, isFinalBlock); @@ -102,7 +101,7 @@ public static OperationStatus EncodeToChars(ReadOnlySpan source, SpanThe output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// The number of bytes written into the destination span. This can be used to slice the output for subsequent calls, if necessary. /// The buffer in is too small to hold the encoded output. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static int EncodeToChars(ReadOnlySpan source, Span destination) { OperationStatus status = EncodeToChars(source, destination, out _, out int charsWritten); @@ -121,7 +120,7 @@ public static int EncodeToChars(ReadOnlySpan source, Span destinatio /// /// The input span which contains binary data that needs to be encoded. /// A char array which contains the result of the operation, i.e. the ASCII chars in Base64Url. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static char[] EncodeToChars(ReadOnlySpan source) { char[] destination = new char[GetEncodedLength(source.Length)]; @@ -136,7 +135,7 @@ public static char[] EncodeToChars(ReadOnlySpan source) /// /// The input span which contains binary data that needs to be encoded. /// A string which contains the result of the operation, i.e. the ASCII string in Base64Url. - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static unsafe string EncodeToString(ReadOnlySpan source) { int encodedLength = GetEncodedLength(source.Length); @@ -158,7 +157,7 @@ public static unsafe string EncodeToString(ReadOnlySpan source) /// The output span which contains the result of the operation, i.e. the ASCII chars in Base64Url. /// When this method returns, contains the number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if chars encoded successfully, otherwise . - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static bool TryEncodeToChars(ReadOnlySpan source, Span destination, out int charsWritten) { OperationStatus status = EncodeToChars(source, destination, out _, out charsWritten); @@ -173,7 +172,7 @@ public static bool TryEncodeToChars(ReadOnlySpan source, Span destin /// The output span which contains the result of the operation, i.e. the UTF-8 encoded text in Base64Url. /// When this method returns, contains the number of chars written into the output span. This can be used to slice the output for subsequent calls, if necessary. This parameter is treated as uninitialized. /// if bytes encoded successfully, otherwise . - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destination, out int bytesWritten) { OperationStatus status = EncodeToUtf8(source, destination, out _, out bytesWritten); @@ -191,7 +190,7 @@ public static bool TryEncodeToUtf8(ReadOnlySpan source, Span destina /// (and needs to be smaller than the buffer length). /// When this method returns, contains the number of bytes written into the buffer. This parameter is treated as uninitialized. /// if bytes encoded successfully, otherwise . - /// The output will not be padded even if the input is not a multiple of 3. + /// This implementation of the base64url encoding omits the optional padding characters. public static unsafe bool TryEncodeToUtf8InPlace(Span buffer, int dataLength, out int bytesWritten) { OperationStatus status = EncodeToUtf8InPlace(buffer, dataLength, out bytesWritten);