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