From aa158f5d2522949cc9566b060411ead324f6a943 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Wed, 2 Nov 2016 17:32:22 -0700 Subject: [PATCH] Modified Base64UrlTextEncoder to reduce string allocations. --- .../Base64UrlTextEncoder.cs | 74 +++++++++++++++++-- .../Properties/AssemblyInfo.cs | 4 +- .../Base64UrlTextEncoderTests.cs | 39 ++++++++++ .../project.json | 3 +- 4 files changed, 112 insertions(+), 8 deletions(-) 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": {