Last active
October 28, 2025 21:26
-
-
Save MiloszKrajewski/442136d5b81553dbcb405fbca1daace6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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