guid to base64, for URL
Asked Answered
P

6

58

Question: is there a better way to do that?

VB.Net

Function GuidToBase64(ByVal guid As Guid) As String
    Return Convert.ToBase64String(guid.ToByteArray).Replace("/", "-").Replace("+", "_").Replace("=", "")
End Function

Function Base64ToGuid(ByVal base64 As String) As Guid
    Dim guid As Guid
    base64 = base64.Replace("-", "/").Replace("_", "+") & "=="

    Try
        guid = New Guid(Convert.FromBase64String(base64))
    Catch ex As Exception
        Throw New Exception("Bad Base64 conversion to GUID", ex)
    End Try

    Return guid
End Function

C#

public string GuidToBase64(Guid guid)
{
    return Convert.ToBase64String(guid.ToByteArray()).Replace("/", "-").Replace("+", "_").Replace("=", "");
}

public Guid Base64ToGuid(string base64)
{
   Guid guid = default(Guid);
   base64 = base64.Replace("-", "/").Replace("_", "+") + "==";

   try {
       guid = new Guid(Convert.FromBase64String(base64));
   }
   catch (Exception ex) {
       throw new Exception("Bad Base64 conversion to GUID", ex);
   }

   return guid;
}
Purgation answered 23/6, 2009 at 12:51 Comment(12)
Any special reason of removing standard special characters of Base64 encoding?Forde
Is there a particular reason you need to encode it? None of the characters in a GUID need encoding for URLs or attributes.Tracy
@Hemant, because for URL, + and / and = doesn't work well in a GET, @blowdart, to make the url smallerPurgation
@blowdart: I guess @Purgation wants resulting string as short as possible thats why he is encoding it.Forde
Base64 makes a string 33% BIGGER, not smallerCappello
@Charlie: Base64 encoded string is smaller than hex encoding which is default formatting when using .ToString (). Offcourse no-one would like to transmit raw (non printable bytes) directly.Forde
@Charlie how "37945704-cf86-4b2e-a4b5-0db0204902c8" is bigger than "BFeUN4bPLkuktQ2wIEkCyA"Purgation
@usr, can you give me 1 guid that would have "=" when converted into base64?Purgation
I would consider not doing the .replace for URL encoding, or provide a seperate method for that. This would allow a separation of concerns where users of the API could choose whether they wanted real base64 encoding or the URL friendly base64 encoding, depending on what they are wanting to accomplish. I understand your goal was for URL use, but everything except the URL encoding step could potentially be used for those who want a shorter base64 encoding but aren't using it in a URL.Amman
Run "aaaaaaaaaaaaaaaaaaaaaa" through Base64ToGuid(), then take the Guid returned and run it through GuidToBase64() and you get "aaaaaaaaaaaaaaaaaaaaaQ". Lots of other examples like this.Scleroprotein
@Rick, sorry for the late reply. your right that the encoding to base64 give a different result but both give the same Guid.Purgation
I would recommend changing the replacements, i.e. instead of .Replace("+", "_") and vice versa and Replace("/", "-") and vice versa, use .Replace("+", "-") and vice versa and Replace("/", "_") and vice versa. This would make the encoding compliant with RFC 4648 base64url (see tools.ietf.org/html/rfc4648#section-5)Scirrhous
F
22

I understand that the reason you are clipping == in the end is that because you can be certain that for GUID (of 16 bytes), encoded string will always end with ==. So 2 characters can be saved in every conversion.

Beside the point @Skurmedal already mentioned (should throw an exception in case of invalid string as input), I think the code you posted is just good enough.

Forde answered 23/6, 2009 at 13:4 Comment(6)
Didn't think of that first thing, a clever space saver when you think about it :)Immunogenetics
what would be best, dealing with an exception or querying the database anyway with something that doesn't exist? would it add more code in the end since I check if there is at least one row in the result?Purgation
The point is only about where you want to put that check. My experience is that low level library routines should be as transparent as possible. Offcourse here you are the best judge of where the error checking code should go because you know your product and where this library/code stands. It was just a point for consideration.Forde
Well if you are dealing with an exception at least you know something has gone wrong. It might not matter now, but in the future maybe. I don't know your program good enoug :) I think querying the database for stuff that you (in theory) know doesn't exist is the least favourable solution.Immunogenetics
I think I agree with you guys, about throwing an exception, it make more sensePurgation
You should place ´base64.Replace("-", "/")...´ inside of the ´try´-block to avoid an uncaught ´NullReferenceException+ if ´base64 == null´Lightness
M
33

You might want to check out this site: http://prettycode.org/2009/11/12/short-guid/

It looks very close to what you're doing.

public class ShortGuid
{
    private readonly Guid guid;
    private readonly string value;

    /// <summary>Create a 22-character case-sensitive short GUID.</summary>
    public ShortGuid(Guid guid)
    {
        if (guid == null)
        {
            throw new ArgumentNullException("guid");
        }

        this.guid = guid;
        this.value = Convert.ToBase64String(guid.ToByteArray())
            .Substring(0, 22)
            .Replace("/", "_")
            .Replace("+", "-");
    }

    /// <summary>Get the short GUID as a string.</summary>
    public override string ToString()
    {
        return this.value;
    }

    /// <summary>Get the Guid object from which the short GUID was created.</summary>
    public Guid ToGuid()
    {
        return this.guid;
    }

    /// <summary>Get a short GUID as a Guid object.</summary>
    /// <exception cref="System.ArgumentNullException"></exception>
    /// <exception cref="System.FormatException"></exception>
    public static ShortGuid Parse(string shortGuid)
    {
        if (shortGuid == null)
        {
            throw new ArgumentNullException("shortGuid");
        }
        else if (shortGuid.Length != 22)
        {
            throw new FormatException("Input string was not in a correct format.");
        }

        return new ShortGuid(new Guid(Convert.FromBase64String
            (shortGuid.Replace("_", "/").Replace("-", "+") + "==")));
    }

    public static implicit operator String(ShortGuid guid)
    {
        return guid.ToString();
    }

    public static implicit operator Guid(ShortGuid shortGuid)
    {
        return shortGuid.guid;
    }
}
Mccluskey answered 23/2, 2010 at 5:18 Comment(1)
I understand, that this answer is quite an old, but does your code actually work? String result produced by Guid.ToByteArray() + byte-to-hex conversion would be different to calling Guid.ToString(). As per .Net documentation link: the order of bytes in the returned byte array is different from the string representation of a Guid valueFabricate
S
24

One problem with using this technique to format a GUID for use in a URL or filename is that two distinct GUIDs can produce two values that differ only in case, e.g.:

    var b1 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab83c"));
    var b2 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab8a4"));
    Console.WriteLine(b1);  // 80XQyRzi0EaXHbkuvCq4PA
    Console.WriteLine(b2);  // 80XQyRzi0EaXHbkuvCq4pA

Since URLs are sometimes interpreted as being case-insensitive, and in Windows file paths and filenames are case-insensitive. this could lead to collisions.

Stigmatism answered 29/9, 2016 at 13:36 Comment(4)
This is incorrect for URLs. Only the scheme and host should be considered case-insensitive as per RFC 3986. Query, fragment, and even path should be considered case-sensitive. Of course, it's all up to your code/server implementation to adhere to this.Rockwell
@RogerSpurrell - good point re: URLs, but I was thinking more of application-specific handling of a URL such as http://.../user/{id}, where {id} could be a random guid-like id to avoid the OWASP Brute Force Predictable Resource Location vulnerability, and the id might be looked up in a case-insensitive database.Stigmatism
This should be a comment. It is incorrect and a bit far fetched w case-insensitive db (which would suggest use of base32).Twelfthtide
@VajkHermecz I disagree. If you think that the answer is incorrect and/or not useful, you're perfectly free to downvote and/or comment to that effect, but it isn't NAA because it appears to be a good-faith attempt to answer the question.Bathulda
F
22

I understand that the reason you are clipping == in the end is that because you can be certain that for GUID (of 16 bytes), encoded string will always end with ==. So 2 characters can be saved in every conversion.

Beside the point @Skurmedal already mentioned (should throw an exception in case of invalid string as input), I think the code you posted is just good enough.

Forde answered 23/6, 2009 at 13:4 Comment(6)
Didn't think of that first thing, a clever space saver when you think about it :)Immunogenetics
what would be best, dealing with an exception or querying the database anyway with something that doesn't exist? would it add more code in the end since I check if there is at least one row in the result?Purgation
The point is only about where you want to put that check. My experience is that low level library routines should be as transparent as possible. Offcourse here you are the best judge of where the error checking code should go because you know your product and where this library/code stands. It was just a point for consideration.Forde
Well if you are dealing with an exception at least you know something has gone wrong. It might not matter now, but in the future maybe. I don't know your program good enoug :) I think querying the database for stuff that you (in theory) know doesn't exist is the least favourable solution.Immunogenetics
I think I agree with you guys, about throwing an exception, it make more sensePurgation
You should place ´base64.Replace("-", "/")...´ inside of the ´try´-block to avoid an uncaught ´NullReferenceException+ if ´base64 == null´Lightness
A
4

In .NET Core you can use Spans for better performance and no memory allocation.

using System.Buffers.Text;
using System.Runtime.InteropServices;

namespace Extensions;

public static class GuidExtensions
{
    private const char Dash = '-';
    private const char EqualsChar = '=';
    private const byte ForwardSlashByte = (byte)Slash;
    private const char Plus = '+';
    private const byte PlusByte = (byte)Plus;
    private const char Slash = '/';
    private const char Underscore = '_';
    private const int Base64LengthWithoutEquals = 22;

    public static string EncodeBase64String(this Guid guid)
    {
        Span<byte> guidBytes = stackalloc byte[16];
        Span<byte> encodedBytes = stackalloc byte[24];

        MemoryMarshal.TryWrite(guidBytes, ref guid);
        Base64.EncodeToUtf8(guidBytes, encodedBytes, out _, out _);

        Span<char> chars = stackalloc char[Base64LengthWithoutEquals];

        // Replace any characters which are not URL safe.
        // And skip the final two bytes as these will be '==' padding we don't need.
        for (int i = 0; i < Base64LengthWithoutEquals; i++)
        {
            chars[i] = encodedBytes[i] switch
            {
                ForwardSlashByte => Dash,
                PlusByte => Underscore,
                _ => (char)encodedBytes[i],
            };
        }

        return new(chars);
    }

    public static Guid DecodeBase64String(this ReadOnlySpan<char> id)
    {
        Span<char> base64Chars = stackalloc char[24];

        for (var i = 0; i < Base64LengthWithoutEquals; i++)
        {
            base64Chars[i] = id[i] switch
            {
                Dash => Slash,
                Underscore => Plus,
                _ => id[i],
            };
        }

        base64Chars[22] = EqualsChar;
        base64Chars[23] = EqualsChar;

        Span<byte> idBytes = stackalloc byte[16];
        Convert.TryFromBase64Chars(base64Chars, idBytes, out _);

        return new(idBytes);
    }
}
using AutoFixture.Xunit2;
using FluentAssertions;
using Extensions;
using Xunit;

namespace ExtensionTests;

public class GuidExtensionsTests
{
    private const int Base64LengthWithoutEquals = 22;
    private const string EmptyBase64 = "AAAAAAAAAAAAAAAAAAAAAA";

    [Theory]
    [AutoData]
    public void EncodeBase64String_DecodeBase64String_Should_ReturnInitialGuid(Guid guid)
    {
        string actualBase64 = guid.EncodeBase64String();
        actualBase64.Should().NotBe(string.Empty)
            .And.HaveLength(Base64LengthWithoutEquals);

        Guid actualGuid = ((ReadOnlySpan<char>)actualBase64).DecodeBase64String();
        actualGuid.Should().Be(guid);
    }

    [Theory]
    [InlineData(EmptyBase64)]
    public void EncodeBase64String_Should_ReturnEmptyBase64_When_GuidIsEmpty(string expected)
    {
        string actualBase64 = Guid.Empty.EncodeBase64String();
        actualBase64.Should().Be(expected);
    }

    [Theory]
    [InlineData(EmptyBase64)]
    public void DecodeBase64String_Should_ReturnEmptyGuid_When_StringIsEmptyBase64(string base64)
    {
        Guid actual = ((ReadOnlySpan<char>)base64).DecodeBase64String();
        actual.Should().Be(Guid.Empty);
    }
}

For more info read about using high-performance techniques to base64 encode a guid, and a very nice video explanation.

Atc answered 14/9, 2022 at 8:12 Comment(0)
I
3

If your method cannot convert the Base64 passed to it to a GUID, shouldn't you throw an exception? The data passed to the method is clearly erronous.

Immunogenetics answered 23/6, 2009 at 13:0 Comment(1)
I think I agree with you guys, about throwing an exception, it make more sensePurgation
B
0

There is a method that I am using to encode as well as shorten for my URL (Guid): https://dotnetfiddle.net/iQ7nGv

public static void Main()
{
    Guid gg = Guid.NewGuid();
    string ss = Encode(gg);
    Console.WriteLine(gg);
    Console.WriteLine(ss);
    Console.WriteLine(Decode(ss));
}

public static string Encode(Guid guid)
{
    string encoded = Convert.ToBase64String(guid.ToByteArray());
    encoded = encoded.Replace("/", "_").Replace("+", "-");
    return encoded.Substring(0, 22);
}

public static Guid Decode(string value)
{
    value = value.Replace("_", "/").Replace("-", "+");
    byte[] buffer = Convert.FromBase64String(value + "==");
    return new Guid(buffer);
}
Brookes answered 26/2, 2023 at 8:48 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Basidiomycete

© 2022 - 2024 — McMap. All rights reserved.