diff --git a/src/Microsoft.AspNetCore.WebUtilities/Base64UrlTextEncoder.cs b/src/Microsoft.AspNetCore.WebUtilities/Base64UrlTextEncoder.cs
index 0402429878..a972fa8b9c 100644
--- a/src/Microsoft.AspNetCore.WebUtilities/Base64UrlTextEncoder.cs
+++ b/src/Microsoft.AspNetCore.WebUtilities/Base64UrlTextEncoder.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Text;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.WebUtilities
{
@@ -15,7 +17,8 @@ namespace Microsoft.AspNetCore.WebUtilities
/// Base64 encoded string modified with non-URL encodable characters
public static string Encode(byte[] data)
{
- return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
+ var encodedValue = Convert.ToBase64String(data);
+ return EncodeInternal(encodedValue);
}
///
@@ -26,17 +29,76 @@ namespace Microsoft.AspNetCore.WebUtilities
/// The decoded data.
public static byte[] Decode(string text)
{
- return Convert.FromBase64String(Pad(text.Replace('-', '+').Replace('_', '/')));
+ return Convert.FromBase64String(DecodeToBase64String(text));
}
- private static string Pad(string text)
+ // To enable unit testing
+ internal static string EncodeInternal(string base64EncodedString)
{
- var padding = 3 - ((text.Length + 3) % 4);
- if (padding == 0)
+ var length = base64EncodedString.Length;
+ while (length > 0 && base64EncodedString[length - 1] == '=')
+ {
+ length--;
+ }
+
+ if (length == 0)
+ {
+ return string.Empty;
+ }
+
+ var inplaceStringBuilder = new InplaceStringBuilder(length);
+ for (var i = 0; i < length; i++)
+ {
+ if (base64EncodedString[i] == '+')
+ {
+ inplaceStringBuilder.Append('-');
+ }
+ else if (base64EncodedString[i] == '/')
+ {
+ inplaceStringBuilder.Append('_');
+ }
+ else
+ {
+ inplaceStringBuilder.Append(base64EncodedString[i]);
+ }
+ }
+
+ return inplaceStringBuilder.ToString();
+ }
+
+ // To enable unit testing
+ internal static string DecodeToBase64String(string text)
+ {
+ if (string.IsNullOrEmpty(text))
{
return text;
}
- return text + new string('=', padding);
+
+ var padLength = 3 - ((text.Length + 3) % 4);
+ var inplaceStringBuilder = new InplaceStringBuilder(capacity: text.Length + padLength);
+
+ for (var i = 0; i < text.Length; i++)
+ {
+ if (text[i] == '-')
+ {
+ inplaceStringBuilder.Append('+');
+ }
+ else if (text[i] == '_')
+ {
+ inplaceStringBuilder.Append('/');
+ }
+ else
+ {
+ inplaceStringBuilder.Append(text[i]);
+ }
+ }
+
+ for (var i = 0; i < padLength; i++)
+ {
+ inplaceStringBuilder.Append('=');
+ }
+
+ return inplaceStringBuilder.ToString();
}
}
}
diff --git a/src/Microsoft.AspNetCore.WebUtilities/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.WebUtilities/Properties/AssemblyInfo.cs
index 76feceeff0..0ebd229a18 100644
--- a/src/Microsoft.AspNetCore.WebUtilities/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNetCore.WebUtilities/Properties/AssemblyInfo.cs
@@ -3,9 +3,11 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
[assembly: AssemblyMetadata("Serviceable", "True")]
-[assembly: NeutralResourcesLanguage("en-us")]
[assembly: AssemblyCompany("Microsoft Corporation.")]
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.WebUtilities.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: NeutralResourcesLanguage("en-us")]
diff --git a/test/Microsoft.AspNetCore.WebUtilities.Tests/Base64UrlTextEncoderTests.cs b/test/Microsoft.AspNetCore.WebUtilities.Tests/Base64UrlTextEncoderTests.cs
index 2034ea33d7..7a5701cc85 100644
--- a/test/Microsoft.AspNetCore.WebUtilities.Tests/Base64UrlTextEncoderTests.cs
+++ b/test/Microsoft.AspNetCore.WebUtilities.Tests/Base64UrlTextEncoderTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities
@@ -26,5 +27,43 @@ namespace Microsoft.AspNetCore.WebUtilities
}
}
}
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("+", "-")]
+ [InlineData("/", "_")]
+ [InlineData("=", "")]
+ [InlineData("==", "")]
+ [InlineData("a+b+c+==", "a-b-c-")]
+ [InlineData("a/b/c==", "a_b_c")]
+ [InlineData("a+b/c==", "a-b_c")]
+ [InlineData("a+b/c", "a-b_c")]
+ [InlineData("abcd", "abcd")]
+ public void EncodeInternal_Replaces_UrlEncodableCharacters(string base64EncodedValue, string expectedValue)
+ {
+ // Arrange & Act
+ var result = Base64UrlTextEncoder.EncodeInternal(base64EncodedValue);
+
+ // Assert
+ Assert.Equal(expectedValue, result);
+ }
+
+ [Theory]
+ [InlineData("_", "/===")]
+ [InlineData("-", "+===")]
+ [InlineData("a-b-c", "a+b+c===")]
+ [InlineData("a_b_c_d", "a/b/c/d=")]
+ [InlineData("a-b_c", "a+b/c===")]
+ [InlineData("a-b_c-d", "a+b/c+d=")]
+ [InlineData("a-b_c", "a+b/c===")]
+ [InlineData("abcd", "abcd")]
+ public void DecodeToBase64String_ReturnsValid_Base64String(string text, string expectedValue)
+ {
+ // Arrange & Act
+ var actual = Base64UrlTextEncoder.DecodeToBase64String(text);
+
+ // Assert
+ Assert.Equal(expectedValue, actual);
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.WebUtilities.Tests/project.json b/test/Microsoft.AspNetCore.WebUtilities.Tests/project.json
index f2ed6e6ce4..4975197e7f 100644
--- a/test/Microsoft.AspNetCore.WebUtilities.Tests/project.json
+++ b/test/Microsoft.AspNetCore.WebUtilities.Tests/project.json
@@ -5,7 +5,8 @@
"xunit": "2.2.0-*"
},
"buildOptions": {
- "warningsAsErrors": true
+ "warningsAsErrors": true,
+ "keyFile": "../../tools/Key.snk"
},
"testRunner": "xunit",
"frameworks": {