Skip to content

Instantly share code, notes, and snippets.

@khellang
Last active April 19, 2023 12:22
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save khellang/4993fcfbf8fb2ecdeccc2c822567037c to your computer and use it in GitHub Desktop.
Save khellang/4993fcfbf8fb2ecdeccc2c822567037c to your computer and use it in GitHub Desktop.
// MIT License
//
// Copyright (c) 2018 Kristian Hellang
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class CompactGuid
{
private static ReadOnlySpan<byte> Alphabet => new[]
{
(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', (byte)'h', (byte)'j', (byte)'k',
(byte)'m', (byte)'n', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'v', (byte)'w', (byte)'x',
(byte)'y', (byte)'z'
};
private static readonly string EmptyString = new string('0', Length);
private const int Length = 26;
/// <summary>
/// Produces a 26-character, Base32-encoded representation of a <see cref="Guid"/>.
/// </summary>
/// <remarks>
/// This has a bunch of nice characteristics:
/// - Safe without encoding (uses only characters from ASCII)
/// - Avoids ambiguous characters (i/I/l/L/o/O/0)
/// - Easy for humans to read and pronounce
/// - Supports full UUID range (128 bits)
/// - Safe for URLs and file names
/// - Case-insensitive
/// - 30% smaller
/// </remarks>
/// <param name="value">The <see cref="Guid"/> to encode.</param>
/// <returns>A 26-character, Base32-encoded <see cref="string"/>.</returns>
public static string ToString(Guid value)
{
if (value == Guid.Empty)
{
return EmptyString;
}
Span<byte> target = stackalloc byte[Length];
Encoder.Encode(value, target);
return Encoding.ASCII.GetString(target);
}
/// <summary>
/// Tries to parse a 26-character, Base32-encoded representation of a <see cref="Guid"/>.
/// </summary>
/// <param name="value">The characters to parse.</param>
/// <param name="result">The parsed <see cref="Guid"/>.</param>
/// <returns>Returns <c>true</c> if the parsing succeeded, <c>false</c> otherwise.</returns>
public static bool TryParse(ReadOnlySpan<char> value, out Guid result)
{
if (value.Length != Length)
{
result = default;
return false;
}
Span<byte> source = stackalloc byte[Length];
Encoding.ASCII.GetBytes(value, source);
return Decoder.TryDecode(source, out result);
}
private static Span<ulong> AsSpan<T>(ref T value) where T : unmanaged
{
return MemoryMarshal.Cast<T, ulong>(MemoryMarshal.CreateSpan(ref value, 1));
}
private static class Encoder
{
public static void Encode(Guid value, Span<byte> target)
{
var longs = AsSpan(ref value);
EncodeUInt64(target.Slice(0, Length / 2), longs[0]);
EncodeUInt64(target.Slice(Length / 2), longs[1]);
}
private static void EncodeUInt64(Span<byte> bytes, ulong result)
{
var index = 0;
// Because a GUID is 128 bits and 26 characters with 5 bits
// each is 130, we limit the 1st and 13th character to 4 bits (hex).
bytes[index++] = Alphabet[(int)(result >> 60)];
result <<= 4;
while (index < bytes.Length)
{
// Each following character carries 5 bits each.
bytes[index++] = Alphabet[(int)(result >> 59)];
result <<= 5;
}
}
}
private static class Decoder
{
private static readonly int[] AsciiMapping = GenerateAsciiMapping();
public static bool TryDecode(ReadOnlySpan<byte> bytes, out Guid result)
{
result = default;
var longs = AsSpan(ref result);
return TryDecodeUInt64(bytes.Slice(0, Length / 2), ref longs[0])
&& TryDecodeUInt64(bytes.Slice(Length / 2), ref longs[1]);
}
private static bool TryDecodeUInt64(ReadOnlySpan<byte> bytes, ref ulong result)
{
for (var i = 0; i < bytes.Length; i++)
{
var index = bytes[i];
if (index >= AsciiMapping.Length)
{
return false; // Not ASCII.
}
var value = AsciiMapping[index];
if (value == -1)
{
return false; // Invalid ASCII character.
}
result = (result << 5) | (uint)value;
}
return true;
}
private static int[] GenerateAsciiMapping()
{
const char start = '\x00', end = '\x7F';
var mapping = new int[end - start + 1];
for (var i = start; i <= end; i++)
{
mapping[i] = Alphabet.IndexOf((byte)char.ToLower(i));
}
mapping['o'] = mapping['O'] = 0;
mapping['i'] = mapping['I'] = mapping['l'] = mapping['L'] = 1;
return mapping;
}
}
}
using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
public static class UuidConverter
{
public static Guid ToUuid(this Guid value)
{
var decomposed = new Decomposed(value);
decomposed.Data1 = decomposed.Data1.ToBigEndian();
decomposed.Data2 = decomposed.Data2.ToBigEndian();
decomposed.Data3 = decomposed.Data3.ToBigEndian();
return decomposed.Value;
}
public static Guid ToGuid(this Guid value)
{
var decomposed = new Decomposed(value);
decomposed.Data1 = decomposed.Data1.FromBigEndian();
decomposed.Data2 = decomposed.Data2.FromBigEndian();
decomposed.Data3 = decomposed.Data3.FromBigEndian();
return decomposed.Value;
}
private static uint ToBigEndian(this uint data)
{
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data;
}
private static ushort ToBigEndian(this ushort data)
{
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data;
}
private static uint FromBigEndian(this uint data)
{
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data;
}
private static ushort FromBigEndian(this ushort data)
{
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data;
}
[StructLayout(LayoutKind.Explicit)]
private struct Decomposed
{
[FieldOffset(00)] public Guid Value;
[FieldOffset(00)] public uint Data1;
[FieldOffset(04)] public ushort Data2;
[FieldOffset(06)] public ushort Data3;
[FieldOffset(08)] public ulong Data4;
public Decomposed(Guid value) : this() => Value = value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment