diff --git a/Math/Ascii85.cs b/Math/Ascii85.cs index 592b0d79..f76a2997 100644 --- a/Math/Ascii85.cs +++ b/Math/Ascii85.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.IO; @@ -7,164 +7,217 @@ namespace Vintagestory.API.MathTools { /// - /// Converts between binary data and an Ascii85-encoded string. + /// Conversion between binary data and an Ascii85 string /// - /// See Ascii85 at Wikipedia. public static class Ascii85 { /// - /// Encodes the specified byte array in Ascii85. + /// Encodes a byte array into an Ascii85 string /// - /// The bytes to encode. - /// An Ascii85-encoded string representing the input byte array. public static string Encode(byte[] bytes) { if (bytes == null) - throw new ArgumentNullException("bytes"); + throw new ArgumentNullException(nameof(bytes)); - // preallocate a StringBuilder with enough room to store the encoded bytes - StringBuilder sb = new StringBuilder(bytes.Length * 5 / 4); - - // walk the bytes - int count = 0; - uint value = 0; - foreach (byte b in bytes) + // Calculate the exact string length to allocate exactly enough memory + int exactLength = GetEncodedLength(bytes); + return string.Create(exactLength, bytes, static (chars, state) => // Writes the encoded data into the Span { - // build a 32-bit value from the bytes - value |= ((uint)b) << (24 - (count * 8)); - count++; + int charIdx = 0; + int i = 0; + + // Stack-allocate for digits, no GC involvement + Span temp = stackalloc char[5]; - // every 32 bits, convert the previous 4 bytes into 5 Ascii85 characters - if (count == 4) + // Process full groups of 4 bytes + while (i <= state.Length - 4) { - if (value == 0) - sb.Append('z'); + uint val = (uint)(state[i] << 24) | (uint)(state[i + 1] << 16) | + (uint)(state[i + 2] << 8) | state[i + 3]; + if (val == 0) + { + chars[charIdx++] = 'z'; + } else - EncodeValue(sb, value, 0); - count = 0; - value = 0; + { + uint v = val; + for (int k = 4; k >= 0; k--) + { + temp[k] = (char)(c_firstCharacter + (v % 85)); + v /= 85; + } + temp.CopyTo(chars.Slice(charIdx, 5)); + charIdx += 5; + } + i += 4; } - } - // encode any remaining bytes (that weren't a multiple of 4) - if (count > 0) - EncodeValue(sb, value, 4 - count); + // Process the tail (1–3 bytes) + int rem = state.Length - i; + if (rem > 0) + { + uint val = 0; + for (int j = 0; j < rem; j++) + val |= (uint)(state[i + j]) << (24 - 8 * j); + + int charsToWrite = rem + 1; // correct number of characters - return sb.ToString(); + uint v = val; + for (int k = 4; k >= 0; k--) + { + temp[k] = (char)(c_firstCharacter + (v % 85)); + v /= 85; + } + temp.Slice(0, charsToWrite).CopyTo(chars.Slice(charIdx, charsToWrite)); + } + }); } /// - /// Decodes the specified Ascii85 string into the corresponding byte array. + /// Decodes an Ascii85 string into a byte array /// - /// The Ascii85 string. - /// The decoded byte array. public static byte[] Decode(string encoded) { if (encoded == null) - throw new ArgumentNullException("encoded"); + throw new ArgumentNullException(nameof(encoded)); - // preallocate a memory stream with enough capacity to hold the decoded data - using (MemoryStream stream = new MemoryStream(encoded.Length * 4 / 5)) + int decodedLength = GetDecodedLength(encoded); + byte[] result = new byte[decodedLength]; + + int byteIdx = 0; + int count = 0; + uint value = 0; + + for (int i = 0; i < encoded.Length; i++) { - // walk the input string - int count = 0; - uint value = 0; - foreach (char ch in encoded) + char ch = encoded[i]; + if (ch == 'z' && count == 0) + { + result[byteIdx++] = 0; + result[byteIdx++] = 0; + result[byteIdx++] = 0; + result[byteIdx++] = 0; + } + else if (ch < c_firstCharacter || ch > c_lastCharacter) + { + throw new FormatException($"Invalid character '{ch}' in Ascii85 block."); + } + else { - if (ch == 'z' && count == 0) + try { - // handle "z" block specially - DecodeValue(stream, value, 0); + uint add = checked(s_powersOf85[count] * (uint)(ch - c_firstCharacter)); + value = checked(value + add); } - else if (ch < c_firstCharacter || ch > c_lastCharacter) + catch (OverflowException ex) { - throw new FormatException("Invalid character '" + ch + "' in Ascii85 block."); + throw new FormatException("The current group of characters decodes to a value greater than UInt32.MaxValue.", ex); } - else - { - // build a 32-bit value from the input characters - try - { - checked { value += (uint)(s_powersOf85[count] * (ch - c_firstCharacter)); } - } - catch (OverflowException ex) - { - throw new FormatException("The current group of characters decodes to a value greater than UInt32.MaxValue.", ex); - } - count++; + count++; - // every five characters, convert the characters into the equivalent byte array - if (count == 5) - { - DecodeValue(stream, value, 0); - count = 0; - value = 0; - } + if (count == 5) + { + result[byteIdx++] = (byte)(value >> 24); + result[byteIdx++] = (byte)(value >> 16); + result[byteIdx++] = (byte)(value >> 8); + result[byteIdx++] = (byte)(value); + count = 0; + value = 0; } } + } - if (count == 1) - { - throw new FormatException("The final Ascii85 block must contain more than one character."); - } - else if (count > 1) + if (count == 1) + throw new FormatException("The final Ascii85 block must contain more than one character."); + + if (count > 1) + { + // Pad missing characters with maximum values + for (int padding = count; padding < 5; padding++) { - // decode any remaining characters - for (int padding = count; padding < 5; padding++) + try { - try - { - checked { value += 84 * s_powersOf85[padding]; } - } - catch (OverflowException ex) - { - throw new FormatException("The current group of characters decodes to a value greater than UInt32.MaxValue.", ex); - } + value = checked(value + 84u * s_powersOf85[padding]); + } + catch (OverflowException ex) + { + throw new FormatException("The current group of characters decodes to a value greater than UInt32.MaxValue.", ex); } - DecodeValue(stream, value, 5 - count); } - return stream.ToArray(); + result[byteIdx++] = (byte)(value >> 24); + if (count > 2) result[byteIdx++] = (byte)(value >> 16); + if (count > 3) result[byteIdx++] = (byte)(value >> 8); } + + return result; } - // Writes the Ascii85 characters for a 32-bit value to a StringBuilder. - private static void EncodeValue(StringBuilder sb, uint value, int paddingBytes) - { - char[] encoded = new char[5]; - for (int index = 4; index >= 0; index--) + // Helper methods for length calculation + + private static int GetEncodedLength(byte[] bytes) + { + int len = 0; + int i = 0; + while (i <= bytes.Length - 4) { - encoded[index] = (char)((value % 85) + c_firstCharacter); - value /= 85; + // Check for a zero group for a shortened notation + if (bytes[i] == 0 && bytes[i + 1] == 0 && bytes[i + 2] == 0 && bytes[i + 3] == 0) + len += 1; // 'z' + else + len += 5; + i += 4; } - - if (paddingBytes != 0) - Array.Resize(ref encoded, 5 - paddingBytes); - - sb.Append(encoded); + int rem = bytes.Length - i; + if (rem > 0) + len += rem + 1; // partial group + return len; } - // Writes the bytes of a 32-bit value to a stream. - private static void DecodeValue(Stream stream, uint value, int paddingChars) + private static int GetDecodedLength(string encoded) { - stream.WriteByte((byte)(value >> 24)); - if (paddingChars == 3) - return; - stream.WriteByte((byte)((value >> 16) & 0xFF)); - if (paddingChars == 2) - return; - stream.WriteByte(((byte)((value >> 8) & 0xFF))); - if (paddingChars == 1) - return; - stream.WriteByte((byte)(value & 0xFF)); + int count = 0; + int bytes = 0; + foreach (char ch in encoded) + { + if (ch == 'z' && count == 0) + { + bytes += 4; + } + else if (ch >= c_firstCharacter && ch <= c_lastCharacter) + { + count++; + if (count == 5) + { + bytes += 4; + count = 0; + } + } + else + { + throw new FormatException($"Invalid character '{ch}' in Ascii85 block."); + } + } + if (count == 1) + throw new FormatException("The final Ascii85 block must contain more than one character."); + if (count > 1) + bytes += count - 1; + return bytes; } - // the first and last characters used in the Ascii85 encoding character set + // Constants and static data + const char c_firstCharacter = '!'; const char c_lastCharacter = 'u'; - static readonly uint[] s_powersOf85 = new uint[] { 85u * 85u * 85u * 85u, 85u * 85u * 85u, 85u * 85u, 85u, 1 }; + static readonly uint[] s_powersOf85 = { + 85u * 85u * 85u * 85u, + 85u * 85u * 85u, + 85u * 85u, + 85u, + 1 + }; } }