From ee6845afaa8c316469ef522ac0f4747e157747c0 Mon Sep 17 00:00:00 2001 From: Caesar Chen Date: Thu, 27 Apr 2017 05:44:32 -0700 Subject: [PATCH] Fix netcore WebClient.UploadValuesAsync() UrlEncode behavior (#18933) * fix netcore Webclient UploadValuesAsync UrlEncode behavior * Address PR feedback --- .../src/System/Net/WebClient.cs | 99 ++++++++++++++++++- .../tests/WebClientTest.cs | 10 +- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/System.Net.WebClient/src/System/Net/WebClient.cs b/src/System.Net.WebClient/src/System/Net/WebClient.cs index b678f2b9c34c..e6cbbd196c6c 100644 --- a/src/System.Net.WebClient/src/System/Net/WebClient.cs +++ b/src/System.Net.WebClient/src/System/Net/WebClient.cs @@ -588,9 +588,9 @@ private byte[] GetValuesToUpload(NameValueCollection data) foreach (string name in data.AllKeys) { values.Append(delimiter); - values.Append(WebUtility.UrlEncode(name)); + values.Append(UrlEncode(name)); values.Append('='); - values.Append(WebUtility.UrlEncode(data[name])); + values.Append(UrlEncode(data[name])); delimiter = "&"; } @@ -1176,6 +1176,101 @@ private string MapToDefaultMethod(Uri address) "STOR" : "POST"; } + + private static string UrlEncode(string str) + { + if (str == null) + return null; + byte[] bytes = Encoding.UTF8.GetBytes(str); + return Encoding.ASCII.GetString(UrlEncodeBytesToBytesInternal(bytes, 0, bytes.Length, false)); + } + + private static byte[] UrlEncodeBytesToBytesInternal(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue) + { + int cSpaces = 0; + int cUnsafe = 0; + + // Count them first. + for (int i = 0; i < count; i++) + { + char ch = (char) bytes[offset + i]; + + if (ch == ' ') + { + cSpaces++; + } + else if (!IsSafe(ch)) + { + cUnsafe++; + } + } + + // If nothing to expand. + if (!alwaysCreateReturnValue && cSpaces == 0 && cUnsafe == 0) + return bytes; + + // Expand not 'safe' characters into %XX, spaces to +. + byte[] expandedBytes = new byte[count + cUnsafe * 2]; + int pos = 0; + + for (int i = 0; i < count; i++) + { + byte b = bytes[offset+i]; + char ch = (char) b; + + if (IsSafe(ch)) + { + expandedBytes[pos++] = b; + } + else if (ch == ' ') + { + expandedBytes[pos++] = (byte) '+'; + } + else + { + expandedBytes[pos++] = (byte) '%'; + expandedBytes[pos++] = (byte) IntToHex((b >> 4) & 0xf); + expandedBytes[pos++] = (byte) IntToHex(b & 0x0f); + } + } + + return expandedBytes; + } + + private static char IntToHex(int n) + { + Debug.Assert(n < 0x10); + + if (n <= 9) + { + return(char)(n + (int)'0'); + } + + return(char)(n - 10 + (int)'a'); + } + + private static bool IsSafe(char ch) + { + if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') + { + return true; + } + + switch (ch) + { + case '-': + case '_': + case '.': + case '!': + case '*': + case '\'': + case '(': + case ')': + return true; + } + + return false; + } private void InvokeOperationCompleted(AsyncOperation asyncOp, SendOrPostCallback callback, AsyncCompletedEventArgs eventArgs) { diff --git a/src/System.Net.WebClient/tests/WebClientTest.cs b/src/System.Net.WebClient/tests/WebClientTest.cs index 703769ecd71a..74d9e08cc758 100644 --- a/src/System.Net.WebClient/tests/WebClientTest.cs +++ b/src/System.Net.WebClient/tests/WebClientTest.cs @@ -485,6 +485,13 @@ public abstract class WebClientTestBase "The Slings and Arrows of outrageous Fortune," + "Or to take Arms against a Sea of troubles," + "And by opposing end them:"; + + const string ExpectedTextAfterUrlEncode = + "To+be%2c+or+not+to+be%2c+that+is+the+question%3a" + + "Whether+'tis+Nobler+in+the+mind+to+suffer" + + "The+Slings+and+Arrows+of+outrageous+Fortune%2c" + + "Or+to+take+Arms+against+a+Sea+of+troubles%2c" + + "And+by+opposing+end+them%3a"; protected abstract bool IsAsync { get; } @@ -688,7 +695,6 @@ public async Task UploadString_Success(Uri echoServer) Assert.Contains(ExpectedText, result); } - [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "dotnet/corefx #18674")] // Difference in behavior. [OuterLoop("Networking test talking to remote server: issue #11345")] [Theory] [MemberData(nameof(EchoServers))] @@ -696,7 +702,7 @@ public async Task UploadValues_Success(Uri echoServer) { var wc = new WebClient(); byte[] result = await UploadValuesAsync(wc, echoServer.ToString(), new NameValueCollection() { { "Data", ExpectedText } }); - Assert.Contains(WebUtility.UrlEncode(ExpectedText), Encoding.UTF8.GetString(result)); + Assert.Contains(ExpectedTextAfterUrlEncode, Encoding.UTF8.GetString(result)); } }