// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Utils
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
///
/// Provides Ascii85 encoding and decoding
/// helpers backed by thread-local buffers to minimize allocations when converting binary data to
/// printable ASCII representations.
///
public static class Ascii85
{
private static readonly ThreadLocal StringBuilderCache = new(() =>
new StringBuilder()
);
private static readonly ThreadLocal ChunkCache = new(() => new byte[4]);
private static readonly ThreadLocal EncodedCache = new(() => new char[5]);
private static readonly ThreadLocal> ByteListCache = new(() => new List());
private static readonly uint[] Pow85 = { 85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1 };
///
/// Encodes the supplied binary into its Ascii85 string
/// representation. The method reuses thread-local buffers to avoid GC spikes during
/// frequent conversions.
///
/// Binary payload to convert.
/// Ascii85 encoded string or null when is null.
public static string Encode(byte[] data)
{
if (data == null)
{
return null;
}
StringBuilder stringBuilder = StringBuilderCache.Value;
stringBuilder.Clear();
int index = 0;
byte[] chunk = ChunkCache.Value;
char[] encoded = EncodedCache.Value;
while (index < data.Length)
{
Array.Clear(chunk, 0, chunk.Length);
int chunkLength = 0;
for (int i = 0; i < 4 && index < data.Length; ++i)
{
chunk[i] = data[index++];
chunkLength++;
}
uint val =
((uint)chunk[0] << 24)
| ((uint)chunk[1] << 16)
| ((uint)chunk[2] << 8)
| chunk[3];
if (val == 0 && chunkLength == 4)
{
stringBuilder.Append('z');
continue;
}
Array.Clear(encoded, 0, encoded.Length);
for (int i = 0; i < encoded.Length; ++i)
{
encoded[i] = (char)(val / Pow85[i] + '!');
val %= Pow85[i];
}
for (int i = 0; i < chunkLength + 1; ++i)
{
stringBuilder.Append(encoded[i]);
}
}
return stringBuilder.ToString();
}
///
/// Decodes Ascii85 text back into its binary representation.
///
/// Ascii85 encoded text. Blank strings yield an empty byte array.
/// The decoded byte array. Returns an empty array when the input is null or empty.
public static byte[] Decode(string encoded)
{
if (string.IsNullOrEmpty(encoded))
{
return Array.Empty();
}
encoded = encoded.Replace("z", "!!!!!");
List result = ByteListCache.Value;
result.Clear();
int index = 0;
char[] chunk = EncodedCache.Value;
byte[] decodedBytes = ChunkCache.Value;
while (index < encoded.Length)
{
Array.Fill(chunk, (char)117);
int chunkLen = 0;
for (int i = 0; i < 5 && index < encoded.Length; ++i)
{
chunk[i] = encoded[index++];
chunkLen++;
}
uint val = 0;
for (int i = 0; i < 5; ++i)
{
val += (uint)(chunk[i] - '!') * Pow85[i];
}
decodedBytes[0] = (byte)(val >> 24);
decodedBytes[1] = (byte)(val >> 16);
decodedBytes[2] = (byte)(val >> 8);
decodedBytes[3] = (byte)val;
for (int i = 0; i < chunkLen - 1; ++i)
{
result.Add(decodedBytes[i]);
}
}
return result.ToArray();
}
}
}