AyCode.Core/AyCode.Core/Compression/BrotliHelper.cs

196 lines
6.5 KiB
C#

using AyCode.Core.Serializers.Toons;
using System.Buffers;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Text;
namespace AyCode.Core.Compression;
/// <summary>
/// Brotli compression/decompression helper for SignalR message transport.
/// Optimized for zero-allocation scenarios with pooled buffers.
/// </summary>
public static class BrotliHelper
{
//[ToonDescription("Unique identifier for the person")]
private const int DefaultBufferSize = 4096;
private const int MaxStackAllocSize = 1024;
#region Compression
/// <summary>
/// Compresses a string using Brotli compression with pooled buffers.
/// </summary>
public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
if (string.IsNullOrEmpty(text))
return [];
// Use stack allocation for small strings, pooled buffer for larger
var maxByteCount = Encoding.UTF8.GetMaxByteCount(text.Length);
if (maxByteCount <= MaxStackAllocSize)
{
Span<byte> utf8Bytes = stackalloc byte[maxByteCount];
var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), utf8Bytes);
return CompressSpan(utf8Bytes[..actualLength], compressionLevel);
}
var rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
try
{
var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), rentedBuffer);
return CompressSpan(rentedBuffer.AsSpan(0, actualLength), compressionLevel);
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
/// <summary>
/// Compresses a byte array using Brotli compression.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
=> data == null || data.Length == 0 ? [] : CompressSpan(data.AsSpan(), compressionLevel);
/// <summary>
/// Compresses a ReadOnlySpan using Brotli compression with pooled output buffer.
/// </summary>
public static byte[] CompressSpan(ReadOnlySpan<byte> data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
if (data.IsEmpty)
return [];
using var outputStream = new MemoryStream();
using (var brotliStream = new BrotliStream(outputStream, compressionLevel, leaveOpen: true))
{
brotliStream.Write(data);
}
return outputStream.ToArray();
}
#endregion
#region Decompression
/// <summary>
/// Decompresses Brotli-compressed data to a string.
/// Consider using Decompress + direct UTF-8 deserialization for better performance.
/// </summary>
public static string DecompressToString(byte[] compressedData)
{
if (compressedData == null || compressedData.Length == 0)
return string.Empty;
var decompressedBytes = Decompress(compressedData);
return Encoding.UTF8.GetString(decompressedBytes);
}
/// <summary>
/// Decompresses Brotli-compressed data to a byte array.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Decompress(byte[] compressedData)
{
if (compressedData == null || compressedData.Length == 0)
return [];
return DecompressCore(compressedData);
}
/// <summary>
/// Decompresses Brotli-compressed data from a ReadOnlySpan.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] DecompressSpan(ReadOnlySpan<byte> compressedData)
{
if (compressedData.IsEmpty)
return [];
return DecompressCore(compressedData.ToArray());
}
private static byte[] DecompressCore(byte[] compressedData)
{
using var inputStream = new MemoryStream(compressedData, writable: false);
using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
using var outputStream = new MemoryStream();
brotliStream.CopyTo(outputStream);
return outputStream.ToArray();
}
/// <summary>
/// Decompresses to a rented buffer. Caller must return the buffer to ArrayPool.
/// Returns the actual decompressed length.
/// </summary>
public static (byte[] Buffer, int Length) DecompressToRentedBuffer(ReadOnlySpan<byte> compressedData)
{
if (compressedData.IsEmpty)
return ([], 0);
// Estimate decompressed size (typically 3-10x compressed for text)
var estimatedSize = Math.Max(compressedData.Length * 4, DefaultBufferSize);
var outputBuffer = ArrayPool<byte>.Shared.Rent(estimatedSize);
using var inputStream = new MemoryStream(compressedData.ToArray(), writable: false);
using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
var totalRead = 0;
int bytesRead;
while ((bytesRead = brotliStream.Read(outputBuffer.AsSpan(totalRead))) > 0)
{
totalRead += bytesRead;
// Need larger buffer
if (totalRead >= outputBuffer.Length - DefaultBufferSize)
{
var newBuffer = ArrayPool<byte>.Shared.Rent(outputBuffer.Length * 2);
outputBuffer.AsSpan(0, totalRead).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(outputBuffer);
outputBuffer = newBuffer;
}
}
return (outputBuffer, totalRead);
}
#endregion
#region Utility
/// <summary>
/// Checks if the data appears to be Brotli compressed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsBrotliCompressed(byte[] data)
{
if (data == null || data.Length < 4)
return false;
var firstByte = data[0];
// If it starts with '{' (0x7B) or '[' (0x5B), it's likely uncompressed JSON
if (firstByte == 0x7B || firstByte == 0x5B)
return false;
// Try to decompress - if it fails, it's not Brotli
try
{
using var inputStream = new MemoryStream(data, 0, Math.Min(data.Length, 64), writable: false);
using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
Span<byte> buffer = stackalloc byte[1];
return brotliStream.Read(buffer) >= 0;
}
catch
{
return false;
}
}
#endregion
}