196 lines
6.5 KiB
C#
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
|
|
}
|