Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Fix netcore WebClient.UploadValuesAsync() UrlEncode behavior #18933

Merged
merged 2 commits into from
Apr 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions src/System.Net.WebClient/src/System/Net/WebClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "&";
}

Expand Down Expand Up @@ -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)
{
Expand Down
10 changes: 8 additions & 2 deletions src/System.Net.WebClient/tests/WebClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -688,15 +695,14 @@ 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))]
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));
}
}

Expand Down