From a29704e262243def043885814ad333cfcda705f9 Mon Sep 17 00:00:00 2001
From: tehtelev <50070668+tehtelev@users.noreply.github.com>
Date: Thu, 9 Jul 2026 10:29:06 +0300
Subject: [PATCH] Ascii85 Improvements
---
Math/Ascii85.cs | 265 +++++++++++++++++++++++++++++-------------------
1 file changed, 159 insertions(+), 106 deletions(-)
diff --git a/Math/Ascii85.cs b/Math/Ascii85.cs
index 592b0d792..f76a2997c 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
+ };
}
}