using System.Buffers; using System.Collections.Concurrent; using System.IO.Compression; namespace ZB.MOM.WW.CBDD.Core.Compression; /// /// Compression codec registry and utility service. /// public sealed class CompressionService { private readonly ConcurrentDictionary _codecs = new(); public CompressionService(IEnumerable? additionalCodecs = null) { RegisterCodec(new NoneCompressionCodec()); RegisterCodec(new BrotliCompressionCodec()); RegisterCodec(new DeflateCompressionCodec()); if (additionalCodecs == null) return; foreach (var codec in additionalCodecs) { RegisterCodec(codec); } } public void RegisterCodec(ICompressionCodec codec) { ArgumentNullException.ThrowIfNull(codec); _codecs[codec.Codec] = codec; } public bool TryGetCodec(CompressionCodec codec, out ICompressionCodec compressionCodec) { return _codecs.TryGetValue(codec, out compressionCodec!); } public ICompressionCodec GetCodec(CompressionCodec codec) { if (_codecs.TryGetValue(codec, out var compressionCodec)) return compressionCodec; throw new InvalidOperationException($"Compression codec '{codec}' is not registered."); } public byte[] Compress(ReadOnlySpan input, CompressionCodec codec, CompressionLevel level) { return GetCodec(codec).Compress(input, level); } public byte[] Decompress(ReadOnlySpan input, CompressionCodec codec, int expectedLength, int maxDecompressedSizeBytes) { return GetCodec(codec).Decompress(input, expectedLength, maxDecompressedSizeBytes); } public byte[] Roundtrip(ReadOnlySpan input, CompressionCodec codec, CompressionLevel level, int maxDecompressedSizeBytes) { var compressed = Compress(input, codec, level); return Decompress(compressed, codec, input.Length, maxDecompressedSizeBytes); } private sealed class NoneCompressionCodec : ICompressionCodec { public CompressionCodec Codec => CompressionCodec.None; public byte[] Compress(ReadOnlySpan input, CompressionLevel level) => input.ToArray(); public byte[] Decompress(ReadOnlySpan 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 { public CompressionCodec Codec => CompressionCodec.Brotli; public byte[] Compress(ReadOnlySpan input, CompressionLevel level) { return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, leaveOpen: true)); } public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) { return DecompressWithCodecStream(input, stream => new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes); } } private sealed class DeflateCompressionCodec : ICompressionCodec { public CompressionCodec Codec => CompressionCodec.Deflate; public byte[] Compress(ReadOnlySpan input, CompressionLevel level) { return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, leaveOpen: true)); } public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) { return DecompressWithCodecStream(input, stream => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes); } } private static byte[] CompressWithCodecStream(ReadOnlySpan input, Func streamFactory) { using var output = new MemoryStream(capacity: input.Length); using (var codecStream = streamFactory(output)) { codecStream.Write(input); codecStream.Flush(); } return output.ToArray(); } private static byte[] DecompressWithCodecStream( ReadOnlySpan input, Func streamFactory, int expectedLength, int maxDecompressedSizeBytes) { if (maxDecompressedSizeBytes <= 0) throw new ArgumentOutOfRangeException(nameof(maxDecompressedSizeBytes)); using var compressed = new MemoryStream(input.ToArray(), writable: false); using var codecStream = streamFactory(compressed); using var output = expectedLength > 0 ? new MemoryStream(capacity: expectedLength) : new MemoryStream(); var buffer = ArrayPool.Shared.Rent(8192); try { int totalWritten = 0; while (true) { var 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)."); output.Write(buffer, 0, bytesRead); } if (expectedLength >= 0 && totalWritten != expectedLength) throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {totalWritten}."); return output.ToArray(); } finally { ArrayPool.Shared.Return(buffer); } } }