Skip to content

Instantly share code, notes, and snippets.

@MiloszKrajewski
Last active October 28, 2025 21:26
Show Gist options
  • Save MiloszKrajewski/442136d5b81553dbcb405fbca1daace6 to your computer and use it in GitHub Desktop.
Save MiloszKrajewski/442136d5b81553dbcb405fbca1daace6 to your computer and use it in GitHub Desktop.
public static class Url64
{
public static int DecodeBytesMaxSize(int textLength) =>
(((textLength + 3) & ~0x03) >> 2) * 3;
public static int EncodeBytesMaxSize(int bytesCount) =>
((bytesCount + 2) / 3) << 2;
public static int EncodeBytes(ReadOnlySpan<byte> bytes, Span<char> chars)
{
var bytesLength = bytes.Length;
var maximumChars = ((bytesLength + 2) / 3) << 2;
if (chars.Length < maximumChars)
ThrowInsufficientSpace(chars.Length, maximumChars, "char(s)");
var success = Convert.TryToBase64Chars(bytes, chars, out var actualChars);
Debug.Assert(success);
chars = chars[..actualChars].TrimEnd('=');
chars.Replace('/', '_');
chars.Replace('+', '-');
return chars.Length;
}
public static int DecodeBytes(ReadOnlySpan<char> chars, Span<byte> bytes)
{
var paddedLength = (chars.Length + 3) & ~0x03;
var maximumBytes = (paddedLength >> 2) * 3;
if (bytes.Length < maximumBytes)
ThrowInsufficientSpace(bytes.Length, maximumBytes, "byte(s)");
Span<char> padded = stackalloc char[paddedLength];
chars.CopyTo(padded);
padded[chars.Length..].Fill('=');
padded.Replace('_', '/');
padded.Replace('-', '+');
var success = Convert.TryFromBase64Chars(padded, bytes, out var actualBytes);
Debug.Assert(success);
return actualBytes;
}
public static string EncodeBytes(ReadOnlySpan<byte> bytes)
{
var maximumChars = EncodeBytesMaxSize(bytes.Length);
Span<char> chars = stackalloc char[maximumChars];
var actualChars = EncodeBytes(bytes, chars);
return new string(chars[..actualChars]);
}
public static byte[] DecodeBytes(ReadOnlySpan<char> text)
{
var bytesNeeded = DecodeBytesMaxSize(text.Length);
var bytes = new byte[bytesNeeded];
var actualBytes = DecodeBytes(text, bytes.AsSpan());
if (actualBytes == bytesNeeded) return bytes;
Array.Resize(ref bytes, actualBytes);
return bytes;
}
public static int EncodeStringMaxSize(int textLength) =>
EncodeBytesMaxSize(Encoding.UTF8.GetMaxByteCount(textLength));
public static int DecodeStringMaxSize(int textLength) =>
Encoding.UTF8.GetMaxCharCount(DecodeBytesMaxSize(textLength));
public static int EncodeString(ReadOnlySpan<char> text, Span<char> output)
{
var maximumBytes = Encoding.UTF8.GetMaxByteCount(text.Length);
Span<byte> bytes = stackalloc byte[maximumBytes];
var success = Encoding.UTF8.TryGetBytes(text, bytes, out var actualBytes);
Debug.Assert(success);
return EncodeBytes(bytes[..actualBytes], output);
}
public static int DecodeString(ReadOnlySpan<char> encoded, Span<char> output)
{
var maximumBytes = DecodeBytesMaxSize(encoded.Length);
Span<byte> bytes = stackalloc byte[maximumBytes];
var actualBytes = DecodeBytes(encoded, bytes);
var maximumChars = Encoding.UTF8.GetMaxCharCount(actualBytes);
if (output.Length < maximumChars)
ThrowInsufficientSpace(output.Length, maximumChars, "char(s)");
var success = Encoding.UTF8.TryGetChars(bytes[..actualBytes], output, out var actualChars);
Debug.Assert(success);
return actualChars;
}
public static string EncodeString(string text)
{
var maximumChars = EncodeStringMaxSize(text.Length);
Span<char> encodedChars = stackalloc char[maximumChars];
var actualChars = EncodeString(text.AsSpan(), encodedChars);
return new string(encodedChars[..actualChars]);
}
public static string DecodeString(ReadOnlySpan<char> encoded)
{
var maximumChars = DecodeStringMaxSize(encoded.Length);
Span<char> decodedChars = stackalloc char[maximumChars];
var actualChars = DecodeString(encoded, decodedChars);
return new string(decodedChars[..actualChars]);
}
public static unsafe int EncodeGuid(Guid guid, Span<char> output)
{
var bytes = new ReadOnlySpan<byte>(&guid, 16);
return EncodeBytes(bytes, output);
}
public static string EncodeGuid(Guid guid)
{
const int maximumChars = 24; // Precomputed maximum size for encoded GUID
Debug.Assert(EncodeBytesMaxSize(16) <= maximumChars, "Precomputed size mismatch");
Span<char> encodedChars = stackalloc char[maximumChars];
var actualChars = EncodeGuid(guid, encodedChars);
return new string(encodedChars[..actualChars]);
}
public static Guid DecodeGuid(ReadOnlySpan<char> encoded)
{
if (encoded.Length > 24) ThrowNotUrl64Guid();
const int maximumBytes = 18; // Precomputed maximum size for decoded GUID (with padding)
Debug.Assert(DecodeBytesMaxSize(24) <= maximumBytes, "Precomputed size mismatch");
Span<byte> guidBytes = stackalloc byte[maximumBytes];
var actualBytes = DecodeBytes(encoded, guidBytes);
if (actualBytes != 16) ThrowNotUrl64Guid();
return new Guid(guidBytes);
}
[DoesNotReturn]
private static void ThrowNotUrl64Guid() =>
throw new ArgumentException("Provided value is not Url64 encoded GUID");
[DoesNotReturn]
private static void ThrowInsufficientSpace(int actualLength, int expectedLength, string unit) =>
throw new ArgumentException(
$"Insufficient buffer size: expected {expectedLength} {unit}, got {actualLength}.");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment