diff --git a/sdk/src/Core/AWSSDK.Core.NetFramework.csproj b/sdk/src/Core/AWSSDK.Core.NetFramework.csproj index 3d740253846d..0c065257520a 100644 --- a/sdk/src/Core/AWSSDK.Core.NetFramework.csproj +++ b/sdk/src/Core/AWSSDK.Core.NetFramework.csproj @@ -21,6 +21,8 @@ $(NoWarn);CS1591 True + + true diff --git a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj index ca3d2cd81cb8..2e9e23b042d0 100644 --- a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj +++ b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj @@ -21,8 +21,8 @@ false $(NoWarn);CS1591;CA1822 - true - True + true + True true diff --git a/sdk/src/Core/Amazon.Runtime/Internal/MissingTypes/SkipLocalsInitAttribute.cs b/sdk/src/Core/Amazon.Runtime/Internal/MissingTypes/SkipLocalsInitAttribute.cs new file mode 100644 index 000000000000..dce5b822e352 --- /dev/null +++ b/sdk/src/Core/Amazon.Runtime/Internal/MissingTypes/SkipLocalsInitAttribute.cs @@ -0,0 +1,10 @@ +#if !NET8_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + /// Indicates to the compiler that the .locals init flag should not be set in nested method headers when emitting to metadata. + [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] + internal sealed class SkipLocalsInitAttribute : Attribute + { + } +} +#endif \ No newline at end of file diff --git a/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs b/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs index 5bce4abb050a..64ae04b6fd07 100644 --- a/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs +++ b/sdk/src/Core/Amazon.Util/AWSSDKUtils.cs @@ -33,6 +33,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using Amazon.Runtime.Endpoints; using ThirdParty.RuntimeBackports; @@ -79,12 +80,6 @@ public static partial class AWSSDKUtils // Default value of progress update interval for streaming is 100KB. public const long DefaultProgressUpdateInterval = 102400; - internal static Dictionary RFCEncodingSchemes = new Dictionary - { - { 3986, ValidUrlCharacters }, - { 1738, ValidUrlCharactersRFC1738 } - }; - internal const string S3Accelerate = "s3-accelerate"; internal const string S3Control = "s3-control"; @@ -1028,35 +1023,76 @@ public static string UrlEncode(string data, bool path) /// Currently recognised RFC versions are 1738 (Dec '94) and 3986 (Jan '05). /// If the specified RFC is not recognised, 3986 is used by default. /// + [SkipLocalsInit] public static string UrlEncode(int rfcNumber, string data, bool path) { - StringBuilder encoded = new StringBuilder(data.Length * 2); - string validUrlCharacters; - if (!RFCEncodingSchemes.TryGetValue(rfcNumber, out validUrlCharacters)) - validUrlCharacters = ValidUrlCharacters; - - string unreservedChars = String.Concat(validUrlCharacters, (path ? ValidPathCharacters : "")); - foreach (char symbol in System.Text.Encoding.UTF8.GetBytes(data)) + byte[] sharedDataBuffer = null; + const int MaxStackLimit = 256; + try { - if (unreservedChars.IndexOf(symbol) != -1) - { - encoded.Append(symbol); - } - else + if (!TryGetRFCEncodingSchemes(rfcNumber, out var validUrlCharacters)) + validUrlCharacters = ValidUrlCharacters; + + var unreservedChars = string.Concat(validUrlCharacters, path ? ValidPathCharacters : string.Empty).AsSpan(); + + var dataAsSpan = data.AsSpan(); + var encoding = Encoding.UTF8; + + var dataByteLength = encoding.GetMaxByteCount(dataAsSpan.Length); + var encodedByteLength = 2 * dataByteLength; + var dataBuffer = encodedByteLength <= MaxStackLimit + ? stackalloc byte[MaxStackLimit] + : sharedDataBuffer = ArrayPool.Shared.Rent(encodedByteLength); + // Instead of stack allocating or renting two buffers we use one buffer with at least twice the capacity of the + // max encoding length. Then store the character data as bytes in the second half reserving the first half of the buffer + // for the encoded representation. + var encodingBuffer = dataBuffer.Slice(dataBuffer.Length - dataByteLength); + var bytesWritten = encoding.GetBytes(dataAsSpan, encodingBuffer); + + var index = 0; + foreach (var symbol in encodingBuffer.Slice(0, bytesWritten)) { - encoded.Append('%'); - - // Break apart the byte into two four-bit components and - // then convert each into their hexadecimal equivalent. - byte b = (byte)symbol; - int hiNibble = b >> 4; - int loNibble = b & 0xF; - encoded.Append(ToUpperHex(hiNibble)); - encoded.Append(ToUpperHex(loNibble)); + if (unreservedChars.IndexOf((char)symbol) != -1) + { + dataBuffer[index++] = symbol; + } + else + { + dataBuffer[index++] = (byte)'%'; + + // Break apart the byte into two four-bit components and + // then convert each into their hexadecimal equivalent. + var hiNibble = symbol >> 4; + var loNibble = symbol & 0xF; + dataBuffer[index++] = (byte)ToUpperHex(hiNibble); + dataBuffer[index++] = (byte)ToUpperHex(loNibble); + } } + + return encoding.GetString(dataBuffer.Slice(0, index)); + } + finally + { + if (sharedDataBuffer != null) ArrayPool.Shared.Return(sharedDataBuffer); + } + } + + internal static bool TryGetRFCEncodingSchemes(int rfcNumber, out string encodingScheme) + { + if (rfcNumber == 3986) + { + encodingScheme = ValidUrlCharacters; + return true; } - return encoded.ToString(); + if (rfcNumber == 1738) + { + encodingScheme = ValidUrlCharactersRFC1738; + return true; + } + + encodingScheme = null; + return false; } private static void ToHexString(Span source, Span destination, bool lowercase) diff --git a/sdk/src/Core/Amazon.Util/_bcl+netstandard/Extensions.cs b/sdk/src/Core/Amazon.Util/_bcl+netstandard/Extensions.cs new file mode 100644 index 000000000000..bc496f8ed99b --- /dev/null +++ b/sdk/src/Core/Amazon.Util/_bcl+netstandard/Extensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Amazon.Util +{ + internal static class Extensions + { + internal static string ToUpper(this String str, CultureInfo culture) + { + if (culture != CultureInfo.InvariantCulture) + throw new ArgumentException("The extension method ToUpper only works for invariant culture"); + return str.ToUpperInvariant(); + } + +#if NETSTANDARD || NETFRAMEWORK + /// + /// Encodes into a span of bytes a set of characters from the specified read-only span. + /// + /// The encoding to be used. + /// The span containing the set of characters to encode. + /// The byte span to hold the encoded bytes. + /// The count of encoded bytes. + /// + /// The method was introduced as a compatibility shim for .NET Standard and can be replaced for target frameworks that provide those methods out of the box. + /// + /// + public static unsafe int GetBytes(this Encoding encoding, + ReadOnlySpan src, + Span dest) + { + if (src.Length == 0) return 0; + + if (dest.Length == 0) return 0; + + fixed (char* charPointer = src) + { + fixed (byte* bytePointer = dest) + { + return encoding.GetBytes( + charPointer, + src.Length, + bytePointer, + dest.Length); + } + } + } + + /// + /// When overridden in a derived class, decodes all the bytes in the specified byte span into a string. + /// + /// The encoding to be used. + /// A read-only byte span to decode to a Unicode string. + /// A string that contains the decoded bytes from the provided read-only span. + /// + /// The method was introduced as a compatibility shim for .NET Standard and can be replaced for target frameworks that provide those methods out of the box. + /// + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan bytes) + { + if (bytes.Length == 0) return string.Empty; + + fixed (byte* bytePointer = bytes) + { + return encoding.GetString(bytePointer, bytes.Length); + } + } +#endif + } +} diff --git a/sdk/src/Core/Amazon.Util/_netstandard/Extensions.cs b/sdk/src/Core/Amazon.Util/_netstandard/Extensions.cs deleted file mode 100644 index 792f332675e1..000000000000 --- a/sdk/src/Core/Amazon.Util/_netstandard/Extensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; - -namespace Amazon.Util -{ - internal static class Extensions - { - internal static string ToUpper(this String str, CultureInfo culture) - { - if (culture != CultureInfo.InvariantCulture) - throw new ArgumentException("The extension method ToUpper only works for invariant culture"); - return str.ToUpperInvariant(); - } - } -} diff --git a/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs b/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs index 159d35e9bb5b..943692170f4b 100644 --- a/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs +++ b/sdk/test/NetStandard/UnitTests/Core/AWSSDKUtilsTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Amazon.Util; using System.Text; using Amazon.Runtime.Internal; @@ -63,5 +63,26 @@ public void DetermineService(string url, string expectedService) Assert.Equal(expectedService, service); } + + [Theory] + [InlineData("value, with special chars!", "value%2C%20with%20special%20chars%21")] + [InlineData("value, with special chars and path {/+:}", "value%2C%20with%20special%20chars%20and%20path%20%7B%2F%2B%3A%7D")] + public void UrlEncodeWithoutPath(string input, string expected) + { + var encoded = AWSSDKUtils.UrlEncode(input, path: false); + + Assert.Equal(expected, encoded); + } + + [Theory] + [InlineData("\ud83d\ude02 value, with special chars!", "%F0%9F%98%82%20value%2C%20with%20special%20chars!")] + [InlineData("value, with special chars!", "value%2C%20with%20special%20chars!")] + [InlineData("value, with special chars and path {/+:}", "value%2C%20with%20special%20chars%20and%20path%20%7B/%2B:%7D")] + public void UrlEncodeWithPath(string input, string expected) + { + var encoded = AWSSDKUtils.UrlEncode(input, path: true); + + Assert.Equal(expected, encoded); + } } }