diff options
Diffstat (limited to 'lib/Utils/src/VnEncoding.cs')
-rw-r--r-- | lib/Utils/src/VnEncoding.cs | 914 |
1 files changed, 914 insertions, 0 deletions
diff --git a/lib/Utils/src/VnEncoding.cs b/lib/Utils/src/VnEncoding.cs new file mode 100644 index 0000000..94d8a1a --- /dev/null +++ b/lib/Utils/src/VnEncoding.cs @@ -0,0 +1,914 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnEncoding.cs +* +* VnEncoding.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; +using System.Buffers; +using System.Text.Json; +using System.Threading; +using System.Buffers.Text; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + + +namespace VNLib.Utils +{ + /// <summary> + /// Contains static methods for encoding data + /// </summary> + public static class VnEncoding + { + /// <summary> + /// Encodes a <see cref="ReadOnlySpan{T}"/> with the specified <see cref="Encoding"/> to a <see cref="VnMemoryStream"/> that must be disposed by the user + /// </summary> + /// <param name="data">Data to be encoded</param> + /// <param name="encoding"><see cref="Encoding"/> to encode data with</param> + /// <returns>A <see cref="Stream"/> contating the encoded data</returns> + public static VnMemoryStream GetMemoryStream(in ReadOnlySpan<char> data, Encoding encoding) + { + _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); + //Create new memory handle to copy data to + MemoryHandle<byte>? handle = null; + try + { + //get number of bytes + int byteCount = encoding.GetByteCount(data); + //resize the handle to fit the data + handle = Memory.Memory.Shared.Alloc<byte>(byteCount); + //encode + int size = encoding.GetBytes(data, handle); + //Consume the handle into a new vnmemstream and return it + return VnMemoryStream.ConsumeHandle(handle, size, true); + } + catch + { + //Dispose the handle if there is an excpetion + handle?.Dispose(); + throw; + } + } + + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <typeparam name="T">The type of the object to deserialize</typeparam> + /// <param name="data">Binary data to read from</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? JSONDeserializeFromBinary<T>(Stream? data, JsonSerializerOptions? options = null) + { + //Return default if null + if (data == null) + { + return default; + } + //Create a memory stream as a buffer + using VnMemoryStream ms = new(); + //Copy stream data to memory + data.CopyTo(ms, null); + if (ms.Length > 0) + { + //Rewind + ms.Position = 0; + //Recover data from stream + return ms.AsSpan().AsJsonObject<T>(options); + } + //Stream is empty + return default; + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <param name="data">Binary data to read from</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static object? JSONDeserializeFromBinary(Stream? data, Type type, JsonSerializerOptions? options = null) + { + //Return default if null + if (data == null) + { + return default; + } + //Create a memory stream as a buffer + using VnMemoryStream ms = new(); + //Copy stream data to memory + data.CopyTo(ms, null); + if (ms.Length > 0) + { + //Rewind + ms.Position = 0; + //Recover data from stream + return JsonSerializer.Deserialize(ms.AsSpan(), type, options); + } + //Stream is empty + return default; + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <typeparam name="T">The type of the object to deserialize</typeparam> + /// <param name="data">Binary data to read from</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <param name="cancellationToken"></param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static ValueTask<T?> JSONDeserializeFromBinaryAsync<T>(Stream? data, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + //Return default if null + return data == null || data.Length == 0 ? ValueTask.FromResult<T?>(default) : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken); + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <param name="data">Binary data to read from</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <param name="cancellationToken"></param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static ValueTask<object?> JSONDeserializeFromBinaryAsync(Stream? data, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + //Return default if null + return data == null || data.Length == 0 ? ValueTask.FromResult<object?>(default) : JsonSerializer.DeserializeAsync(data, type, options, cancellationToken); + } + /// <summary> + /// Attempts to serialize the object to json and write the encoded data to the stream + /// </summary> + /// <typeparam name="T">The object type to serialize</typeparam> + /// <param name="data">The object to serialize</param> + /// <param name="output">The <see cref="Stream"/> to write output data to</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> + /// <exception cref="JsonException"></exception> + public static void JSONSerializeToBinary<T>(T data, Stream output, JsonSerializerOptions? options = null) + { + //return if null + if(data == null) + { + return; + } + //Serialize + JsonSerializer.Serialize(output, data, options); + } + /// <summary> + /// Attempts to serialize the object to json and write the encoded data to the stream + /// </summary> + /// <param name="data">The object to serialize</param> + /// <param name="output">The <see cref="Stream"/> to write output data to</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> + /// <exception cref="JsonException"></exception> + public static void JSONSerializeToBinary(object data, Stream output, Type type, JsonSerializerOptions? options = null) + { + //return if null + if (data == null) + { + return; + } + //Serialize + JsonSerializer.Serialize(output, data, type, options); + } + + #region Base32 + + private const string RFC_4648_BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + /// <summary> + /// Attempts to convert the specified byte sequence in Base32 encoding + /// and writing the encoded data to the output buffer. + /// </summary> + /// <param name="input">The input buffer to convert</param> + /// <param name="output">The ouput buffer to write encoded data to</param> + /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns> + public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, Span<char> output) + { + ForwardOnlyWriter<char> writer = new(output); + return TryToBase32Chars(input, ref writer); + } + /// <summary> + /// Attempts to convert the specified byte sequence in Base32 encoding + /// and writing the encoded data to the output buffer. + /// </summary> + /// <param name="input">The input buffer to convert</param> + /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write encoded chars to</param> + /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns> + public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer) + { + //calculate char size + int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + //Make sure there is enough room + if(charCount > writer.RemainingSize) + { + return false; + } + //sliding window over input buffer + ForwardOnlyReader<byte> reader = new(input); + + while (reader.WindowSize > 0) + { + //Convert the current window + WriteChars(reader.Window, ref writer); + //shift the window + reader.Advance(Math.Min(5, reader.WindowSize)); + } + return writer.Written; + } + private unsafe static void WriteChars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer) + { + //Get the input buffer as long + ulong inputAsLong = 0; + //Get a byte pointer over the ulong to index it as a byte buffer + byte* buffer = (byte*)&inputAsLong; + //Check proc endianness + if (BitConverter.IsLittleEndian) + { + //store each byte consecutivley and allow for padding + for (int i = 0; (i < 5 && i < input.Length); i++) + { + //Write bytes from upper to lower byte order for little endian systems + buffer[7 - i] = input[i]; + } + } + else + { + //store each byte consecutivley and allow for padding + for (int i = 0; (i < 5 && i < input.Length); i++) + { + //Write bytes from lower to upper byte order for Big Endian systems + buffer[i] = input[i]; + } + } + int rounds = (input.Length) switch + { + 1 => 2, + 2 => 4, + 3 => 5, + 4 => 7, + _ => 8 + }; + //Convert each byte segment up to the number of bytes encoded + for (int i = 0; i < rounds; i++) + { + //store the leading byte + byte val = buffer[7]; + //right shift the value to lower 5 bits + val >>= 3; + //Lookup charcode + char base32Char = RFC_4648_BASE32_CHARS[val]; + //append the character to the writer + writer.Append(base32Char); + //Shift input left by 5 bits so the next 5 bits can be read + inputAsLong <<= 5; + } + //Fill remaining bytes with padding chars + for(; rounds < 8; rounds++) + { + //Append trailing '=' + writer.Append('='); + } + } + + /// <summary> + /// Attempts to decode the Base32 encoded string + /// </summary> + /// <param name="input">The Base32 encoded data to decode</param> + /// <param name="output">The output buffer to write decoded data to</param> + /// <returns>The number of bytes written to the output</returns> + /// <exception cref="FormatException"></exception> + public static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, Span<byte> output) + { + ForwardOnlyWriter<byte> writer = new(output); + return TryFromBase32Chars(input, ref writer); + } + /// <summary> + /// Attempts to decode the Base32 encoded string + /// </summary> + /// <param name="input">The Base32 encoded data to decode</param> + /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write decoded bytes to</param> + /// <returns>The number of bytes written to the output</returns> + /// <exception cref="FormatException"></exception> + public unsafe static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, ref ForwardOnlyWriter<byte> writer) + { + //TODO support Big-Endian byte order + + //trim padding characters + input = input.Trim('='); + //Calc the number of bytes to write + int outputSize = (input.Length * 5) / 8; + //make sure the output buffer is large enough + if(writer.RemainingSize < outputSize) + { + return false; + } + + //buffer used to shift data while decoding + ulong bufferLong = 0; + + //re-cast to byte* to index it as a byte buffer + byte* buffer = (byte*)&bufferLong; + + int count = 0, len = input.Length; + while(count < len) + { + //Convert the character to its char code + byte charCode = GetCharCode(input[count]); + //write byte to buffer + buffer[0] |= charCode; + count++; + //If 8 characters have been decoded, reset the buffer + if((count % 8) == 0) + { + //Write the 5 upper bytes in reverse order to the output buffer + for(int j = 0; j < 5; j++) + { + writer.Append(buffer[4 - j]); + } + //reset + bufferLong = 0; + } + //left shift the buffer up by 5 bits + bufferLong <<= 5; + } + //If remaining data has not be written, but has been buffed, finalize it + if (writer.Written < outputSize) + { + //calculate how many bits the buffer still needs to be shifted by (will be 5 bits off because of the previous loop) + int remainingShift = (7 - (count % 8)) * 5; + //right shift the buffer by the remaining bit count + bufferLong <<= remainingShift; + //calc remaining bytes + int remaining = (outputSize - writer.Written); + //Write remaining bytes to the output + for(int i = 0; i < remaining; i++) + { + writer.Append(buffer[4 - i]); + } + } + return writer.Written; + } + private static byte GetCharCode(char c) + { + //cast to byte to get its base 10 value + return c switch + { + //Upper case + 'A' => 0, + 'B' => 1, + 'C' => 2, + 'D' => 3, + 'E' => 4, + 'F' => 5, + 'G' => 6, + 'H' => 7, + 'I' => 8, + 'J' => 9, + 'K' => 10, + 'L' => 11, + 'M' => 12, + 'N' => 13, + 'O' => 14, + 'P' => 15, + 'Q' => 16, + 'R' => 17, + 'S' => 18, + 'T' => 19, + 'U' => 20, + 'V' => 21, + 'W' => 22, + 'X' => 23, + 'Y' => 24, + 'Z' => 25, + //Lower case + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + 'e' => 4, + 'f' => 5, + 'g' => 6, + 'h' => 7, + 'i' => 8, + 'j' => 9, + 'k' => 10, + 'l' => 11, + 'm' => 12, + 'n' => 13, + 'o' => 14, + 'p' => 15, + 'q' => 16, + 'r' => 17, + 's' => 18, + 't' => 19, + 'u' => 20, + 'v' => 21, + 'w' => 22, + 'x' => 23, + 'y' => 24, + 'z' => 25, + //Base10 digits + '2' => 26, + '3' => 27, + '4' => 28, + '5' => 29, + '6' => 30, + '7' => 31, + + _=> throw new FormatException("Character found is not a Base32 encoded character") + }; + } + + /// <summary> + /// Calculates the maximum buffer size required to encode a binary block to its Base32 + /// character encoding + /// </summary> + /// <param name="bufferSize">The binary buffer size used to calculate the base32 buffer size</param> + /// <returns>The maximum size (including padding) of the character buffer required to encode the binary data</returns> + public static int Base32CalcMaxBufferSize(int bufferSize) + { + /* + * Base32 encoding consumes 8 bytes for every 5 bytes + * of input data + */ + //Add up to 8 bytes for padding + return (int)(Math.Ceiling(bufferSize / 5d) * 8) + (8 - (bufferSize % 8)); + } + + /// <summary> + /// Converts the binary buffer to a base32 character string with optional padding characters + /// </summary> + /// <param name="binBuffer">The buffer to encode</param> + /// <param name="withPadding">Should padding be included in the result</param> + /// <returns>The base32 encoded string representation of the specified buffer</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static string ToBase32String(ReadOnlySpan<byte> binBuffer, bool withPadding = false) + { + string value; + //Calculate the base32 entropy to alloc an appropriate buffer (minium buffer of 2 chars) + int entropy = Base32CalcMaxBufferSize(binBuffer.Length); + //Alloc buffer for enough size (2*long bytes) is not an issue + using (UnsafeMemoryHandle<char> charBuffer = Memory.Memory.UnsafeAlloc<char>(entropy)) + { + //Encode + ERRNO encoded = TryToBase32Chars(binBuffer, charBuffer.Span); + if (!encoded) + { + throw new InternalBufferTooSmallException("Base32 char buffer was too small"); + } + //Convert with or w/o padding + if (withPadding) + { + value = charBuffer.Span[0..(int)encoded].ToString(); + } + else + { + value = charBuffer.Span[0..(int)encoded].Trim('=').ToString(); + } + } + return value; + } + /// <summary> + /// Converts the base32 character buffer to its structure representation + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="base32">The base32 character buffer</param> + /// <returns>The new structure of the base32 data</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static T FromBase32String<T>(ReadOnlySpan<char> base32) where T: unmanaged + { + //calc size of bin buffer + int size = base32.Length; + //Rent a bin buffer + using UnsafeMemoryHandle<byte> binBuffer = Memory.Memory.UnsafeAlloc<byte>(size); + //Try to decode the data + ERRNO decoded = TryFromBase32Chars(base32, binBuffer.Span); + //Marshal back to a struct + return decoded ? MemoryMarshal.Read<T>(binBuffer.Span[..(int)decoded]) : throw new InternalBufferTooSmallException("Binbuffer was too small"); + } + + /// <summary> + /// Gets a byte array of the base32 decoded data + /// </summary> + /// <param name="base32">The character array to decode</param> + /// <returns>The byte[] of the decoded binary data, or null if the supplied character array was empty</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static byte[]? FromBase32String(ReadOnlySpan<char> base32) + { + if (base32.IsEmpty) + { + return null; + } + //Buffer size of the base32 string will always be enough buffer space + using UnsafeMemoryHandle<byte> tempBuffer = Memory.Memory.UnsafeAlloc<byte>(base32.Length); + //Try to decode the data + ERRNO decoded = TryFromBase32Chars(base32, tempBuffer.Span); + + return decoded ? tempBuffer.Span[0..(int)decoded].ToArray() : throw new InternalBufferTooSmallException("Binbuffer was too small"); + } + + /// <summary> + /// Converts a structure to its base32 representation and returns the string of its value + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="value">The structure to encode</param> + /// <param name="withPadding">A value indicating if padding should be used</param> + /// <returns>The base32 string representation of the structure</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static string ToBase32String<T>(T value, bool withPadding = false) where T : unmanaged + { + //get the size of the structure + int binSize = Unsafe.SizeOf<T>(); + //Rent a bin buffer + Span<byte> binBuffer = stackalloc byte[binSize]; + //Write memory to buffer + MemoryMarshal.Write(binBuffer, ref value); + //Convert to base32 + return ToBase32String(binBuffer, withPadding); + } + + #endregion + + #region percent encoding + + private static readonly ReadOnlyMemory<byte> HexToUtf8Pos = new byte[16] + { + 0x30, //0 + 0x31, //1 + 0x32, //2 + 0x33, //3 + 0x34, //4 + 0x35, //5 + 0x36, //6 + 0x37, //7 + 0x38, //8 + 0x39, //9 + + 0x41, //A + 0x42, //B + 0x43, //C + 0x44, //D + 0x45, //E + 0x46 //F + }; + + /// <summary> + /// Deterimes the size of the buffer needed to encode a utf8 encoded + /// character buffer into its url-safe percent/hex encoded representation + /// </summary> + /// <param name="utf8Bytes">The buffer to examine</param> + /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param> + /// <returns>The size of the buffer required to encode</returns> + public static unsafe int PercentEncodeCalcBufferSize(ReadOnlySpan<byte> utf8Bytes, in ReadOnlySpan<byte> allowedChars = default) + { + /* + * For every illegal character, the percent encoding adds 3 bytes of + * entropy. So a single byte will be replaced by 3, so adding + * 2 bytes for every illegal character plus the length of the + * intial buffer, we get the size of the buffer needed to + * percent encode. + */ + int count = 0, len = utf8Bytes.Length; + fixed (byte* utfBase = &MemoryMarshal.GetReference(utf8Bytes)) + { + //Find all unsafe characters and add the entropy size + for (int i = 0; i < len; i++) + { + if (!IsUrlSafeChar(utfBase[i], in allowedChars)) + { + count += 2; + } + } + } + //Size is initial buffer size + count bytes + return len + count; + } + + /// <summary> + /// Percent encodes the buffer for utf8 encoded characters to its percent/hex encoded + /// utf8 character representation + /// </summary> + /// <param name="utf8Bytes">The buffer of utf8 encoded characters to encode</param> + /// <param name="utf8Output">The buffer to write the encoded characters to</param> + /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param> + /// <returns>The number of characters encoded and written to the output buffer</returns> + public static ERRNO PercentEncode(ReadOnlySpan<byte> utf8Bytes, Span<byte> utf8Output, in ReadOnlySpan<byte> allowedChars = default) + { + int outPos = 0, len = utf8Bytes.Length; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + for (int i = 0; i < len; i++) + { + byte value = utf8Bytes[i]; + //Check if value is url safe + if(IsUrlSafeChar(value, in allowedChars)) + { + //Skip + utf8Output[outPos++] = value; + } + else + { + //Percent encode + utf8Output[outPos++] = 0x25; // '%' + //Calc and store the encoded by the upper 4 bits + utf8Output[outPos++] = lookupTable[(value & 0xf0) >> 4]; + //Store lower 4 bits in encoded value + utf8Output[outPos++] = lookupTable[value & 0x0f]; + } + } + //Return the size of the output buffer + return outPos; + } + + private static bool IsUrlSafeChar(byte value, in ReadOnlySpan<byte> allowedChars) + { + return + // base10 digits + value > 0x2f && value < 0x3a + // '_' (underscore) + || value == 0x5f + // '-' (hyphen) + || value == 0x2d + // Uppercase letters + || value > 0x40 && value < 0x5b + // lowercase letters + || value > 0x60 && value < 0x7b + // Check allowed characters + || allowedChars.Contains(value); + } + + //TODO: Implement decode with better performance, lookup table or math vs searching the table + + /// <summary> + /// Decodes a percent (url/hex) encoded utf8 encoded character buffer to its utf8 + /// encoded binary value + /// </summary> + /// <param name="utf8Encoded">The buffer containg characters to be decoded</param> + /// <param name="utf8Output">The buffer to write deocded values to</param> + /// <returns>The nuber of bytes written to the output buffer</returns> + /// <exception cref="FormatException"></exception> + public static ERRNO PercentDecode(ReadOnlySpan<byte> utf8Encoded, Span<byte> utf8Output) + { + int outPos = 0, len = utf8Encoded.Length; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + for (int i = 0; i < len; i++) + { + byte value = utf8Encoded[i]; + //Begining of percent encoding character + if(value == 0x25) + { + //Calculate the base16 multiplier from the upper half of the + int multiplier = lookupTable.IndexOf(utf8Encoded[i + 1]); + //get the base16 lower half to add + int lower = lookupTable.IndexOf(utf8Encoded[i + 2]); + //Check format + if(multiplier < 0 || lower < 0) + { + throw new FormatException($"Encoded buffer contains invalid hexadecimal characters following the % character at position {i}"); + } + //Calculate the new value, shift multiplier to the upper 4 bits, then mask + or the lower 4 bits + value = (byte)(((byte)(multiplier << 4)) | ((byte)lower & 0x0f)); + //Advance the encoded index by the two consumed chars + i += 2; + } + utf8Output[outPos++] = value; + } + return outPos; + } + + #endregion + + #region Base64 + + /// <summary> + /// Tries to convert the specified span containing a string representation that is + /// encoded with base-64 digits into a span of 8-bit unsigned integers. + /// </summary> + /// <param name="base64">Base64 character data to recover</param> + /// <param name="buffer">The binary output buffer to write converted characters to</param> + /// <returns>The number of bytes written, or <see cref="ERRNO.E_FAIL"/> of the conversion was unsucessful</returns> + public static ERRNO TryFromBase64Chars(ReadOnlySpan<char> base64, Span<byte> buffer) + { + return Convert.TryFromBase64Chars(base64, buffer, out int bytesWritten) ? bytesWritten : ERRNO.E_FAIL; + } + /// <summary> + /// Tries to convert the 8-bit unsigned integers inside the specified read-only span + /// into their equivalent string representation that is encoded with base-64 digits. + /// You can optionally specify whether to insert line breaks in the return value. + /// </summary> + /// <param name="buffer">The binary buffer to convert characters from</param> + /// <param name="base64">The base64 output buffer</param> + /// <param name="options"> + /// One of the enumeration values that specify whether to insert line breaks in the + /// return value. The default value is System.Base64FormattingOptions.None. + /// </param> + /// <returns>The number of characters encoded, or <see cref="ERRNO.E_FAIL"/> if conversion was unsuccessful</returns> + public static ERRNO TryToBase64Chars(ReadOnlySpan<byte> buffer, Span<char> base64, Base64FormattingOptions options = Base64FormattingOptions.None) + { + return Convert.TryToBase64Chars(buffer, base64, out int charsWritten, options: options) ? charsWritten : ERRNO.E_FAIL; + } + + + /* + * Calc base64 padding chars excluding the length mod 4 = 0 case + * by and-ing 0x03 (011) with the result + */ + + /// <summary> + /// Determines the number of missing padding bytes from the length of the base64 + /// data sequence. + /// <code> + /// Formula (4 - (length mod 4) and 0x03 + /// </code> + /// </summary> + /// <param name="length">The length of the base64 buffer</param> + /// <returns>The number of padding bytes to add to the end of the sequence</returns> + public static int Base64CalcRequiredPadding(int length) => (4 - (length % 4)) & 0x03; + + /// <summary> + /// Converts a base64 utf8 encoded binary buffer to + /// its base64url encoded version + /// </summary> + /// <param name="base64">The binary buffer to convert</param> + public static unsafe void Base64ToUrlSafeInPlace(Span<byte> base64) + { + int len = base64.Length; + + fixed(byte* ptr = &MemoryMarshal.GetReference(base64)) + { + for (int i = 0; i < len; i++) + { + switch (ptr[i]) + { + //Replace + with - (minus) + case 0x2b: + ptr[i] = 0x2d; + break; + //Replace / with _ (underscore) + case 0x2f: + ptr[i] = 0x5f; + break; + } + } + } + } + /// <summary> + /// Converts a base64url encoded utf8 encoded binary buffer to + /// its base64 encoded version + /// </summary> + /// <param name="uft8Base64Url">The base64url utf8 to decode</param> + public static unsafe void Base64FromUrlSafeInPlace(Span<byte> uft8Base64Url) + { + int len = uft8Base64Url.Length; + + fixed (byte* ptr = &MemoryMarshal.GetReference(uft8Base64Url)) + { + for (int i = 0; i < len; i++) + { + switch (ptr[i]) + { + //Replace - with + (plus) + case 0x2d: + ptr[i] = 0x2b; + break; + //Replace _ with / (slash) + case 0x5f: + ptr[i] = 0x2f; + break; + } + } + } + } + + /// <summary> + /// Converts the input buffer to a url safe base64 encoded + /// utf8 buffer from the base64 input buffer. The base64 is copied + /// directly to the output then converted in place. This is + /// just a shortcut method for readonly spans + /// </summary> + /// <param name="base64">The base64 encoded data</param> + /// <param name="base64Url">The base64url encoded output</param> + /// <returns>The size of the <paramref name="base64"/> buffer</returns> + public static ERRNO Base64ToUrlSafe(ReadOnlySpan<byte> base64, Span<byte> base64Url) + { + //Aligned copy to the output buffer + base64.CopyTo(base64Url); + //One time convert the output buffer to url safe + Base64ToUrlSafeInPlace(base64Url); + return base64.Length; + } + + /// <summary> + /// Converts the urlsafe input buffer to a base64 encoded + /// utf8 buffer from the base64 input buffer. The base64 is copied + /// directly to the output then converted in place. This is + /// just a shortcut method for readonly spans + /// </summary> + /// <param name="base64">The base64 encoded data</param> + /// <param name="base64Url">The base64url encoded output</param> + /// <returns>The size of the <paramref name="base64Url"/> buffer</returns> + public static ERRNO Base64FromUrlSafe(ReadOnlySpan<byte> base64Url, Span<byte> base64) + { + //Aligned copy to the output buffer + base64Url.CopyTo(base64); + //One time convert the output buffer to url safe + Base64FromUrlSafeInPlace(base64); + return base64Url.Length; + } + + /// <summary> + /// Decodes a utf8 base64url encoded sequence of data and writes it + /// to the supplied output buffer + /// </summary> + /// <param name="utf8Base64Url">The utf8 base64 url encoded string</param> + /// <param name="output">The output buffer to write the decoded data to</param> + /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns> + public static ERRNO Base64UrlDecode(ReadOnlySpan<byte> utf8Base64Url, Span<byte> output) + { + if(utf8Base64Url.IsEmpty || output.IsEmpty) + { + return ERRNO.E_FAIL; + } + //url deocde + ERRNO count = Base64FromUrlSafe(utf8Base64Url, output); + + //Writer for adding padding bytes + ForwardOnlyWriter<byte> writer = new (output); + writer.Advance(count); + + //Calc required padding + int paddingToAdd = Base64CalcRequiredPadding(writer.Written); + //Add padding bytes + for (; paddingToAdd > 0; paddingToAdd--) + { + writer.Append(0x3d); // '=' + } + + //Base64 decode in place, we should have a buffer large enough + OperationStatus status = Base64.DecodeFromUtf8InPlace(writer.AsSpan(), out int bytesWritten); + //If status is successful return the number of bytes written + return status == OperationStatus.Done ? bytesWritten : ERRNO.E_FAIL; + } + + /// <summary> + /// Decodes a base64url encoded character sequence + /// of data and writes it to the supplied output buffer + /// </summary> + /// <param name="chars">The character buffer to decode</param> + /// <param name="output">The output buffer to write decoded data to</param> + /// <param name="encoding">The character encoding</param> + /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static ERRNO Base64UrlDecode(ReadOnlySpan<char> chars, Span<byte> output, Encoding? encoding = null) + { + if (chars.IsEmpty || output.IsEmpty) + { + return ERRNO.E_FAIL; + } + //Set the encoding to utf8 + encoding ??= Encoding.UTF8; + //get the number of bytes to alloc a buffer + int decodedSize = encoding.GetByteCount(chars); + + //alloc buffer + using UnsafeMemoryHandle<byte> decodeHandle = Memory.Memory.UnsafeAlloc<byte>(decodedSize); + //Get the utf8 binary data + int count = encoding.GetBytes(chars, decodeHandle); + return Base64UrlDecode(decodeHandle.Span[..count], output); + } + + #endregion + } +}
\ No newline at end of file |