21

I'm porting a project targeting net472 to netstandard. The last System.Web dependency I'm stuck with is HttpServerUtility.UrlTokenEncode(Byte[]).

I found Microsoft.AspNetCore.WebUtilities, which contains Base64UrlTextEncoder and WebEncoders, but those are not interchangeable with the UrlTokenEncode/Decode, as it appends / expects the number of = padding characters at the end, e.g.:

var data = Encoding.UTF8.GetBytes("SO");
Convert.ToBase64String(data);              // U08=
HttpServerUtility.UrlTokenEncode(data);    // U081 - this is what's expected and 
                                           // the only thing UrlTokenDecode can handle
Base64UrlTextEncoder.Encode(data);         // U08
WebEncoders.Base64UrlEncode(data);         // U08

As far as I can tell, there are no other differences (I ran tests with random strings), but it also pulls in some other dependencies (Microsoft.Net.Http.Headers & Microsoft.Extensions.Primitives), that I don't really need in that project.

Is there any nuget package with a drop-in replacement? I'm thinking of implementing this myself, if not.

m0sa
  • 10,712
  • 4
  • 44
  • 91

3 Answers3

31

For those whom looks for the answer of this removed utility method and wish to migrate legacy application across, I have done some extract from M$ source.

   private static string UrlTokenEncode(byte[] input)
    {
        if (input == null)
            throw new ArgumentNullException("input");
        if (input.Length < 1)
            return String.Empty;
        char[] base64Chars = null;

        ////////////////////////////////////////////////////////
        // Step 1: Do a Base64 encoding
        string base64Str = Convert.ToBase64String(input);
        if (base64Str == null)
            return null;

        int endPos;
        ////////////////////////////////////////////////////////
        // Step 2: Find how many padding chars are present in the end
        for (endPos = base64Str.Length; endPos > 0; endPos--)
        {
            if (base64Str[endPos - 1] != '=') // Found a non-padding char!
            {
                break; // Stop here
            }
        }

        ////////////////////////////////////////////////////////
        // Step 3: Create char array to store all non-padding chars,
        //      plus a char to indicate how many padding chars are needed
        base64Chars = new char[endPos + 1];
        base64Chars[endPos] = (char)((int)'0' + base64Str.Length - endPos); // Store a char at the end, to indicate how many padding chars are needed

        ////////////////////////////////////////////////////////
        // Step 3: Copy in the other chars. Transform the "+" to "-", and "/" to "_"
        for (int iter = 0; iter < endPos; iter++)
        {
            char c = base64Str[iter];

            switch (c)
            {
                case '+':
                    base64Chars[iter] = '-';
                    break;

                case '/':
                    base64Chars[iter] = '_';
                    break;

                case '=':
                    Debug.Assert(false);
                    base64Chars[iter] = c;
                    break;

                default:
                    base64Chars[iter] = c;
                    break;
            }
        }
        return new string(base64Chars);
    }

    private static byte[] UrlTokenDecode(string input)
    {
        if (input == null)
            throw new ArgumentNullException("input");

        int len = input.Length;
        if (len < 1)
            return new byte[0];

        ///////////////////////////////////////////////////////////////////
        // Step 1: Calculate the number of padding chars to append to this string.
        //         The number of padding chars to append is stored in the last char of the string.
        int numPadChars = (int)input[len - 1] - (int)'0';
        if (numPadChars < 0 || numPadChars > 10)
            return null;


        ///////////////////////////////////////////////////////////////////
        // Step 2: Create array to store the chars (not including the last char)
        //          and the padding chars
        char[] base64Chars = new char[len - 1 + numPadChars];


        ////////////////////////////////////////////////////////
        // Step 3: Copy in the chars. Transform the "-" to "+", and "*" to "/"
        for (int iter = 0; iter < len - 1; iter++)
        {
            char c = input[iter];

            switch (c)
            {
                case '-':
                    base64Chars[iter] = '+';
                    break;

                case '_':
                    base64Chars[iter] = '/';
                    break;

                default:
                    base64Chars[iter] = c;
                    break;
            }
        }

        ////////////////////////////////////////////////////////
        // Step 4: Add padding chars
        for (int iter = len - 1; iter < base64Chars.Length; iter++)
        {
            base64Chars[iter] = '=';
        }

        // Do the actual conversion
        return Convert.FromBase64CharArray(base64Chars, 0, base64Chars.Length);
    }
Leon Hong
  • 309
  • 3
  • 2
  • 3
    This was tested within our project on 9-25-19 and works as a replacement for: System.Web.HttpServerUtility.UrlTokenDecode (str) – phanf Sep 25 '19 at 16:22
  • 2
    Here's a link to the reference source: https://github.com/microsoft/referencesource/blob/f461f1986ca4027720656a0c77bede9963e20b7e/System.Web/Util/HttpEncoder.cs#L856 – Dustin Jan 25 '21 at 23:24
  • I'm not fan of the idea to add a big class to my project directly ripped from MS sources, but it appeared to be the easiest and safest solution to mimic the exact behavior of UrlTokenEncode/Decode without to have a dependency to System.Web for a legacy assembly used in both .Net 4 and .Net 7 projects (migration in progress). Thanks – AFract Feb 28 '23 at 19:12
2

My KISS, solution, that works on netstandard1.6:

public static class Utils
{
    private static readonly Regex InvalidBase64UrlTokens = new Regex(
        @"[^=a-z0-9]", 
        RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

    public static string Base64UrlTokenEncode(byte[] data)
    {
        var padding = 0;
        var base64String = Convert.ToBase64String(data);
        return InvalidBase64UrlTokens.Replace(base64String, m => {
            switch (m.Value)
            {
                case "+": return "-";
                case "=":
                    padding++;
                    return "";
                default: return "_";
            }
        }) + padding;
    }
}

Passes the following nunit tests:

[TestFixture]
public class Base64UrlTokenEncodingTests
{        
    private static IEnumerable<TestCaseData> CompareAgainstSystemWebImplementationCases()
    {
        var random = new Random(42);
        for (var i = 0; i < 100; i++)
        {
            var bytes = new byte[i + 1];
            random.NextBytes(bytes);

            var name = Convert.ToBase64String(bytes);
            var systemWeb = System.Web.HttpServerUtility.UrlTokenEncode(bytes);
            yield return new TestCaseData(bytes).SetName(name).Returns(systemWeb);
        }
    }

    [TestCaseSource(nameof(CompareAgainstSystemWebImplementationCases))]
    public string CompareAgainstSystemWebImplementation(byte[] data) =>
        Utils.Base64UrlTokenEncode(data);
}
m0sa
  • 10,712
  • 4
  • 44
  • 91
  • This implementation is buggy, it gives a different result than `HttpServerUtility.UrlTokenEncoding` when an equals sign (`=`) is present in the input byte array. – rexcfnghk Apr 30 '19 at 01:56
-1

In net core : Microsoft.AspNetCore.WebUtilities.Base64UrlEndCoder

Visit https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.webencoders?view=aspnetcore-5.0?

it dũng
  • 29
  • 2