diff --git a/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs b/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs
new file mode 100644
index 0000000000..f73c3c249f
--- /dev/null
+++ b/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs
@@ -0,0 +1,183 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+
+namespace Microsoft.AspNet.WebUtilities
+{
+ ///
+ /// Contains utility APIs to assist with common encoding and decoding operations.
+ ///
+ public static class WebEncoders
+ {
+ ///
+ /// Decodes a base64url-encoded string.
+ ///
+ /// The base64url-encoded input to decode.
+ /// The base64url-decoded form of the input.
+ ///
+ /// The input must not contain any whitespace or padding characters.
+ /// Throws FormatException if the input is malformed.
+ ///
+ public static byte[] Base64UrlDecode([NotNull] string input)
+ {
+ return Base64UrlDecode(input, 0, input.Length);
+ }
+
+ ///
+ /// Decodes a base64url-encoded substring of a given string.
+ ///
+ /// A string containing the base64url-encoded input to decode.
+ /// The position in at which decoding should begin.
+ /// The number of characters in to decode.
+ /// The base64url-decoded form of the input.
+ ///
+ /// The input must not contain any whitespace or padding characters.
+ /// Throws FormatException if the input is malformed.
+ ///
+ public static byte[] Base64UrlDecode([NotNull] string input, int offset, int count)
+ {
+ ValidateParameters(input.Length, offset, count);
+
+ // Assumption: input is base64url encoded without padding and contains no whitespace.
+
+ // First, we need to add the padding characters back.
+ int numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
+ char[] completeBase64Array = new char[checked(count + numPaddingCharsToAdd)];
+ Debug.Assert(completeBase64Array.Length % 4 == 0, "Invariant: Array length must be a multiple of 4.");
+ input.CopyTo(offset, completeBase64Array, 0, count);
+ for (int i = 1; i <= numPaddingCharsToAdd; i++)
+ {
+ completeBase64Array[completeBase64Array.Length - i] = '=';
+ }
+
+ // Next, fix up '-' -> '+' and '_' -> '/'
+ for (int i = 0; i < completeBase64Array.Length; i++)
+ {
+ char c = completeBase64Array[i];
+ if (c == '-')
+ {
+ completeBase64Array[i] = '+';
+ }
+ else if (c == '_')
+ {
+ completeBase64Array[i] = '/';
+ }
+ }
+
+ // Finally, decode.
+ // If the caller provided invalid base64 chars, they'll be caught here.
+ return Convert.FromBase64CharArray(completeBase64Array, 0, completeBase64Array.Length);
+ }
+
+ ///
+ /// Encodes an input using base64url encoding.
+ ///
+ /// The binary input to encode.
+ /// The base64url-encoded form of the input.
+ public static string Base64UrlEncode([NotNull] byte[] input)
+ {
+ return Base64UrlEncode(input, 0, input.Length);
+ }
+
+ ///
+ /// Encodes an input using base64url encoding.
+ ///
+ /// The binary input to encode.
+ /// The offset into at which to begin encoding.
+ /// The number of bytes of to encode.
+ /// The base64url-encoded form of the input.
+ public static string Base64UrlEncode([NotNull] byte[] input, int offset, int count)
+ {
+ ValidateParameters(input.Length, offset, count);
+
+ // Special-case empty input
+ if (count == 0)
+ {
+ return String.Empty;
+ }
+
+ // We're going to use base64url encoding with no padding characters.
+ // See RFC 4648, Sec. 5.
+ char[] buffer = new char[GetNumBase64CharsRequiredForInput(count)];
+ int numBase64Chars = Convert.ToBase64CharArray(input, offset, count, buffer, 0);
+
+ // Fix up '+' -> '-' and '/' -> '_'
+ for (int i = 0; i < numBase64Chars; i++)
+ {
+ char ch = buffer[i];
+ if (ch == '+')
+ {
+ buffer[i] = '-';
+ }
+ else if (ch == '/')
+ {
+ buffer[i] = '_';
+ }
+ else if (ch == '=')
+ {
+ // We've reached a padding character: truncate the string from this point
+ return new String(buffer, 0, i);
+ }
+ }
+
+ // If we got this far, the buffer didn't contain any padding chars, so turn
+ // it directly into a string.
+ return new String(buffer, 0, numBase64Chars);
+ }
+
+ private static int GetNumBase64CharsRequiredForInput(int inputLength)
+ {
+ int numWholeOrPartialInputBlocks = checked(inputLength + 2) / 3;
+ return checked(numWholeOrPartialInputBlocks * 4);
+ }
+
+ private static int GetNumBase64PaddingCharsInString(string str)
+ {
+ // Assumption: input contains a well-formed base64 string with no whitespace.
+
+ // base64 guaranteed have 0 - 2 padding characters.
+ if (str[str.Length - 1] == '=')
+ {
+ if (str[str.Length - 2] == '=')
+ {
+ return 2;
+ }
+ return 1;
+ }
+ return 0;
+ }
+
+ private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
+ {
+ switch (inputLength % 4)
+ {
+ case 0:
+ return 0;
+ case 2:
+ return 2;
+ case 3:
+ return 1;
+ default:
+ throw new FormatException("TODO: Malformed input.");
+ }
+ }
+
+ private static void ValidateParameters(int bufferLength, int offset, int count)
+ {
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException("offset");
+ }
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException("count");
+ }
+ if (bufferLength - offset < count)
+ {
+ throw new ArgumentException("Invalid offset / length.");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json
index fcb9fea61d..74757f774a 100644
--- a/src/Microsoft.AspNet.WebUtilities/project.json
+++ b/src/Microsoft.AspNet.WebUtilities/project.json
@@ -1,14 +1,15 @@
{
- "version": "1.0.0-*",
- "dependencies": {
- "Microsoft.AspNet.Http": "1.0.0-*"
- },
- "frameworks": {
- "aspnet50": {},
- "aspnetcore50": {
- "dependencies": {
- "System.Runtime": "4.0.20-beta-*"
- }
+ "version": "1.0.0-*",
+ "dependencies": {
+ "Microsoft.AspNet.Http": "1.0.0-*"
+ },
+ "frameworks": {
+ "aspnet50": { },
+ "aspnetcore50": {
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.10-beta-*",
+ "System.Runtime": "4.0.20-beta-*"
+ }
+ }
}
- }
}
diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs
new file mode 100644
index 0000000000..9813a780c3
--- /dev/null
+++ b/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.AspNet.WebUtilities
+{
+ public class WebEncodersTests
+ {
+ [Theory]
+ [InlineData("", 1, 0)]
+ [InlineData("", 0, 1)]
+ [InlineData("0123456789", 9, 2)]
+ [InlineData("0123456789", Int32.MaxValue, 2)]
+ [InlineData("0123456789", 9, -1)]
+ public void Base64UrlDecode_BadOffsets(string input, int offset, int count)
+ {
+ // Act & assert
+ Assert.ThrowsAny(() =>
+ {
+ var retVal = WebEncoders.Base64UrlDecode(input, offset, count);
+ });
+ }
+
+ [Theory]
+ [InlineData("x")]
+ [InlineData("(x)")]
+ public void Base64UrlDecode_MalformedInput(string input)
+ {
+ // Act & assert
+ Assert.Throws(() =>
+ {
+ var retVal = WebEncoders.Base64UrlDecode(input);
+ });
+ }
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")]
+ [InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")]
+ [InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")]
+ public void Base64UrlEncode_And_Decode(string base64Input, string expectedBase64Url)
+ {
+ // Arrange
+ byte[] input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray();
+
+ // Act & assert - 1
+ string actualBase64Url = WebEncoders.Base64UrlEncode(input, 3, input.Length - 5); // also helps test offsets
+ Assert.Equal(expectedBase64Url, actualBase64Url);
+
+ // Act & assert - 2
+ // Verify that values round-trip
+ byte[] roundTripped = WebEncoders.Base64UrlDecode("xx" + actualBase64Url + "yyy", 2, actualBase64Url.Length); // also helps test offsets
+ string roundTrippedAsBase64 = Convert.ToBase64String(roundTripped);
+ Assert.Equal(roundTrippedAsBase64, base64Input);
+ }
+
+ [Theory]
+ [InlineData(0, 1, 0)]
+ [InlineData(0, 0, 1)]
+ [InlineData(10, 9, 2)]
+ [InlineData(10, Int32.MaxValue, 2)]
+ [InlineData(10, 9, -1)]
+ public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count)
+ {
+ // Arrange
+ byte[] input = new byte[inputLength];
+
+ // Act & assert
+ Assert.ThrowsAny(() =>
+ {
+ var retVal = WebEncoders.Base64UrlEncode(input, offset, count);
+ });
+ }
+ }
+}