Reformat / cleanup
This commit is contained in:
@@ -3,34 +3,34 @@ using System.Buffers.Binary;
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed header prefix for compressed payload blobs.
|
||||
/// Fixed header prefix for compressed payload blobs.
|
||||
/// </summary>
|
||||
public readonly struct CompressedPayloadHeader
|
||||
{
|
||||
public const int Size = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Compression codec used for payload bytes.
|
||||
/// Compression codec used for payload bytes.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Original uncompressed payload length.
|
||||
/// Original uncompressed payload length.
|
||||
/// </summary>
|
||||
public int OriginalLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compressed payload length.
|
||||
/// Compressed payload length.
|
||||
/// </summary>
|
||||
public int CompressedLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// CRC32 checksum of compressed payload bytes.
|
||||
/// CRC32 checksum of compressed payload bytes.
|
||||
/// </summary>
|
||||
public uint Checksum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompressedPayloadHeader"/> class.
|
||||
/// Initializes a new instance of the <see cref="CompressedPayloadHeader" /> class.
|
||||
/// </summary>
|
||||
/// <param name="codec">Compression codec used for payload bytes.</param>
|
||||
/// <param name="originalLength">Original uncompressed payload length.</param>
|
||||
@@ -50,19 +50,20 @@ public readonly struct CompressedPayloadHeader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create.
|
||||
/// Create.
|
||||
/// </summary>
|
||||
/// <param name="codec">Compression codec used for payload bytes.</param>
|
||||
/// <param name="originalLength">Original uncompressed payload length.</param>
|
||||
/// <param name="compressedPayload">Compressed payload bytes.</param>
|
||||
public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, ReadOnlySpan<byte> compressedPayload)
|
||||
public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength,
|
||||
ReadOnlySpan<byte> compressedPayload)
|
||||
{
|
||||
var checksum = ComputeChecksum(compressedPayload);
|
||||
uint checksum = ComputeChecksum(compressedPayload);
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedPayload.Length, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write To.
|
||||
/// Write To.
|
||||
/// </summary>
|
||||
/// <param name="destination">Destination span that receives the serialized header.</param>
|
||||
public void WriteTo(Span<byte> destination)
|
||||
@@ -80,7 +81,7 @@ public readonly struct CompressedPayloadHeader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read From.
|
||||
/// Read From.
|
||||
/// </summary>
|
||||
/// <param name="source">Source span containing a serialized header.</param>
|
||||
public static CompressedPayloadHeader ReadFrom(ReadOnlySpan<byte> source)
|
||||
@@ -89,14 +90,14 @@ public readonly struct CompressedPayloadHeader
|
||||
throw new ArgumentException($"Source must be at least {Size} bytes.", nameof(source));
|
||||
|
||||
var codec = (CompressionCodec)source[0];
|
||||
var originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4));
|
||||
var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4));
|
||||
var checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4));
|
||||
int originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4));
|
||||
int compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4));
|
||||
uint checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4));
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedLength, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate Checksum.
|
||||
/// Validate Checksum.
|
||||
/// </summary>
|
||||
/// <param name="compressedPayload">Compressed payload bytes to validate.</param>
|
||||
public bool ValidateChecksum(ReadOnlySpan<byte> compressedPayload)
|
||||
@@ -105,10 +106,13 @@ public readonly struct CompressedPayloadHeader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute Checksum.
|
||||
/// Compute Checksum.
|
||||
/// </summary>
|
||||
/// <param name="payload">Payload bytes.</param>
|
||||
public static uint ComputeChecksum(ReadOnlySpan<byte> payload) => Crc32Calculator.Compute(payload);
|
||||
public static uint ComputeChecksum(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
return Crc32Calculator.Compute(payload);
|
||||
}
|
||||
|
||||
private static class Crc32Calculator
|
||||
{
|
||||
@@ -116,15 +120,15 @@ public readonly struct CompressedPayloadHeader
|
||||
private static readonly uint[] Table = CreateTable();
|
||||
|
||||
/// <summary>
|
||||
/// Compute.
|
||||
/// Compute.
|
||||
/// </summary>
|
||||
/// <param name="payload">Payload bytes.</param>
|
||||
public static uint Compute(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
uint crc = 0xFFFFFFFFu;
|
||||
for (int i = 0; i < payload.Length; i++)
|
||||
var crc = 0xFFFFFFFFu;
|
||||
for (var i = 0; i < payload.Length; i++)
|
||||
{
|
||||
var index = (crc ^ payload[i]) & 0xFF;
|
||||
uint index = (crc ^ payload[i]) & 0xFF;
|
||||
crc = (crc >> 8) ^ Table[index];
|
||||
}
|
||||
|
||||
@@ -137,10 +141,7 @@ public readonly struct CompressedPayloadHeader
|
||||
for (uint i = 0; i < table.Length; i++)
|
||||
{
|
||||
uint value = i;
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1;
|
||||
}
|
||||
for (var bit = 0; bit < 8; bit++) value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1;
|
||||
|
||||
table[i] = value;
|
||||
}
|
||||
@@ -148,4 +149,4 @@ public readonly struct CompressedPayloadHeader
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Supported payload compression codecs.
|
||||
/// Supported payload compression codecs.
|
||||
/// </summary>
|
||||
public enum CompressionCodec : byte
|
||||
{
|
||||
None = 0,
|
||||
Brotli = 1,
|
||||
Deflate = 2
|
||||
}
|
||||
}
|
||||
@@ -3,52 +3,52 @@ using System.IO.Compression;
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Compression configuration for document payload processing.
|
||||
/// Compression configuration for document payload processing.
|
||||
/// </summary>
|
||||
public sealed class CompressionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default compression options (compression disabled).
|
||||
/// Default compression options (compression disabled).
|
||||
/// </summary>
|
||||
public static CompressionOptions Default { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Enables payload compression for new writes.
|
||||
/// Enables payload compression for new writes.
|
||||
/// </summary>
|
||||
public bool EnableCompression { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum payload size (bytes) required before compression is attempted.
|
||||
/// Minimum payload size (bytes) required before compression is attempted.
|
||||
/// </summary>
|
||||
public int MinSizeBytes { get; init; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum percentage of size reduction required to keep compressed output.
|
||||
/// Minimum percentage of size reduction required to keep compressed output.
|
||||
/// </summary>
|
||||
public int MinSavingsPercent { get; init; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Preferred default codec for new writes.
|
||||
/// Preferred default codec for new writes.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec { get; init; } = CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Compression level passed to codec implementations.
|
||||
/// Compression level passed to codec implementations.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; init; } = CompressionLevel.Fastest;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed decompressed payload size.
|
||||
/// Maximum allowed decompressed payload size.
|
||||
/// </summary>
|
||||
public int MaxDecompressedSizeBytes { get; init; } = 16 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Optional maximum input size allowed for compression attempts.
|
||||
/// Optional maximum input size allowed for compression attempts.
|
||||
/// </summary>
|
||||
public int? MaxCompressionInputBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes and validates compression options.
|
||||
/// Normalizes and validates compression options.
|
||||
/// </summary>
|
||||
/// <param name="options">Optional user-provided options.</param>
|
||||
internal static CompressionOptions Normalize(CompressionOptions? options)
|
||||
@@ -59,17 +59,20 @@ public sealed class CompressionOptions
|
||||
throw new ArgumentOutOfRangeException(nameof(MinSizeBytes), "MinSizeBytes must be non-negative.");
|
||||
|
||||
if (candidate.MinSavingsPercent is < 0 or > 100)
|
||||
throw new ArgumentOutOfRangeException(nameof(MinSavingsPercent), "MinSavingsPercent must be between 0 and 100.");
|
||||
throw new ArgumentOutOfRangeException(nameof(MinSavingsPercent),
|
||||
"MinSavingsPercent must be between 0 and 100.");
|
||||
|
||||
if (!Enum.IsDefined(candidate.Codec))
|
||||
throw new ArgumentOutOfRangeException(nameof(Codec), $"Unsupported codec: {candidate.Codec}.");
|
||||
|
||||
if (candidate.MaxDecompressedSizeBytes <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxDecompressedSizeBytes), "MaxDecompressedSizeBytes must be greater than 0.");
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxDecompressedSizeBytes),
|
||||
"MaxDecompressedSizeBytes must be greater than 0.");
|
||||
|
||||
if (candidate.MaxCompressionInputBytes is <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxCompressionInputBytes), "MaxCompressionInputBytes must be greater than 0 when provided.");
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxCompressionInputBytes),
|
||||
"MaxCompressionInputBytes must be greater than 0 when provided.");
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@ using System.IO.Compression;
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Compression codec registry and utility service.
|
||||
/// Compression codec registry and utility service.
|
||||
/// </summary>
|
||||
public sealed class CompressionService
|
||||
{
|
||||
private readonly ConcurrentDictionary<CompressionCodec, ICompressionCodec> _codecs = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompressionService"/> class.
|
||||
/// Initializes a new instance of the <see cref="CompressionService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="additionalCodecs">Optional additional codecs to register.</param>
|
||||
public CompressionService(IEnumerable<ICompressionCodec>? additionalCodecs = null)
|
||||
@@ -24,14 +24,11 @@ public sealed class CompressionService
|
||||
if (additionalCodecs == null)
|
||||
return;
|
||||
|
||||
foreach (var codec in additionalCodecs)
|
||||
{
|
||||
RegisterCodec(codec);
|
||||
}
|
||||
foreach (var codec in additionalCodecs) RegisterCodec(codec);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers or replaces a compression codec implementation.
|
||||
/// Registers or replaces a compression codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec implementation to register.</param>
|
||||
public void RegisterCodec(ICompressionCodec codec)
|
||||
@@ -41,18 +38,21 @@ public sealed class CompressionService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a registered codec implementation.
|
||||
/// Attempts to resolve a registered codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec identifier to resolve.</param>
|
||||
/// <param name="compressionCodec">When this method returns, contains the resolved codec when found.</param>
|
||||
/// <returns><see langword="true"/> when a codec is registered for <paramref name="codec"/>; otherwise, <see langword="false"/>.</returns>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> when a codec is registered for <paramref name="codec" />; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public bool TryGetCodec(CompressionCodec codec, out ICompressionCodec compressionCodec)
|
||||
{
|
||||
return _codecs.TryGetValue(codec, out compressionCodec!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a registered codec implementation.
|
||||
/// Gets a registered codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec identifier to resolve.</param>
|
||||
/// <returns>The registered codec implementation.</returns>
|
||||
@@ -65,7 +65,7 @@ public sealed class CompressionService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using the selected codec and level.
|
||||
/// Compresses payload bytes using the selected codec and level.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
@@ -77,131 +77,40 @@ public sealed class CompressionService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses payload bytes using the selected codec.
|
||||
/// Decompresses payload bytes using the selected codec.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="expectedLength">
|
||||
/// The expected decompressed byte length, or a negative value to skip exact-length
|
||||
/// validation.
|
||||
/// </param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, CompressionCodec codec, int expectedLength, int maxDecompressedSizeBytes)
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, CompressionCodec codec, int expectedLength,
|
||||
int maxDecompressedSizeBytes)
|
||||
{
|
||||
return GetCodec(codec).Decompress(input, expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses and then decompresses payload bytes using the selected codec.
|
||||
/// Compresses and then decompresses payload bytes using the selected codec.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to roundtrip.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes after roundtrip.</returns>
|
||||
public byte[] Roundtrip(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level, int maxDecompressedSizeBytes)
|
||||
public byte[] Roundtrip(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level,
|
||||
int maxDecompressedSizeBytes)
|
||||
{
|
||||
var compressed = Compress(input, codec, level);
|
||||
byte[] compressed = Compress(input, codec, level);
|
||||
return Decompress(compressed, codec, input.Length, maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
private sealed class NoneCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.None;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the input payload without compression.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to copy.</param>
|
||||
/// <param name="level">The requested compression level.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level) => input.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Validates and returns an uncompressed payload copy.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to validate and copy.</param>
|
||||
/// <param name="expectedLength">The expected payload length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed payload size in bytes.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
if (input.Length > maxDecompressedSizeBytes)
|
||||
throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
|
||||
if (expectedLength >= 0 && expectedLength != input.Length)
|
||||
throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {input.Length}.");
|
||||
|
||||
return input.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BrotliCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Brotli.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Brotli-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeflateCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Deflate;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Deflate.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Deflate-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] CompressWithCodecStream(ReadOnlySpan<byte> input, Func<Stream, Stream> streamFactory)
|
||||
{
|
||||
using var output = new MemoryStream(capacity: input.Length);
|
||||
using var output = new MemoryStream(input.Length);
|
||||
using (var codecStream = streamFactory(output))
|
||||
{
|
||||
codecStream.Write(input);
|
||||
@@ -220,31 +129,33 @@ public sealed class CompressionService
|
||||
if (maxDecompressedSizeBytes <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDecompressedSizeBytes));
|
||||
|
||||
using var compressed = new MemoryStream(input.ToArray(), writable: false);
|
||||
using var compressed = new MemoryStream(input.ToArray(), false);
|
||||
using var codecStream = streamFactory(compressed);
|
||||
using var output = expectedLength > 0
|
||||
? new MemoryStream(capacity: expectedLength)
|
||||
? new MemoryStream(expectedLength)
|
||||
: new MemoryStream();
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
try
|
||||
{
|
||||
int totalWritten = 0;
|
||||
var totalWritten = 0;
|
||||
while (true)
|
||||
{
|
||||
var bytesRead = codecStream.Read(buffer, 0, buffer.Length);
|
||||
int bytesRead = codecStream.Read(buffer, 0, buffer.Length);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
|
||||
totalWritten += bytesRead;
|
||||
if (totalWritten > maxDecompressedSizeBytes)
|
||||
throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
throw new InvalidDataException(
|
||||
$"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
|
||||
output.Write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
if (expectedLength >= 0 && totalWritten != expectedLength)
|
||||
throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {totalWritten}.");
|
||||
throw new InvalidDataException(
|
||||
$"Expected decompressed length {expectedLength}, actual {totalWritten}.");
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
@@ -253,4 +164,115 @@ public sealed class CompressionService
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoneCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.None;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the input payload without compression.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to copy.</param>
|
||||
/// <param name="level">The requested compression level.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return input.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and returns an uncompressed payload copy.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to validate and copy.</param>
|
||||
/// <param name="expectedLength">The expected payload length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed payload size in bytes.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
if (input.Length > maxDecompressedSizeBytes)
|
||||
throw new InvalidDataException(
|
||||
$"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
|
||||
if (expectedLength >= 0 && expectedLength != input.Length)
|
||||
throw new InvalidDataException(
|
||||
$"Expected decompressed length {expectedLength}, actual {input.Length}.");
|
||||
|
||||
return input.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BrotliCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Brotli.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Brotli-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">
|
||||
/// The expected decompressed byte length, or a negative value to skip exact-length
|
||||
/// validation.
|
||||
/// </param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input,
|
||||
stream => new BrotliStream(stream, CompressionMode.Decompress, true), expectedLength,
|
||||
maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeflateCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Deflate;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Deflate.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Deflate-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">
|
||||
/// The expected decompressed byte length, or a negative value to skip exact-length
|
||||
/// validation.
|
||||
/// </param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input,
|
||||
stream => new DeflateStream(stream, CompressionMode.Decompress, true), expectedLength,
|
||||
maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,47 @@
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of aggregated compression and decompression telemetry.
|
||||
/// Snapshot of aggregated compression and decompression telemetry.
|
||||
/// </summary>
|
||||
public readonly struct CompressionStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressedDocumentCount.
|
||||
/// Gets or sets the CompressedDocumentCount.
|
||||
/// </summary>
|
||||
public long CompressedDocumentCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BytesBeforeCompression.
|
||||
/// Gets or sets the BytesBeforeCompression.
|
||||
/// </summary>
|
||||
public long BytesBeforeCompression { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BytesAfterCompression.
|
||||
/// Gets or sets the BytesAfterCompression.
|
||||
/// </summary>
|
||||
public long BytesAfterCompression { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressionCpuTicks.
|
||||
/// Gets or sets the CompressionCpuTicks.
|
||||
/// </summary>
|
||||
public long CompressionCpuTicks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DecompressionCpuTicks.
|
||||
/// Gets or sets the DecompressionCpuTicks.
|
||||
/// </summary>
|
||||
public long DecompressionCpuTicks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressionFailureCount.
|
||||
/// Gets or sets the CompressionFailureCount.
|
||||
/// </summary>
|
||||
public long CompressionFailureCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ChecksumFailureCount.
|
||||
/// Gets or sets the ChecksumFailureCount.
|
||||
/// </summary>
|
||||
public long ChecksumFailureCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SafetyLimitRejectionCount.
|
||||
/// Gets or sets the SafetyLimitRejectionCount.
|
||||
/// </summary>
|
||||
public long SafetyLimitRejectionCount { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,109 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe counters for compression/decompression lifecycle events.
|
||||
/// Thread-safe counters for compression/decompression lifecycle events.
|
||||
/// </summary>
|
||||
public sealed class CompressionTelemetry
|
||||
{
|
||||
private long _checksumFailureCount;
|
||||
private long _compressedDocumentCount;
|
||||
private long _compressionAttempts;
|
||||
private long _compressionSuccesses;
|
||||
private long _compressionCpuTicks;
|
||||
private long _compressionFailures;
|
||||
private long _compressionSkippedTooSmall;
|
||||
private long _compressionSkippedInsufficientSavings;
|
||||
private long _decompressionAttempts;
|
||||
private long _decompressionSuccesses;
|
||||
private long _decompressionFailures;
|
||||
private long _compressionInputBytes;
|
||||
private long _compressionOutputBytes;
|
||||
private long _decompressionOutputBytes;
|
||||
private long _compressedDocumentCount;
|
||||
private long _compressionCpuTicks;
|
||||
private long _compressionSkippedInsufficientSavings;
|
||||
private long _compressionSkippedTooSmall;
|
||||
private long _compressionSuccesses;
|
||||
private long _decompressionAttempts;
|
||||
private long _decompressionCpuTicks;
|
||||
private long _checksumFailureCount;
|
||||
private long _decompressionFailures;
|
||||
private long _decompressionOutputBytes;
|
||||
private long _decompressionSuccesses;
|
||||
private long _safetyLimitRejectionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of attempted compression operations.
|
||||
/// Gets the number of attempted compression operations.
|
||||
/// </summary>
|
||||
public long CompressionAttempts => Interlocked.Read(ref _compressionAttempts);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of successful compression operations.
|
||||
/// Gets the number of successful compression operations.
|
||||
/// </summary>
|
||||
public long CompressionSuccesses => Interlocked.Read(ref _compressionSuccesses);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of failed compression operations.
|
||||
/// Gets the number of failed compression operations.
|
||||
/// </summary>
|
||||
public long CompressionFailures => Interlocked.Read(ref _compressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of compression attempts skipped because payloads were too small.
|
||||
/// Gets the number of compression attempts skipped because payloads were too small.
|
||||
/// </summary>
|
||||
public long CompressionSkippedTooSmall => Interlocked.Read(ref _compressionSkippedTooSmall);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of compression attempts skipped due to insufficient savings.
|
||||
/// Gets the number of compression attempts skipped due to insufficient savings.
|
||||
/// </summary>
|
||||
public long CompressionSkippedInsufficientSavings => Interlocked.Read(ref _compressionSkippedInsufficientSavings);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of attempted decompression operations.
|
||||
/// Gets the number of attempted decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionAttempts => Interlocked.Read(ref _decompressionAttempts);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of successful decompression operations.
|
||||
/// Gets the number of successful decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionSuccesses => Interlocked.Read(ref _decompressionSuccesses);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of failed decompression operations.
|
||||
/// Gets the number of failed decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionFailures => Interlocked.Read(ref _decompressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total input bytes observed by compression attempts.
|
||||
/// Gets the total input bytes observed by compression attempts.
|
||||
/// </summary>
|
||||
public long CompressionInputBytes => Interlocked.Read(ref _compressionInputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total output bytes produced by successful compression attempts.
|
||||
/// Gets the total output bytes produced by successful compression attempts.
|
||||
/// </summary>
|
||||
public long CompressionOutputBytes => Interlocked.Read(ref _compressionOutputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total output bytes produced by successful decompression attempts.
|
||||
/// Gets the total output bytes produced by successful decompression attempts.
|
||||
/// </summary>
|
||||
public long DecompressionOutputBytes => Interlocked.Read(ref _decompressionOutputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of documents stored in compressed form.
|
||||
/// Gets the number of documents stored in compressed form.
|
||||
/// </summary>
|
||||
public long CompressedDocumentCount => Interlocked.Read(ref _compressedDocumentCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total CPU ticks spent on compression.
|
||||
/// Gets the total CPU ticks spent on compression.
|
||||
/// </summary>
|
||||
public long CompressionCpuTicks => Interlocked.Read(ref _compressionCpuTicks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total CPU ticks spent on decompression.
|
||||
/// Gets the total CPU ticks spent on decompression.
|
||||
/// </summary>
|
||||
public long DecompressionCpuTicks => Interlocked.Read(ref _decompressionCpuTicks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of checksum validation failures.
|
||||
/// Gets the number of checksum validation failures.
|
||||
/// </summary>
|
||||
public long ChecksumFailureCount => Interlocked.Read(ref _checksumFailureCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of decompression safety-limit rejections.
|
||||
/// Gets the number of decompression safety-limit rejections.
|
||||
/// </summary>
|
||||
public long SafetyLimitRejectionCount => Interlocked.Read(ref _safetyLimitRejectionCount);
|
||||
|
||||
/// <summary>
|
||||
/// Records a compression attempt and its input byte size.
|
||||
/// Records a compression attempt and its input byte size.
|
||||
/// </summary>
|
||||
/// <param name="inputBytes">The number of input bytes provided to compression.</param>
|
||||
public void RecordCompressionAttempt(int inputBytes)
|
||||
@@ -115,7 +113,7 @@ public sealed class CompressionTelemetry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful compression operation.
|
||||
/// Records a successful compression operation.
|
||||
/// </summary>
|
||||
/// <param name="outputBytes">The number of compressed bytes produced.</param>
|
||||
public void RecordCompressionSuccess(int outputBytes)
|
||||
@@ -126,49 +124,73 @@ public sealed class CompressionTelemetry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a failed compression operation.
|
||||
/// Records a failed compression operation.
|
||||
/// </summary>
|
||||
public void RecordCompressionFailure() => Interlocked.Increment(ref _compressionFailures);
|
||||
public void RecordCompressionFailure()
|
||||
{
|
||||
Interlocked.Increment(ref _compressionFailures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that compression was skipped because the payload was too small.
|
||||
/// Records that compression was skipped because the payload was too small.
|
||||
/// </summary>
|
||||
public void RecordCompressionSkippedTooSmall() => Interlocked.Increment(ref _compressionSkippedTooSmall);
|
||||
public void RecordCompressionSkippedTooSmall()
|
||||
{
|
||||
Interlocked.Increment(ref _compressionSkippedTooSmall);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that compression was skipped due to insufficient expected savings.
|
||||
/// Records that compression was skipped due to insufficient expected savings.
|
||||
/// </summary>
|
||||
public void RecordCompressionSkippedInsufficientSavings() => Interlocked.Increment(ref _compressionSkippedInsufficientSavings);
|
||||
public void RecordCompressionSkippedInsufficientSavings()
|
||||
{
|
||||
Interlocked.Increment(ref _compressionSkippedInsufficientSavings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a decompression attempt.
|
||||
/// Records a decompression attempt.
|
||||
/// </summary>
|
||||
public void RecordDecompressionAttempt() => Interlocked.Increment(ref _decompressionAttempts);
|
||||
public void RecordDecompressionAttempt()
|
||||
{
|
||||
Interlocked.Increment(ref _decompressionAttempts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds CPU ticks spent performing compression.
|
||||
/// Adds CPU ticks spent performing compression.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The CPU ticks to add.</param>
|
||||
public void RecordCompressionCpuTicks(long ticks) => Interlocked.Add(ref _compressionCpuTicks, ticks);
|
||||
public void RecordCompressionCpuTicks(long ticks)
|
||||
{
|
||||
Interlocked.Add(ref _compressionCpuTicks, ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds CPU ticks spent performing decompression.
|
||||
/// Adds CPU ticks spent performing decompression.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The CPU ticks to add.</param>
|
||||
public void RecordDecompressionCpuTicks(long ticks) => Interlocked.Add(ref _decompressionCpuTicks, ticks);
|
||||
public void RecordDecompressionCpuTicks(long ticks)
|
||||
{
|
||||
Interlocked.Add(ref _decompressionCpuTicks, ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a checksum validation failure.
|
||||
/// Records a checksum validation failure.
|
||||
/// </summary>
|
||||
public void RecordChecksumFailure() => Interlocked.Increment(ref _checksumFailureCount);
|
||||
public void RecordChecksumFailure()
|
||||
{
|
||||
Interlocked.Increment(ref _checksumFailureCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a decompression rejection due to safety limits.
|
||||
/// Records a decompression rejection due to safety limits.
|
||||
/// </summary>
|
||||
public void RecordSafetyLimitRejection() => Interlocked.Increment(ref _safetyLimitRejectionCount);
|
||||
public void RecordSafetyLimitRejection()
|
||||
{
|
||||
Interlocked.Increment(ref _safetyLimitRejectionCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful decompression operation.
|
||||
/// Records a successful decompression operation.
|
||||
/// </summary>
|
||||
/// <param name="outputBytes">The number of decompressed bytes produced.</param>
|
||||
public void RecordDecompressionSuccess(int outputBytes)
|
||||
@@ -178,12 +200,15 @@ public sealed class CompressionTelemetry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a failed decompression operation.
|
||||
/// Records a failed decompression operation.
|
||||
/// </summary>
|
||||
public void RecordDecompressionFailure() => Interlocked.Increment(ref _decompressionFailures);
|
||||
public void RecordDecompressionFailure()
|
||||
{
|
||||
Interlocked.Increment(ref _decompressionFailures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a point-in-time snapshot of compression telemetry.
|
||||
/// Returns a point-in-time snapshot of compression telemetry.
|
||||
/// </summary>
|
||||
/// <returns>The aggregated compression statistics.</returns>
|
||||
public CompressionStats GetSnapshot()
|
||||
@@ -200,4 +225,4 @@ public sealed class CompressionTelemetry
|
||||
SafetyLimitRejectionCount = SafetyLimitRejectionCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,27 @@ using System.IO.Compression;
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Codec abstraction for payload compression and decompression.
|
||||
/// Codec abstraction for payload compression and decompression.
|
||||
/// </summary>
|
||||
public interface ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Codec identifier.
|
||||
/// Codec identifier.
|
||||
/// </summary>
|
||||
CompressionCodec Codec { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compresses input bytes.
|
||||
/// Compresses input bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">Input payload bytes to compress.</param>
|
||||
/// <param name="level">Compression level to apply.</param>
|
||||
byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level);
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses payload bytes with output bounds validation.
|
||||
/// Decompresses payload bytes with output bounds validation.
|
||||
/// </summary>
|
||||
/// <param name="input">Input payload bytes to decompress.</param>
|
||||
/// <param name="expectedLength">Expected decompressed length.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">Maximum allowed decompressed payload size in bytes.</param>
|
||||
byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user