From e402958743ee573cebaba5cbf1debc14fa2dcd7b Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 12 Jan 2026 20:46:21 +0100 Subject: [PATCH 01/15] Add Zlib Encoder and Decoder classes --- .../ref/System.IO.Compression.cs | 35 +++ .../src/Resources/Strings.resx | 15 + .../src/System.IO.Compression.csproj | 4 + .../IO/Compression/ZlibCompressionFormat.cs | 38 +++ .../src/System/IO/Compression/ZlibDecoder.cs | 191 ++++++++++++ .../src/System/IO/Compression/ZlibEncoder.cs | 276 ++++++++++++++++++ .../IO/Compression/ZlibEncoderOptions.cs | 71 +++++ 7 files changed, 630 insertions(+) create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibCompressionFormat.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs diff --git a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 6f4c376de6c241..eaf1be306540df 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -156,6 +156,41 @@ public enum ZLibCompressionStrategy RunLengthEncoding = 3, Fixed = 4, } + public enum ZlibCompressionFormat + { + Deflate = 0, + ZLib = 1, + GZip = 2, + } + public sealed partial class ZlibDecoder : System.IDisposable + { + public ZlibDecoder(System.IO.Compression.ZlibCompressionFormat format) { } + public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } + public void Dispose() { } + public void Reset() { } + public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.ZlibCompressionFormat format) { throw null; } + } + public sealed partial class ZlibEncoder : System.IDisposable + { + public ZlibEncoder(int compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { } + public ZlibEncoder(int compressionLevel, System.IO.Compression.ZlibCompressionFormat format, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public ZlibEncoder(System.IO.Compression.ZlibEncoderOptions options) { } + public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } + public void Dispose() { } + public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } + public static int GetMaxCompressedLength(int inputSize) { throw null; } + public void Reset() { } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { throw null; } + } + public sealed partial class ZlibEncoderOptions + { + public ZlibEncoderOptions() { } + public int CompressionLevel { get { throw null; } set { } } + public System.IO.Compression.ZLibCompressionStrategy CompressionStrategy { get { throw null; } set { } } + public System.IO.Compression.ZlibCompressionFormat Format { get { throw null; } set { } } + } public sealed partial class ZLibStream : System.IO.Stream { public ZLibStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index d477d0c40f6624..d2f1be30975788 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -170,6 +170,21 @@ The underlying compression routine returned an unexpected error code: '{0}'. + + The compression level ({0}) must be between {1} and {2}, inclusive. + + + The ZlibEncoder has not been initialized. Use the constructor to initialize. + + + The ZlibDecoder has not been initialized. Use the constructor to initialize. + + + The encoder has already finished compressing. Call Reset() to reuse for a new compression operation. + + + The decoder has already finished decompressing. Call Reset() to reuse for a new decompression operation. + Central Directory corrupt. diff --git a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj index 91ad2914646cd3..c1a709020e4726 100644 --- a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj +++ b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj @@ -52,7 +52,11 @@ + + + + + /// Specifies the compression format for and . + /// + public enum ZlibCompressionFormat + { + /// + /// Raw deflate format without any header or trailer. + /// + /// + /// This format produces the smallest output but provides no error checking. + /// It is compatible with . + /// + Deflate = 0, + + /// + /// ZLib format with a small header and Adler-32 checksum trailer. + /// + /// + /// This format adds a 2-byte header and 4-byte Adler-32 checksum for error detection. + /// It is compatible with . + /// + ZLib = 1, + + /// + /// GZip format with header and CRC-32 checksum trailer. + /// + /// + /// This format adds a larger header with optional metadata and a CRC-32 checksum. + /// It is compatible with and the gzip file format. + /// + GZip = 2 + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs new file mode 100644 index 00000000000000..ef874be2f260f2 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO.Compression +{ + /// + /// Provides non-allocating, performant decompression methods for data compressed using the Deflate, ZLib, or GZip data format specification. + /// + public sealed class ZlibDecoder : IDisposable + { + private ZLibNative.ZLibStreamHandle? _state; + private bool _disposed; + private bool _finished; + + // Store construction parameters for Reset() + private readonly int _windowBits; + + /// + /// Initializes a new instance of the class using the specified format. + /// + /// The compression format to decompress. + /// is not a valid value. + /// Failed to create the instance. + public ZlibDecoder(ZlibCompressionFormat format) + { + _disposed = false; + _finished = false; + _windowBits = GetWindowBits(format); + + _state = ZLibNative.ZLibStreamHandle.CreateForInflate(_windowBits); + } + + private static int GetWindowBits(ZlibCompressionFormat format) + { + return format switch + { + ZlibCompressionFormat.Deflate => ZLibNative.Deflate_DefaultWindowBits, + ZlibCompressionFormat.ZLib => ZLibNative.ZLib_DefaultWindowBits, + ZlibCompressionFormat.GZip => ZLibNative.GZip_DefaultWindowBits, + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + _state?.Dispose(); + _state = null; + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private void EnsureInitialized() + { + EnsureNotDisposed(); + if (_state is null) + { + throw new InvalidOperationException(SR.ZlibDecoder_NotInitialized); + } + } + + /// + /// Decompresses data that was compressed using the Deflate, ZLib, or GZip algorithm. + /// + /// A buffer containing the compressed data. + /// When this method returns, a byte span containing the decompressed data. + /// The total number of bytes that were read from . + /// The total number of bytes that were written in the . + /// One of the enumeration values that indicates the status of the decompression operation. + /// + /// The return value can be as follows: + /// - : was successfully and completely decompressed into . + /// - : There is not enough space in to decompress . + /// - : The decompression action is partially done. At least one more byte is required to complete the decompression task. + /// - : The data in is invalid and could not be decompressed. + /// + public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) + { + EnsureInitialized(); + Debug.Assert(_state is not null); + + bytesConsumed = 0; + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + if (source.IsEmpty) + { + return OperationStatus.NeedMoreData; + } + + unsafe + { + fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) + fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) + { + _state.NextIn = (IntPtr)inputPtr; + _state.AvailIn = (uint)source.Length; + _state.NextOut = (IntPtr)outputPtr; + _state.AvailOut = (uint)destination.Length; + + ZLibNative.ErrorCode errorCode = _state.Inflate(ZLibNative.FlushCode.NoFlush); + + bytesConsumed = source.Length - (int)_state.AvailIn; + bytesWritten = destination.Length - (int)_state.AvailOut; + + OperationStatus status = errorCode switch + { + ZLibNative.ErrorCode.Ok => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : _state.AvailIn == 0 + ? OperationStatus.NeedMoreData + : OperationStatus.Done, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.NeedMoreData, + ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, + _ => OperationStatus.InvalidData + }; + + // Track if decompression is finished + if (errorCode == ZLibNative.ErrorCode.StreamEnd) + { + _finished = true; + } + + return status; + } + } + } + + /// + /// Resets the decoder to its initial state, allowing it to be reused for a new decompression operation. + /// + /// The decoder has been disposed. + public void Reset() + { + EnsureNotDisposed(); + + _finished = false; + + // Dispose the old state and create a new one + _state?.Dispose(); + _state = ZLibNative.ZLibStreamHandle.CreateForInflate(_windowBits); + } + + /// + /// Attempts to decompress data. + /// + /// A buffer containing the compressed data. + /// When this method returns, a byte span containing the decompressed data. + /// The total number of bytes that were written in the . + /// on success; otherwise. + /// If this method returns , may be empty or contain partially decompressed data, with being zero or greater than zero but less than the expected total. + public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + return TryDecompress(source, destination, out bytesWritten, ZlibCompressionFormat.Deflate); + } + + /// + /// Attempts to decompress data using the specified format. + /// + /// A buffer containing the compressed data. + /// When this method returns, a byte span containing the decompressed data. + /// The total number of bytes that were written in the . + /// The compression format to decompress. + /// on success; otherwise. + /// If this method returns , may be empty or contain partially decompressed data, with being zero or greater than zero but less than the expected total. + public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten, ZlibCompressionFormat format) + { + using var decoder = new ZlibDecoder(format); + OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs new file mode 100644 index 00000000000000..4f94cc3f5f1241 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to encode and decode data in a streamless, non-allocating, and performant manner using the Deflate, ZLib, or GZip data format specification. + /// + public sealed class ZlibEncoder : IDisposable + { + private ZLibNative.ZLibStreamHandle? _state; + private bool _disposed; + private bool _finished; + + // Store construction parameters for Reset() + private readonly int _compressionLevel; + private readonly int _windowBits; + private readonly ZLibCompressionStrategy _strategy; + + /// + /// Initializes a new instance of the class using the specified compression level and format. + /// + /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression format to use. + /// is not between -1 and 9. + /// Failed to create the instance. + public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format) + : this(compressionLevel, format, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level, format, and strategy. + /// + /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression format to use. + /// The compression strategy to use. + /// is not between -1 and 9. + /// Failed to create the instance. + public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format, ZLibCompressionStrategy strategy) + { + ValidateCompressionLevel(compressionLevel); + + _disposed = false; + _finished = false; + _compressionLevel = compressionLevel; + _windowBits = GetWindowBits(format); + _strategy = strategy; + + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + (ZLibNative.CompressionLevel)_compressionLevel, + _windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)_strategy); + } + + /// + /// Initializes a new instance of the class using the specified options. + /// + /// The compression options. + /// is null. + /// Failed to create the instance. + public ZlibEncoder(ZlibEncoderOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + _disposed = false; + _finished = false; + _compressionLevel = options.CompressionLevel; + _windowBits = GetWindowBits(options.Format); + _strategy = options.CompressionStrategy; + + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + (ZLibNative.CompressionLevel)_compressionLevel, + _windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)_strategy); + } + + private static void ValidateCompressionLevel(int compressionLevel) + { + if (compressionLevel < -1 || compressionLevel > 9) + { + throw new ArgumentOutOfRangeException(nameof(compressionLevel), SR.Format(SR.ZlibEncoder_CompressionLevel, compressionLevel, -1, 9)); + } + } + + private static int GetWindowBits(ZlibCompressionFormat format) + { + return format switch + { + ZlibCompressionFormat.Deflate => ZLibNative.Deflate_DefaultWindowBits, + ZlibCompressionFormat.ZLib => ZLibNative.ZLib_DefaultWindowBits, + ZlibCompressionFormat.GZip => ZLibNative.GZip_DefaultWindowBits, + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + _state?.Dispose(); + _state = null; + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private void EnsureInitialized() + { + EnsureNotDisposed(); + if (_state is null) + { + throw new InvalidOperationException(SR.ZlibEncoder_NotInitialized); + } + } + + /// + /// Gets the maximum expected compressed length for the provided input size. + /// + /// The input size to get the maximum expected compressed length from. + /// A number representing the maximum compressed length for the provided input size. + /// is negative. + public static int GetMaxCompressedLength(int inputSize) + { + ArgumentOutOfRangeException.ThrowIfNegative(inputSize); + + // ZLib's compressBound formula: inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 13 + // For GZip, add 18 bytes for header/trailer + // We use a conservative estimate that works for all formats + long result = inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 32; + + if (result > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(inputSize)); + } + + return (int)result; + } + + /// + /// Compresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a byte span where the compressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) + { + EnsureInitialized(); + Debug.Assert(_state is not null); + + bytesConsumed = 0; + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + ZLibNative.FlushCode flushCode = isFinalBlock ? ZLibNative.FlushCode.Finish : ZLibNative.FlushCode.NoFlush; + + unsafe + { + fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) + fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) + { + _state.NextIn = (IntPtr)inputPtr; + _state.AvailIn = (uint)source.Length; + _state.NextOut = (IntPtr)outputPtr; + _state.AvailOut = (uint)destination.Length; + + ZLibNative.ErrorCode errorCode = _state.Deflate(flushCode); + + bytesConsumed = source.Length - (int)_state.AvailIn; + bytesWritten = destination.Length - (int)_state.AvailOut; + + OperationStatus status = errorCode switch + { + ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 && _state.AvailOut > 0 + ? OperationStatus.Done + : _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.Done, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.NeedMoreData, + ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, + _ => OperationStatus.InvalidData + }; + + // Track if compression is finished + if (isFinalBlock && errorCode == ZLibNative.ErrorCode.StreamEnd) + { + _finished = true; + } + + return status; + } + } + } + + /// + /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. + /// + /// When this method returns, a span of bytes where the compressed data will be stored. + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the operation finished. + public OperationStatus Flush(Span destination, out int bytesWritten) + { + return Compress(ReadOnlySpan.Empty, destination, out _, out bytesWritten, isFinalBlock: false); + } + + /// + /// Resets the encoder to its initial state, allowing it to be reused for a new compression operation. + /// + /// The encoder has been disposed. + public void Reset() + { + EnsureNotDisposed(); + + _finished = false; + + // Dispose the old state and create a new one + _state?.Dispose(); + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + (ZLibNative.CompressionLevel)_compressionLevel, + _windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)_strategy); + } + + /// + /// Tries to compress a source byte span into a destination span using the default compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + return TryCompress(source, destination, out bytesWritten, compressionLevel: -1, ZlibCompressionFormat.Deflate); + } + + /// + /// Tries to compress a source byte span into a destination span using the specified compression level and format. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression format to use. + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int compressionLevel, ZlibCompressionFormat format) + { + ValidateCompressionLevel(compressionLevel); + + using var encoder = new ZlibEncoder(compressionLevel, format); + OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs new file mode 100644 index 00000000000000..1309c084c781db --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Compression +{ + /// + /// Provides compression options for . + /// + public sealed class ZlibEncoderOptions + { + private int _compressionLevel = -1; + private ZLibCompressionStrategy _compressionStrategy; + private ZlibCompressionFormat _format = ZlibCompressionFormat.Deflate; + + /// + /// Gets or sets the compression level for the encoder. + /// + /// The value is less than -1 or greater than 9. + /// + /// The compression level can be any value between -1 and 9 (inclusive). + /// -1 requests the default compression level (currently equivalent to 6). + /// 0 gives no compression. + /// 1 gives best speed. + /// 9 gives best compression. + /// The default value is -1. + /// + public int CompressionLevel + { + get => _compressionLevel; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 9); + + _compressionLevel = value; + } + } + + /// + /// Gets or sets the compression strategy for the encoder. + /// + /// The value is not a valid value. + public ZLibCompressionStrategy CompressionStrategy + { + get => _compressionStrategy; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan((int)value, (int)ZLibCompressionStrategy.Default, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan((int)value, (int)ZLibCompressionStrategy.Fixed, nameof(value)); + + _compressionStrategy = value; + } + } + + /// + /// Gets or sets the compression format for the encoder. + /// + /// The value is not a valid value. + public ZlibCompressionFormat Format + { + get => _format; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan((int)value, (int)ZlibCompressionFormat.Deflate, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan((int)value, (int)ZlibCompressionFormat.GZip, nameof(value)); + + _format = value; + } + } + } +} From 95ac930d6c81b1ad7a371cf14381089a0cfa8539 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 12 Jan 2026 20:46:28 +0100 Subject: [PATCH 02/15] Add tests --- .../tests/System.IO.Compression.Tests.csproj | 1 + .../tests/ZlibEncoderDecoderTests.cs | 724 ++++++++++++++++++ 2 files changed, 725 insertions(+) create mode 100644 src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs diff --git a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj index fa2d85fc0656da..5191c4f8ab1d7f 100644 --- a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj +++ b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj @@ -47,6 +47,7 @@ + diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs new file mode 100644 index 00000000000000..4d7f9c8f59b825 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -0,0 +1,724 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +namespace System.IO.Compression +{ + public class ZlibEncoderDecoderTests + { + private static readonly byte[] s_sampleData = Encoding.UTF8.GetBytes( + "Hello, World! This is a test string for compression. " + + "We need some repeated content to make compression effective. " + + "Hello, World! This is a test string for compression. " + + "The quick brown fox jumps over the lazy dog. " + + "Sphinx of black quartz, judge my vow."); + + #region ZlibEncoder Tests + + [Fact] + public void ZlibEncoder_Ctor_InvalidCompressionLevel_Throws() + { + Assert.Throws(() => new ZlibEncoder(-2, ZlibCompressionFormat.Deflate)); + Assert.Throws(() => new ZlibEncoder(10, ZlibCompressionFormat.Deflate)); + } + + [Fact] + public void ZlibEncoder_Ctor_InvalidFormat_Throws() + { + Assert.Throws(() => new ZlibEncoder(6, (ZlibCompressionFormat)99)); + } + + [Fact] + public void ZlibEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new ZlibEncoder(null!)); + } + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void ZlibEncoder_Compress_AllFormats(ZlibCompressionFormat format) + { + using var encoder = new ZlibEncoder(6, format); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, bytesConsumed); + Assert.True(bytesWritten > 0); + Assert.True(bytesWritten < s_sampleData.Length); // Compression should reduce size + } + + [Fact] + public void ZlibEncoder_Dispose_MultipleCallsSafe() + { + var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + encoder.Dispose(); + encoder.Dispose(); // Should not throw + } + + [Fact] + public void ZlibEncoder_Compress_AfterDispose_Throws() + { + var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + encoder.Dispose(); + + byte[] buffer = new byte[100]; + Assert.Throws(() => + encoder.Compress(s_sampleData, buffer, out _, out _, isFinalBlock: true)); + } + + [Fact] + public void ZlibEncoder_Compress_AfterFinished_ReturnsDone() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + // First compression with final block + encoder.Compress(s_sampleData, destination, out _, out _, isFinalBlock: true); + + // Second call after finished should return Done immediately + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(0, consumed); + Assert.Equal(0, written); + } + + [Fact] + public void ZlibEncoder_Reset_AllowsReuse() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + // First compression + encoder.Compress(s_sampleData, destination, out _, out int firstBytesWritten, isFinalBlock: true); + + // Reset + encoder.Reset(); + + // Second compression should work + Array.Clear(destination); + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int secondBytesWritten, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.Equal(firstBytesWritten, secondBytesWritten); // Should produce same output + } + + [Fact] + public void ZlibEncoder_GetMaxCompressedLength_ValidValues() + { + Assert.True(ZlibEncoder.GetMaxCompressedLength(0) >= 0); + Assert.True(ZlibEncoder.GetMaxCompressedLength(100) >= 100); + Assert.True(ZlibEncoder.GetMaxCompressedLength(1000) >= 1000); + } + + [Fact] + public void ZlibEncoder_GetMaxCompressedLength_NegativeInput_Throws() + { + Assert.Throws(() => ZlibEncoder.GetMaxCompressedLength(-1)); + } + + [Fact] + public void ZlibEncoder_TryCompress_Success() + { + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + bool success = ZlibEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.True(success); + Assert.True(bytesWritten > 0); + } + + [Fact] + public void ZlibEncoder_TryCompress_DestinationTooSmall_ReturnsFalse() + { + byte[] destination = new byte[1]; // Too small + + bool success = ZlibEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.False(success); + } + + [Theory] + [InlineData(-1)] // Default + [InlineData(0)] // No compression + [InlineData(1)] // Best speed + [InlineData(6)] // Default level + [InlineData(9)] // Best compression + public void ZlibEncoder_CompressionLevels(int level) + { + using var encoder = new ZlibEncoder(level, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Theory] + [InlineData(ZLibCompressionStrategy.Default)] + [InlineData(ZLibCompressionStrategy.Filtered)] + [InlineData(ZLibCompressionStrategy.HuffmanOnly)] + [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] + [InlineData(ZLibCompressionStrategy.Fixed)] + public void ZlibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate, strategy); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void ZlibEncoder_Flush() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + // Write some data without finalizing + encoder.Compress(s_sampleData.AsSpan(0, 50), destination, out _, out int written1, isFinalBlock: false); + + // Flush - may return Done, DestinationTooSmall, or NeedMoreData depending on internal state + OperationStatus status = encoder.Flush(destination.AsSpan(written1), out int flushedBytes); + + // Just verify it returns a valid status and doesn't throw + Assert.True( + status == OperationStatus.Done || + status == OperationStatus.DestinationTooSmall || + status == OperationStatus.NeedMoreData, + $"Unexpected status: {status}"); + } + + [Fact] + public void ZlibEncoder_DestinationTooSmall() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[5]; // Very small + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.DestinationTooSmall, status); + Assert.True(consumed >= 0); + Assert.True(written >= 0); + } + + [Fact] + public void ZlibEncoder_EmptySource() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[100]; + + OperationStatus status = encoder.Compress(ReadOnlySpan.Empty, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(0, consumed); + Assert.True(written > 0); // Should still write end-of-stream marker + } + + [Fact] + public void ZlibEncoder_WithOptions() + { + var options = new ZlibEncoderOptions + { + CompressionLevel = 9, + Format = ZlibCompressionFormat.GZip, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new ZlibEncoder(options); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + #endregion + + #region ZlibDecoder Tests + + [Fact] + public void ZlibDecoder_Ctor_InvalidFormat_Throws() + { + Assert.Throws(() => new ZlibDecoder((ZlibCompressionFormat)99)); + } + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void ZlibDecoder_Decompress_AllFormats(ZlibCompressionFormat format) + { + // First, compress the data + byte[] compressed = CompressData(s_sampleData, format); + + // Then decompress + using var decoder = new ZlibDecoder(format); + byte[] decompressed = new byte[s_sampleData.Length * 2]; // Extra room + + OperationStatus status = decoder.Decompress(compressed, decompressed, out int bytesConsumed, out int bytesWritten); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressed.Length, bytesConsumed); + Assert.Equal(s_sampleData.Length, bytesWritten); + Assert.Equal(s_sampleData, decompressed.AsSpan(0, bytesWritten).ToArray()); + } + + [Fact] + public void ZlibDecoder_Dispose_MultipleCallsSafe() + { + var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + decoder.Dispose(); + decoder.Dispose(); // Should not throw + } + + [Fact] + public void ZlibDecoder_Decompress_AfterDispose_Throws() + { + var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + decoder.Dispose(); + + byte[] buffer = new byte[100]; + Assert.Throws(() => + decoder.Decompress(s_sampleData, buffer, out _, out _)); + } + + [Fact] + public void ZlibDecoder_Decompress_AfterFinished_ReturnsDone() + { + byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[s_sampleData.Length * 2]; + + // First decompression + decoder.Decompress(compressed, decompressed, out _, out _); + + // Second call after finished should return Done immediately + OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(0, consumed); + Assert.Equal(0, written); + } + + [Fact] + public void ZlibDecoder_Reset_AllowsReuse() + { + byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[s_sampleData.Length * 2]; + + // First decompression + decoder.Decompress(compressed, decompressed, out _, out int firstBytesWritten); + + // Reset + decoder.Reset(); + + // Second decompression should work + Array.Clear(decompressed); + OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int secondBytesWritten); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressed.Length, consumed); + Assert.Equal(firstBytesWritten, secondBytesWritten); + Assert.Equal(s_sampleData, decompressed.AsSpan(0, secondBytesWritten).ToArray()); + } + + [Fact] + public void ZlibDecoder_TryDecompress_Success() + { + byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[s_sampleData.Length * 2]; + + bool success = ZlibDecoder.TryDecompress(compressed, decompressed, out int bytesWritten, ZlibCompressionFormat.Deflate); + + Assert.True(success); + Assert.Equal(s_sampleData.Length, bytesWritten); + Assert.Equal(s_sampleData, decompressed.AsSpan(0, bytesWritten).ToArray()); + } + + [Fact] + public void ZlibDecoder_TryDecompress_DestinationTooSmall_ReturnsFalse() + { + byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[1]; // Too small + + bool success = ZlibDecoder.TryDecompress(compressed, decompressed, out int bytesWritten, ZlibCompressionFormat.Deflate); + + Assert.False(success); + } + + [Fact] + public void ZlibDecoder_DestinationTooSmall() + { + byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[5]; // Too small + + OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.DestinationTooSmall, status); + } + + [Fact] + public void ZlibDecoder_EmptySource() + { + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[100]; + + OperationStatus status = decoder.Decompress(ReadOnlySpan.Empty, decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.NeedMoreData, status); + Assert.Equal(0, consumed); + Assert.Equal(0, written); + } + + [Fact] + public void ZlibDecoder_InvalidData() + { + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] garbage = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC, 0xFB }; + byte[] decompressed = new byte[100]; + + OperationStatus status = decoder.Decompress(garbage, decompressed, out _, out _); + + Assert.Equal(OperationStatus.InvalidData, status); + } + + #endregion + + #region Round-Trip Tests + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void RoundTrip_WithState(ZlibCompressionFormat format) + { + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + byte[] decompressed = new byte[s_sampleData.Length]; + + // Compress + using (var encoder = new ZlibEncoder(6, format)) + { + OperationStatus compressStatus = encoder.Compress(s_sampleData, compressed, out _, out int written, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, compressStatus); + compressed = compressed.AsSpan(0, written).ToArray(); + } + + // Decompress + using (var decoder = new ZlibDecoder(format)) + { + OperationStatus decompressStatus = decoder.Decompress(compressed, decompressed, out int consumed, out int written); + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(compressed.Length, consumed); + Assert.Equal(s_sampleData.Length, written); + } + + Assert.Equal(s_sampleData, decompressed); + } + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void RoundTrip_Static(ZlibCompressionFormat format) + { + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + byte[] decompressed = new byte[s_sampleData.Length]; + + bool compressSuccess = ZlibEncoder.TryCompress(s_sampleData, compressed, out int compressedSize, 6, format); + Assert.True(compressSuccess); + + compressed = compressed.AsSpan(0, compressedSize).ToArray(); + + bool decompressSuccess = ZlibDecoder.TryDecompress(compressed, decompressed, out int decompressedSize, format); + Assert.True(decompressSuccess); + Assert.Equal(s_sampleData.Length, decompressedSize); + + Assert.Equal(s_sampleData, decompressed); + } + + [Theory] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + [InlineData(100000)] + public void RoundTrip_VariousSizes(int size) + { + byte[] original = new byte[size]; + Random.Shared.NextBytes(original); + + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size)]; + byte[] decompressed = new byte[size]; + + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); + + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); + + Assert.Equal(size, decompressedSize); + Assert.Equal(original, decompressed); + } + + [Fact] + public void RoundTrip_Chunks() + { + int chunkSize = 100; + int totalSize = 2000; + byte[] original = new byte[totalSize]; + Random.Shared.NextBytes(original); + + byte[] allCompressed = new byte[ZlibEncoder.GetMaxCompressedLength(totalSize)]; + int totalCompressed = 0; + + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + + // Compress in chunks + for (int i = 0; i < totalSize; i += chunkSize) + { + int remaining = Math.Min(chunkSize, totalSize - i); + bool isFinal = (i + remaining) >= totalSize; + + OperationStatus status = encoder.Compress( + original.AsSpan(i, remaining), + allCompressed.AsSpan(totalCompressed), + out int consumed, + out int written, + isFinalBlock: isFinal); + + totalCompressed += written; + + if (!isFinal) + { + // Flush intermediate data + encoder.Flush(allCompressed.AsSpan(totalCompressed), out int flushed); + totalCompressed += flushed; + } + } + + // Decompress + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + byte[] decompressed = new byte[totalSize]; + + OperationStatus decompressStatus = decoder.Decompress( + allCompressed.AsSpan(0, totalCompressed), + decompressed, + out int bytesConsumed, + out int bytesWritten); + + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(totalSize, bytesWritten); + Assert.Equal(original, decompressed); + } + + #endregion + + #region Comparison with Stream-based APIs + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void Compare_EncoderOutput_MatchesStreamOutput(ZlibCompressionFormat format) + { + // Compress with span-based API + byte[] spanCompressed = CompressData(s_sampleData, format); + + // Compress with stream-based API + byte[] streamCompressed = CompressWithStream(s_sampleData, format); + + // Both should decompress to the same data + byte[] fromSpan = DecompressWithStream(spanCompressed, format); + byte[] fromStream = DecompressWithStream(streamCompressed, format); + + Assert.Equal(s_sampleData, fromSpan); + Assert.Equal(s_sampleData, fromStream); + } + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void Compare_StreamCompressed_CanDecompressWithDecoder(ZlibCompressionFormat format) + { + // Compress with stream + byte[] streamCompressed = CompressWithStream(s_sampleData, format); + + // Decompress with span-based decoder + using var decoder = new ZlibDecoder(format); + byte[] decompressed = new byte[s_sampleData.Length * 2]; + + OperationStatus status = decoder.Decompress(streamCompressed, decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, written); + Assert.Equal(s_sampleData, decompressed.AsSpan(0, written).ToArray()); + } + + [Theory] + [InlineData(ZlibCompressionFormat.Deflate)] + [InlineData(ZlibCompressionFormat.ZLib)] + [InlineData(ZlibCompressionFormat.GZip)] + public void Compare_EncoderCompressed_CanDecompressWithStream(ZlibCompressionFormat format) + { + // Compress with span-based encoder + byte[] spanCompressed = CompressData(s_sampleData, format); + + // Decompress with stream + byte[] decompressed = DecompressWithStream(spanCompressed, format); + + Assert.Equal(s_sampleData, decompressed); + } + + #endregion + + #region Edge Cases + + [Fact] + public void Compress_HighlyCompressibleData() + { + // All zeros - should compress very well + byte[] zeros = new byte[10000]; + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(zeros.Length)]; + + using var encoder = new ZlibEncoder(9, ZlibCompressionFormat.Deflate); + encoder.Compress(zeros, compressed, out _, out int written, isFinalBlock: true); + + // Should compress to much smaller size + Assert.True(written < zeros.Length / 10, $"Expected significant compression, got {written} bytes from {zeros.Length} bytes"); + } + + [Fact] + public void Compress_IncompressibleData() + { + // Random data - won't compress well + byte[] random = new byte[1000]; + new Random(42).NextBytes(random); + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(random.Length)]; + + using var encoder = new ZlibEncoder(9, ZlibCompressionFormat.Deflate); + encoder.Compress(random, compressed, out _, out int written, isFinalBlock: true); + + // Random data might even expand slightly + Assert.True(written > 0); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + [InlineData(100)] + public void RoundTrip_SmallData(int size) + { + byte[] original = new byte[size]; + if (size > 0) + { + Random.Shared.NextBytes(original); + } + + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size) + 50]; + byte[] decompressed = new byte[Math.Max(size, 1)]; + + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + var compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, compressStatus); + + if (size > 0) + { + using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); + var decompressStatus = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(size, decompressedSize); + Assert.Equal(original, decompressed.AsSpan(0, size).ToArray()); + } + } + + [Fact] + public void MultipleResets_Work() + { + using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + for (int i = 0; i < 5; i++) + { + encoder.Compress(s_sampleData, destination, out _, out int written, isFinalBlock: true); + Assert.True(written > 0); + encoder.Reset(); + } + } + + #endregion + + #region Helper Methods + + private static byte[] CompressData(byte[] data, ZlibCompressionFormat format) + { + byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(data.Length)]; + using var encoder = new ZlibEncoder(6, format); + encoder.Compress(data, compressed, out _, out int written, isFinalBlock: true); + return compressed.AsSpan(0, written).ToArray(); + } + + private static byte[] CompressWithStream(byte[] data, ZlibCompressionFormat format) + { + using var output = new MemoryStream(); + using (Stream compressor = CreateCompressionStream(output, format)) + { + compressor.Write(data, 0, data.Length); + } + return output.ToArray(); + } + + private static byte[] DecompressWithStream(byte[] data, ZlibCompressionFormat format) + { + using var input = new MemoryStream(data); + using Stream decompressor = CreateDecompressionStream(input, format); + using var output = new MemoryStream(); + decompressor.CopyTo(output); + return output.ToArray(); + } + + private static Stream CreateCompressionStream(Stream stream, ZlibCompressionFormat format) + { + return format switch + { + ZlibCompressionFormat.Deflate => new DeflateStream(stream, CompressionLevel.Optimal, leaveOpen: true), + ZlibCompressionFormat.ZLib => new ZLibStream(stream, CompressionLevel.Optimal, leaveOpen: true), + ZlibCompressionFormat.GZip => new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true), + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + } + + private static Stream CreateDecompressionStream(Stream stream, ZlibCompressionFormat format) + { + return format switch + { + ZlibCompressionFormat.Deflate => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), + ZlibCompressionFormat.ZLib => new ZLibStream(stream, CompressionMode.Decompress, leaveOpen: true), + ZlibCompressionFormat.GZip => new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true), + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + } + + #endregion + } +} From 6f1ab80387f77f101799d08e5442acaf08bca693 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Jan 2026 11:28:15 +0100 Subject: [PATCH 03/15] Use CompressionLevel instead of int --- .../ref/System.IO.Compression.cs | 6 +-- .../src/System/IO/Compression/ZlibEncoder.cs | 42 +++++++++------ .../tests/ZlibEncoderDecoderTests.cs | 51 +++++++++---------- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 815c7d786c6b71..de15ae61920ef2 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -175,8 +175,8 @@ public void Reset() { } } public sealed partial class ZlibEncoder : System.IDisposable { - public ZlibEncoder(int compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { } - public ZlibEncoder(int compressionLevel, System.IO.Compression.ZlibCompressionFormat format, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public ZlibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { } + public ZlibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format, System.IO.Compression.ZLibCompressionStrategy strategy) { } public ZlibEncoder(System.IO.Compression.ZlibEncoderOptions options) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } @@ -184,7 +184,7 @@ public void Dispose() { } public static int GetMaxCompressedLength(int inputSize) { throw null; } public void Reset() { } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { throw null; } } public sealed partial class ZlibEncoderOptions { diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs index 4f94cc3f5f1241..419e03469ca44a 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs @@ -17,18 +17,18 @@ public sealed class ZlibEncoder : IDisposable private bool _finished; // Store construction parameters for Reset() - private readonly int _compressionLevel; + private readonly CompressionLevel _compressionLevel; private readonly int _windowBits; private readonly ZLibCompressionStrategy _strategy; /// /// Initializes a new instance of the class using the specified compression level and format. /// - /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression level to use. /// The compression format to use. - /// is not between -1 and 9. + /// is not a valid value. /// Failed to create the instance. - public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format) + public ZlibEncoder(CompressionLevel compressionLevel, ZlibCompressionFormat format) : this(compressionLevel, format, ZLibCompressionStrategy.Default) { } @@ -36,12 +36,12 @@ public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format) /// /// Initializes a new instance of the class using the specified compression level, format, and strategy. /// - /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression level to use. /// The compression format to use. /// The compression strategy to use. - /// is not between -1 and 9. + /// is not a valid value. /// Failed to create the instance. - public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format, ZLibCompressionStrategy strategy) + public ZlibEncoder(CompressionLevel compressionLevel, ZlibCompressionFormat format, ZLibCompressionStrategy strategy) { ValidateCompressionLevel(compressionLevel); @@ -52,7 +52,7 @@ public ZlibEncoder(int compressionLevel, ZlibCompressionFormat format, ZLibCompr _strategy = strategy; _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - (ZLibNative.CompressionLevel)_compressionLevel, + GetZLibNativeCompressionLevel(_compressionLevel), _windowBits, ZLibNative.Deflate_DefaultMemLevel, (ZLibNative.CompressionStrategy)_strategy); @@ -75,20 +75,30 @@ public ZlibEncoder(ZlibEncoderOptions options) _strategy = options.CompressionStrategy; _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - (ZLibNative.CompressionLevel)_compressionLevel, + GetZLibNativeCompressionLevel(_compressionLevel), _windowBits, ZLibNative.Deflate_DefaultMemLevel, (ZLibNative.CompressionStrategy)_strategy); } - private static void ValidateCompressionLevel(int compressionLevel) + private static void ValidateCompressionLevel(CompressionLevel compressionLevel) { - if (compressionLevel < -1 || compressionLevel > 9) + if (compressionLevel < CompressionLevel.Optimal || compressionLevel > CompressionLevel.SmallestSize) { - throw new ArgumentOutOfRangeException(nameof(compressionLevel), SR.Format(SR.ZlibEncoder_CompressionLevel, compressionLevel, -1, 9)); + throw new ArgumentOutOfRangeException(nameof(compressionLevel)); } } + private static ZLibNative.CompressionLevel GetZLibNativeCompressionLevel(CompressionLevel compressionLevel) => + compressionLevel switch + { + CompressionLevel.Optimal => ZLibNative.CompressionLevel.DefaultCompression, + CompressionLevel.Fastest => ZLibNative.CompressionLevel.BestSpeed, + CompressionLevel.NoCompression => ZLibNative.CompressionLevel.NoCompression, + CompressionLevel.SmallestSize => ZLibNative.CompressionLevel.BestCompression, + _ => throw new ArgumentOutOfRangeException(nameof(compressionLevel)), + }; + private static int GetWindowBits(ZlibCompressionFormat format) { return format switch @@ -236,7 +246,7 @@ public void Reset() // Dispose the old state and create a new one _state?.Dispose(); _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - (ZLibNative.CompressionLevel)_compressionLevel, + GetZLibNativeCompressionLevel(_compressionLevel), _windowBits, ZLibNative.Deflate_DefaultMemLevel, (ZLibNative.CompressionStrategy)_strategy); @@ -251,7 +261,7 @@ public void Reset() /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) { - return TryCompress(source, destination, out bytesWritten, compressionLevel: -1, ZlibCompressionFormat.Deflate); + return TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); } /// @@ -260,10 +270,10 @@ public static bool TryCompress(ReadOnlySpan source, Span destination /// A read-only span of bytes containing the source data to compress. /// When this method returns, a span of bytes where the compressed data is stored. /// When this method returns, the total number of bytes that were written to . - /// A number representing compression level. -1 is default, 0 is no compression, 1 is best speed, 9 is best compression. + /// The compression level to use. /// The compression format to use. /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int compressionLevel, ZlibCompressionFormat format) + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel, ZlibCompressionFormat format) { ValidateCompressionLevel(compressionLevel); diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs index 4d7f9c8f59b825..fe594c5e8d34ca 100644 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -23,14 +23,14 @@ public class ZlibEncoderDecoderTests [Fact] public void ZlibEncoder_Ctor_InvalidCompressionLevel_Throws() { - Assert.Throws(() => new ZlibEncoder(-2, ZlibCompressionFormat.Deflate)); - Assert.Throws(() => new ZlibEncoder(10, ZlibCompressionFormat.Deflate)); + Assert.Throws(() => new ZlibEncoder((CompressionLevel)(-1), ZlibCompressionFormat.Deflate)); + Assert.Throws(() => new ZlibEncoder((CompressionLevel)99, ZlibCompressionFormat.Deflate)); } [Fact] public void ZlibEncoder_Ctor_InvalidFormat_Throws() { - Assert.Throws(() => new ZlibEncoder(6, (ZlibCompressionFormat)99)); + Assert.Throws(() => new ZlibEncoder(CompressionLevel.Optimal, (ZlibCompressionFormat)99)); } [Fact] @@ -45,7 +45,7 @@ public void ZlibEncoder_Ctor_NullOptions_Throws() [InlineData(ZlibCompressionFormat.GZip)] public void ZlibEncoder_Compress_AllFormats(ZlibCompressionFormat format) { - using var encoder = new ZlibEncoder(6, format); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, format); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); @@ -59,7 +59,7 @@ public void ZlibEncoder_Compress_AllFormats(ZlibCompressionFormat format) [Fact] public void ZlibEncoder_Dispose_MultipleCallsSafe() { - var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); encoder.Dispose(); encoder.Dispose(); // Should not throw } @@ -67,7 +67,7 @@ public void ZlibEncoder_Dispose_MultipleCallsSafe() [Fact] public void ZlibEncoder_Compress_AfterDispose_Throws() { - var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); encoder.Dispose(); byte[] buffer = new byte[100]; @@ -78,7 +78,7 @@ public void ZlibEncoder_Compress_AfterDispose_Throws() [Fact] public void ZlibEncoder_Compress_AfterFinished_ReturnsDone() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; // First compression with final block @@ -95,7 +95,7 @@ public void ZlibEncoder_Compress_AfterFinished_ReturnsDone() [Fact] public void ZlibEncoder_Reset_AllowsReuse() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; // First compression @@ -149,12 +149,11 @@ public void ZlibEncoder_TryCompress_DestinationTooSmall_ReturnsFalse() } [Theory] - [InlineData(-1)] // Default - [InlineData(0)] // No compression - [InlineData(1)] // Best speed - [InlineData(6)] // Default level - [InlineData(9)] // Best compression - public void ZlibEncoder_CompressionLevels(int level) + [InlineData(CompressionLevel.Optimal)] // Default - maps to level 6 + [InlineData(CompressionLevel.NoCompression)] // No compression + [InlineData(CompressionLevel.Fastest)] // Best speed - maps to level 1 + [InlineData(CompressionLevel.SmallestSize)] // Best compression - maps to level 9 + public void ZlibEncoder_CompressionLevels(CompressionLevel level) { using var encoder = new ZlibEncoder(level, ZlibCompressionFormat.Deflate); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; @@ -174,7 +173,7 @@ public void ZlibEncoder_CompressionLevels(int level) [InlineData(ZLibCompressionStrategy.Fixed)] public void ZlibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate, strategy); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate, strategy); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); @@ -187,7 +186,7 @@ public void ZlibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) [Fact] public void ZlibEncoder_Flush() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; // Write some data without finalizing @@ -207,7 +206,7 @@ public void ZlibEncoder_Flush() [Fact] public void ZlibEncoder_DestinationTooSmall() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[5]; // Very small OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); @@ -220,7 +219,7 @@ public void ZlibEncoder_DestinationTooSmall() [Fact] public void ZlibEncoder_EmptySource() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[100]; OperationStatus status = encoder.Compress(ReadOnlySpan.Empty, destination, out int consumed, out int written, isFinalBlock: true); @@ -416,7 +415,7 @@ public void RoundTrip_WithState(ZlibCompressionFormat format) byte[] decompressed = new byte[s_sampleData.Length]; // Compress - using (var encoder = new ZlibEncoder(6, format)) + using (var encoder = new ZlibEncoder(CompressionLevel.Optimal, format)) { OperationStatus compressStatus = encoder.Compress(s_sampleData, compressed, out _, out int written, isFinalBlock: true); Assert.Equal(OperationStatus.Done, compressStatus); @@ -444,7 +443,7 @@ public void RoundTrip_Static(ZlibCompressionFormat format) byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; byte[] decompressed = new byte[s_sampleData.Length]; - bool compressSuccess = ZlibEncoder.TryCompress(s_sampleData, compressed, out int compressedSize, 6, format); + bool compressSuccess = ZlibEncoder.TryCompress(s_sampleData, compressed, out int compressedSize, CompressionLevel.Optimal, format); Assert.True(compressSuccess); compressed = compressed.AsSpan(0, compressedSize).ToArray(); @@ -490,7 +489,7 @@ public void RoundTrip_Chunks() byte[] allCompressed = new byte[ZlibEncoder.GetMaxCompressedLength(totalSize)]; int totalCompressed = 0; - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); // Compress in chunks for (int i = 0; i < totalSize; i += chunkSize) @@ -600,7 +599,7 @@ public void Compress_HighlyCompressibleData() byte[] zeros = new byte[10000]; byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(zeros.Length)]; - using var encoder = new ZlibEncoder(9, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.SmallestSize, ZlibCompressionFormat.Deflate); encoder.Compress(zeros, compressed, out _, out int written, isFinalBlock: true); // Should compress to much smaller size @@ -615,7 +614,7 @@ public void Compress_IncompressibleData() new Random(42).NextBytes(random); byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(random.Length)]; - using var encoder = new ZlibEncoder(9, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.SmallestSize, ZlibCompressionFormat.Deflate); encoder.Compress(random, compressed, out _, out int written, isFinalBlock: true); // Random data might even expand slightly @@ -638,7 +637,7 @@ public void RoundTrip_SmallData(int size) byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size) + 50]; byte[] decompressed = new byte[Math.Max(size, 1)]; - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); var compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); Assert.Equal(OperationStatus.Done, compressStatus); @@ -655,7 +654,7 @@ public void RoundTrip_SmallData(int size) [Fact] public void MultipleResets_Work() { - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; for (int i = 0; i < 5; i++) @@ -673,7 +672,7 @@ public void MultipleResets_Work() private static byte[] CompressData(byte[] data, ZlibCompressionFormat format) { byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(data.Length)]; - using var encoder = new ZlibEncoder(6, format); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, format); encoder.Compress(data, compressed, out _, out int written, isFinalBlock: true); return compressed.AsSpan(0, written).ToArray(); } From 892675932890e140240fb830ff411ed9a587d297 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Jan 2026 14:28:46 +0100 Subject: [PATCH 04/15] Use CompressionLevel instead of int --- .../System.IO.Compression/tests/ZlibEncoderDecoderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs index fe594c5e8d34ca..0117dfcc339815 100644 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -468,7 +468,7 @@ public void RoundTrip_VariousSizes(int size) byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size)]; byte[] decompressed = new byte[size]; - using var encoder = new ZlibEncoder(6, ZlibCompressionFormat.Deflate); + using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); From bb2d89c89517fc81c1d9c54ad9b283d5c019a863 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 10:51:24 +0100 Subject: [PATCH 05/15] Introduce seperate encoders and decoders --- .../ref/System.IO.Compression.cs | 71 +++-- .../src/Resources/Strings.resx | 23 +- .../src/System.IO.Compression.csproj | 10 +- .../System/IO/Compression/DeflateDecoder.cs | 136 +++++++++ .../System/IO/Compression/DeflateEncoder.cs | 274 ++++++++++++++++++ .../src/System/IO/Compression/GZipDecoder.cs | 55 ++++ .../src/System/IO/Compression/GZipEncoder.cs | 125 ++++++++ .../IO/Compression/ZlibCompressionFormat.cs | 38 --- .../src/System/IO/Compression/ZlibDecoder.cs | 178 ++---------- .../src/System/IO/Compression/ZlibEncoder.cs | 230 ++------------- .../IO/Compression/ZlibEncoderOptions.cs | 71 ----- 11 files changed, 709 insertions(+), 502 deletions(-) create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs delete mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibCompressionFormat.cs delete mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs diff --git a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index de15ae61920ef2..03580cf34ac249 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -18,6 +18,26 @@ public enum CompressionMode Decompress = 0, Compress = 1, } + public sealed partial class DeflateDecoder : System.IDisposable + { + public DeflateDecoder() { } + public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } + public void Dispose() { } + public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + } + public sealed partial class DeflateEncoder : System.IDisposable + { + public DeflateEncoder() { } + public DeflateEncoder(System.IO.Compression.CompressionLevel compressionLevel) { } + public DeflateEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public DeflateEncoder(System.IO.Compression.ZLibCompressionOptions options) { } + public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } + public void Dispose() { } + public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } + public static int GetMaxCompressedLength(int inputSize) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } + } public partial class DeflateStream : System.IO.Stream { public DeflateStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } @@ -54,6 +74,26 @@ public override void Write(System.ReadOnlySpan buffer) { } public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override void WriteByte(byte value) { } } + public sealed partial class GZipDecoder : System.IDisposable + { + public GZipDecoder() { } + public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } + public void Dispose() { } + public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + } + public sealed partial class GZipEncoder : System.IDisposable + { + public GZipEncoder() { } + public GZipEncoder(System.IO.Compression.CompressionLevel compressionLevel) { } + public GZipEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public GZipEncoder(System.IO.Compression.ZLibCompressionOptions options) { } + public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } + public void Dispose() { } + public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } + public static int GetMaxCompressedLength(int inputSize) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } + } public partial class GZipStream : System.IO.Stream { public GZipStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } @@ -158,40 +198,25 @@ public enum ZLibCompressionStrategy RunLengthEncoding = 3, Fixed = 4, } - public enum ZlibCompressionFormat - { - Deflate = 0, - ZLib = 1, - GZip = 2, - } - public sealed partial class ZlibDecoder : System.IDisposable + public sealed partial class ZLibDecoder : System.IDisposable { - public ZlibDecoder(System.IO.Compression.ZlibCompressionFormat format) { } + public ZLibDecoder() { } public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } public void Dispose() { } - public void Reset() { } public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.ZlibCompressionFormat format) { throw null; } } - public sealed partial class ZlibEncoder : System.IDisposable + public sealed partial class ZLibEncoder : System.IDisposable { - public ZlibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { } - public ZlibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format, System.IO.Compression.ZLibCompressionStrategy strategy) { } - public ZlibEncoder(System.IO.Compression.ZlibEncoderOptions options) { } + public ZLibEncoder() { } + public ZLibEncoder(System.IO.Compression.CompressionLevel compressionLevel) { } + public ZLibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public ZLibEncoder(System.IO.Compression.ZLibCompressionOptions options) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } public static int GetMaxCompressedLength(int inputSize) { throw null; } - public void Reset() { } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } - public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZlibCompressionFormat format) { throw null; } - } - public sealed partial class ZlibEncoderOptions - { - public ZlibEncoderOptions() { } - public int CompressionLevel { get { throw null; } set { } } - public System.IO.Compression.ZLibCompressionStrategy CompressionStrategy { get { throw null; } set { } } - public System.IO.Compression.ZlibCompressionFormat Format { get { throw null; } set { } } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } } public sealed partial class ZLibStream : System.IO.Stream { diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index c5fd8e58aa8931..1c07e306dacb0c 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -170,20 +170,23 @@ The underlying compression routine returned an unexpected error code: '{0}'. - - The compression level ({0}) must be between {1} and {2}, inclusive. + + The DeflateEncoder has not been initialized. Use the constructor to initialize. - - The ZlibEncoder has not been initialized. Use the constructor to initialize. + + The DeflateDecoder has not been initialized. Use the constructor to initialize. - - The ZlibDecoder has not been initialized. Use the constructor to initialize. + + The ZLibEncoder has not been initialized. Use the constructor to initialize. - - The encoder has already finished compressing. Call Reset() to reuse for a new compression operation. + + The ZLibDecoder has not been initialized. Use the constructor to initialize. - - The decoder has already finished decompressing. Call Reset() to reuse for a new decompression operation. + + The GZipEncoder has not been initialized. Use the constructor to initialize. + + + The GZipDecoder has not been initialized. Use the constructor to initialize. Central Directory corrupt. diff --git a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj index c1a709020e4726..efec302b204383 100644 --- a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj +++ b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj @@ -52,11 +52,13 @@ - + + + + - - - + + + /// Provides methods and static methods to decode data compressed in the Deflate data format in a streamless, non-allocating, and performant manner. + /// + public sealed class DeflateDecoder : IDisposable + { + private ZLibNative.ZLibStreamHandle? _state; + private bool _disposed; + private bool _finished; + + /// + /// Initializes a new instance of the class. + /// + /// Failed to create the instance. + public DeflateDecoder() + : this(ZLibNative.Deflate_DefaultWindowBits) + { + } + + internal DeflateDecoder(int windowBits) + { + _disposed = false; + _finished = false; + _state = ZLibNative.ZLibStreamHandle.CreateForInflate(windowBits); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + _state?.Dispose(); + _state = null; + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private void EnsureInitialized() + { + EnsureNotDisposed(); + if (_state is null) + { + throw new InvalidOperationException(SR.DeflateDecoder_NotInitialized); + } + } + + /// + /// Decompresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a byte span where the decompressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) + { + EnsureInitialized(); + Debug.Assert(_state is not null); + + bytesConsumed = 0; + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + unsafe + { + fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) + fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) + { + _state.NextIn = (IntPtr)inputPtr; + _state.AvailIn = (uint)source.Length; + _state.NextOut = (IntPtr)outputPtr; + _state.AvailOut = (uint)destination.Length; + + ZLibNative.ErrorCode errorCode = _state.Inflate(ZLibNative.FlushCode.NoFlush); + + bytesConsumed = source.Length - (int)_state.AvailIn; + bytesWritten = destination.Length - (int)_state.AvailOut; + + OperationStatus status = errorCode switch + { + ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 && _state.AvailOut > 0 + ? OperationStatus.NeedMoreData + : _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.NeedMoreData, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.NeedMoreData, + ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, + _ => OperationStatus.InvalidData + }; + + // Track if decompression is finished + if (errorCode == ZLibNative.ErrorCode.StreamEnd) + { + _finished = true; + } + + return status; + } + } + } + + /// + /// Tries to decompress a source byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a span of bytes where the decompressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the decompression operation was successful; otherwise. + public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + using var decoder = new DeflateDecoder(); + OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs new file mode 100644 index 00000000000000..3f7140aae36a0e --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to encode data in a streamless, non-allocating, and performant manner using the Deflate data format specification. + /// + public sealed class DeflateEncoder : IDisposable + { + private ZLibNative.ZLibStreamHandle? _state; + private bool _disposed; + private bool _finished; + + // Store construction parameters for encoder initialization + private readonly ZLibNative.CompressionLevel _zlibCompressionLevel; + private readonly ZLibCompressionStrategy _strategy; + + /// + /// Initializes a new instance of the class using the default compression level. + /// + /// Failed to create the instance. + public DeflateEncoder() + : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level. + /// + /// The compression level to use. + /// is not a valid value. + /// Failed to create the instance. + public DeflateEncoder(CompressionLevel compressionLevel) + : this(compressionLevel, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level and strategy. + /// + /// The compression level to use. + /// The compression strategy to use. + /// is not a valid value. + /// Failed to create the instance. + public DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) + : this(compressionLevel, strategy, ZLibNative.Deflate_DefaultWindowBits) + { + } + + /// + /// Initializes a new instance of the class using the specified options. + /// + /// The compression options. + /// is null. + /// Failed to create the instance. + public DeflateEncoder(ZLibCompressionOptions options) + : this(options, ZLibNative.Deflate_DefaultWindowBits) + { + } + + /// + /// Internal constructor to specify windowBits for different compression formats. + /// + internal DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy, int windowBits) + { + ValidateCompressionLevel(compressionLevel); + + _disposed = false; + _finished = false; + _zlibCompressionLevel = GetZLibNativeCompressionLevel(compressionLevel); + _strategy = strategy; + + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + _zlibCompressionLevel, + windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)_strategy); + } + + /// + /// Internal constructor to specify windowBits with options. + /// + internal DeflateEncoder(ZLibCompressionOptions options, int windowBits) + { + ArgumentNullException.ThrowIfNull(options); + + _disposed = false; + _finished = false; + _zlibCompressionLevel = (ZLibNative.CompressionLevel)options.CompressionLevel; + _strategy = options.CompressionStrategy; + + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + _zlibCompressionLevel, + windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)_strategy); + } + + private static void ValidateCompressionLevel(CompressionLevel compressionLevel) + { + if (compressionLevel < CompressionLevel.Optimal || compressionLevel > CompressionLevel.SmallestSize) + { + throw new ArgumentOutOfRangeException(nameof(compressionLevel)); + } + } + + private static ZLibNative.CompressionLevel GetZLibNativeCompressionLevel(CompressionLevel compressionLevel) => + compressionLevel switch + { + CompressionLevel.Optimal => ZLibNative.CompressionLevel.DefaultCompression, + CompressionLevel.Fastest => ZLibNative.CompressionLevel.BestSpeed, + CompressionLevel.NoCompression => ZLibNative.CompressionLevel.NoCompression, + CompressionLevel.SmallestSize => ZLibNative.CompressionLevel.BestCompression, + _ => throw new ArgumentOutOfRangeException(nameof(compressionLevel)), + }; + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + _state?.Dispose(); + _state = null; + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private void EnsureInitialized() + { + EnsureNotDisposed(); + if (_state is null) + { + throw new InvalidOperationException(SR.DeflateEncoder_NotInitialized); + } + } + + /// + /// Gets the maximum expected compressed length for the provided input size. + /// + /// The input size to get the maximum expected compressed length from. + /// A number representing the maximum compressed length for the provided input size. + /// is negative. + public static int GetMaxCompressedLength(int inputSize) + { + ArgumentOutOfRangeException.ThrowIfNegative(inputSize); + + // ZLib's compressBound formula: inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 13 + // We use a conservative estimate + long result = inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 18; + + if (result > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(inputSize)); + } + + return (int)result; + } + + /// + /// Compresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a byte span where the compressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) + { + EnsureInitialized(); + Debug.Assert(_state is not null); + + bytesConsumed = 0; + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + ZLibNative.FlushCode flushCode = isFinalBlock ? ZLibNative.FlushCode.Finish : ZLibNative.FlushCode.NoFlush; + + unsafe + { + fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) + fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) + { + _state.NextIn = (IntPtr)inputPtr; + _state.AvailIn = (uint)source.Length; + _state.NextOut = (IntPtr)outputPtr; + _state.AvailOut = (uint)destination.Length; + + ZLibNative.ErrorCode errorCode = _state.Deflate(flushCode); + + bytesConsumed = source.Length - (int)_state.AvailIn; + bytesWritten = destination.Length - (int)_state.AvailOut; + + OperationStatus status = errorCode switch + { + ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 && _state.AvailOut > 0 + ? OperationStatus.Done + : _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.Done, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.NeedMoreData, + ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, + _ => OperationStatus.InvalidData + }; + + // Track if compression is finished + if (isFinalBlock && errorCode == ZLibNative.ErrorCode.StreamEnd) + { + _finished = true; + } + + return status; + } + } + } + + /// + /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. + /// + /// When this method returns, a span of bytes where the compressed data will be stored. + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the operation finished. + public OperationStatus Flush(Span destination, out int bytesWritten) + { + return Compress(ReadOnlySpan.Empty, destination, out _, out bytesWritten, isFinalBlock: false); + } + + /// + /// Tries to compress a source byte span into a destination span using the default compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + return TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); + } + + /// + /// Tries to compress a source byte span into a destination span using the specified compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// The compression level to use. + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + { + ValidateCompressionLevel(compressionLevel); + + using var encoder = new DeflateEncoder(compressionLevel); + OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs new file mode 100644 index 00000000000000..685946c1e00eac --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to decode data compressed in the GZip data format in a streamless, non-allocating, and performant manner. + /// + public sealed class GZipDecoder : IDisposable + { + private readonly DeflateDecoder _deflateDecoder; + + /// + /// Initializes a new instance of the class. + /// + /// Failed to create the instance. + public GZipDecoder() + { + _deflateDecoder = new DeflateDecoder(ZLibNative.GZip_DefaultWindowBits); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() => _deflateDecoder.Dispose(); + + /// + /// Decompresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a byte span where the decompressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) + => _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + + /// + /// Tries to decompress a source byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a span of bytes where the decompressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the decompression operation was successful; otherwise. + public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + using var decoder = new GZipDecoder(); + OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs new file mode 100644 index 00000000000000..8ec69608851382 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to encode data in a streamless, non-allocating, and performant manner using the GZip data format specification. + /// + public sealed class GZipEncoder : IDisposable + { + private readonly DeflateEncoder _deflateEncoder; + + /// + /// Initializes a new instance of the class using the default compression level. + /// + /// Failed to create the instance. + public GZipEncoder() + : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level. + /// + /// The compression level to use. + /// is not a valid value. + /// Failed to create the instance. + public GZipEncoder(CompressionLevel compressionLevel) + : this(compressionLevel, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level and strategy. + /// + /// The compression level to use. + /// The compression strategy to use. + /// is not a valid value. + /// Failed to create the instance. + public GZipEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) + { + _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.GZip_DefaultWindowBits); + } + + /// + /// Initializes a new instance of the class using the specified options. + /// + /// The compression options. + /// is null. + /// Failed to create the instance. + public GZipEncoder(ZLibCompressionOptions options) + { + _deflateEncoder = new DeflateEncoder(options, ZLibNative.GZip_DefaultWindowBits); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() => _deflateEncoder.Dispose(); + + /// + /// Gets the maximum expected compressed length for the provided input size. + /// + /// The input size to get the maximum expected compressed length from. + /// A number representing the maximum compressed length for the provided input size. + /// is negative. + public static int GetMaxCompressedLength(int inputSize) + { + // GZip has a larger header than raw deflate, so add extra overhead + int baseLength = DeflateEncoder.GetMaxCompressedLength(inputSize); + + // GZip adds ~18 bytes header/trailer overhead + return baseLength + 10; + } + + /// + /// Compresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a byte span where the compressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) + => _deflateEncoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + /// + /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. + /// + /// When this method returns, a span of bytes where the compressed data will be stored. + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the operation finished. + public OperationStatus Flush(Span destination, out int bytesWritten) + => _deflateEncoder.Flush(destination, out bytesWritten); + + /// + /// Tries to compress a source byte span into a destination span using the default compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) + => TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); + + /// + /// Tries to compress a source byte span into a destination span using the specified compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// The compression level to use. + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + { + using var encoder = new GZipEncoder(compressionLevel); + OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibCompressionFormat.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibCompressionFormat.cs deleted file mode 100644 index 2c51ce3c8c431e..00000000000000 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibCompressionFormat.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.IO.Compression -{ - /// - /// Specifies the compression format for and . - /// - public enum ZlibCompressionFormat - { - /// - /// Raw deflate format without any header or trailer. - /// - /// - /// This format produces the smallest output but provides no error checking. - /// It is compatible with . - /// - Deflate = 0, - - /// - /// ZLib format with a small header and Adler-32 checksum trailer. - /// - /// - /// This format adds a 2-byte header and 4-byte Adler-32 checksum for error detection. - /// It is compatible with . - /// - ZLib = 1, - - /// - /// GZip format with header and CRC-32 checksum trailer. - /// - /// - /// This format adds a larger header with optional metadata and a CRC-32 checksum. - /// It is compatible with and the gzip file format. - /// - GZip = 2 - } -} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs index ef874be2f260f2..5975cbf2377a6d 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs @@ -2,187 +2,51 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics; -using System.Runtime.InteropServices; namespace System.IO.Compression { /// - /// Provides non-allocating, performant decompression methods for data compressed using the Deflate, ZLib, or GZip data format specification. + /// Provides methods and static methods to decode data compressed in the ZLib data format in a streamless, non-allocating, and performant manner. /// - public sealed class ZlibDecoder : IDisposable + public sealed class ZLibDecoder : IDisposable { - private ZLibNative.ZLibStreamHandle? _state; - private bool _disposed; - private bool _finished; - - // Store construction parameters for Reset() - private readonly int _windowBits; + private readonly DeflateDecoder _deflateDecoder; /// - /// Initializes a new instance of the class using the specified format. + /// Initializes a new instance of the class. /// - /// The compression format to decompress. - /// is not a valid value. - /// Failed to create the instance. - public ZlibDecoder(ZlibCompressionFormat format) - { - _disposed = false; - _finished = false; - _windowBits = GetWindowBits(format); - - _state = ZLibNative.ZLibStreamHandle.CreateForInflate(_windowBits); - } - - private static int GetWindowBits(ZlibCompressionFormat format) + /// Failed to create the instance. + public ZLibDecoder() { - return format switch - { - ZlibCompressionFormat.Deflate => ZLibNative.Deflate_DefaultWindowBits, - ZlibCompressionFormat.ZLib => ZLibNative.ZLib_DefaultWindowBits, - ZlibCompressionFormat.GZip => ZLibNative.GZip_DefaultWindowBits, - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; + _deflateDecoder = new DeflateDecoder(ZLibNative.ZLib_DefaultWindowBits); } /// /// Frees and disposes unmanaged resources. /// - public void Dispose() - { - _disposed = true; - _state?.Dispose(); - _state = null; - } - - private void EnsureNotDisposed() - { - ObjectDisposedException.ThrowIf(_disposed, this); - } - - private void EnsureInitialized() - { - EnsureNotDisposed(); - if (_state is null) - { - throw new InvalidOperationException(SR.ZlibDecoder_NotInitialized); - } - } + public void Dispose() => _deflateDecoder.Dispose(); /// - /// Decompresses data that was compressed using the Deflate, ZLib, or GZip algorithm. + /// Decompresses a read-only byte span into a destination span. /// - /// A buffer containing the compressed data. - /// When this method returns, a byte span containing the decompressed data. - /// The total number of bytes that were read from . - /// The total number of bytes that were written in the . - /// One of the enumeration values that indicates the status of the decompression operation. - /// - /// The return value can be as follows: - /// - : was successfully and completely decompressed into . - /// - : There is not enough space in to decompress . - /// - : The decompression action is partially done. At least one more byte is required to complete the decompression task. - /// - : The data in is invalid and could not be decompressed. - /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a byte span where the decompressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the span-based operation finished. public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) - { - EnsureInitialized(); - Debug.Assert(_state is not null); - - bytesConsumed = 0; - bytesWritten = 0; - - if (_finished) - { - return OperationStatus.Done; - } - - if (source.IsEmpty) - { - return OperationStatus.NeedMoreData; - } - - unsafe - { - fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) - fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) - { - _state.NextIn = (IntPtr)inputPtr; - _state.AvailIn = (uint)source.Length; - _state.NextOut = (IntPtr)outputPtr; - _state.AvailOut = (uint)destination.Length; - - ZLibNative.ErrorCode errorCode = _state.Inflate(ZLibNative.FlushCode.NoFlush); - - bytesConsumed = source.Length - (int)_state.AvailIn; - bytesWritten = destination.Length - (int)_state.AvailOut; - - OperationStatus status = errorCode switch - { - ZLibNative.ErrorCode.Ok => _state.AvailOut == 0 - ? OperationStatus.DestinationTooSmall - : _state.AvailIn == 0 - ? OperationStatus.NeedMoreData - : OperationStatus.Done, - ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, - ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 - ? OperationStatus.DestinationTooSmall - : OperationStatus.NeedMoreData, - ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, - _ => OperationStatus.InvalidData - }; - - // Track if decompression is finished - if (errorCode == ZLibNative.ErrorCode.StreamEnd) - { - _finished = true; - } - - return status; - } - } - } - - /// - /// Resets the decoder to its initial state, allowing it to be reused for a new decompression operation. - /// - /// The decoder has been disposed. - public void Reset() - { - EnsureNotDisposed(); - - _finished = false; - - // Dispose the old state and create a new one - _state?.Dispose(); - _state = ZLibNative.ZLibStreamHandle.CreateForInflate(_windowBits); - } + => _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); /// - /// Attempts to decompress data. + /// Tries to decompress a source byte span into a destination span. /// - /// A buffer containing the compressed data. - /// When this method returns, a byte span containing the decompressed data. - /// The total number of bytes that were written in the . - /// on success; otherwise. - /// If this method returns , may be empty or contain partially decompressed data, with being zero or greater than zero but less than the expected total. + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a span of bytes where the decompressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the decompression operation was successful; otherwise. public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) { - return TryDecompress(source, destination, out bytesWritten, ZlibCompressionFormat.Deflate); - } - - /// - /// Attempts to decompress data using the specified format. - /// - /// A buffer containing the compressed data. - /// When this method returns, a byte span containing the decompressed data. - /// The total number of bytes that were written in the . - /// The compression format to decompress. - /// on success; otherwise. - /// If this method returns , may be empty or contain partially decompressed data, with being zero or greater than zero but less than the expected total. - public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten, ZlibCompressionFormat format) - { - using var decoder = new ZlibDecoder(format); + using var decoder = new ZLibDecoder(); OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); return status == OperationStatus.Done && consumed == source.Length; diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs index 419e03469ca44a..59b30a96e555c4 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs @@ -2,137 +2,63 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics; -using System.Runtime.InteropServices; namespace System.IO.Compression { /// - /// Provides methods and static methods to encode and decode data in a streamless, non-allocating, and performant manner using the Deflate, ZLib, or GZip data format specification. + /// Provides methods and static methods to encode data in a streamless, non-allocating, and performant manner using the ZLib data format specification. /// - public sealed class ZlibEncoder : IDisposable + public sealed class ZLibEncoder : IDisposable { - private ZLibNative.ZLibStreamHandle? _state; - private bool _disposed; - private bool _finished; + private readonly DeflateEncoder _deflateEncoder; - // Store construction parameters for Reset() - private readonly CompressionLevel _compressionLevel; - private readonly int _windowBits; - private readonly ZLibCompressionStrategy _strategy; + /// + /// Initializes a new instance of the class using the default compression level. + /// + /// Failed to create the instance. + public ZLibEncoder() + : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + { + } /// - /// Initializes a new instance of the class using the specified compression level and format. + /// Initializes a new instance of the class using the specified compression level. /// /// The compression level to use. - /// The compression format to use. /// is not a valid value. - /// Failed to create the instance. - public ZlibEncoder(CompressionLevel compressionLevel, ZlibCompressionFormat format) - : this(compressionLevel, format, ZLibCompressionStrategy.Default) + /// Failed to create the instance. + public ZLibEncoder(CompressionLevel compressionLevel) + : this(compressionLevel, ZLibCompressionStrategy.Default) { } /// - /// Initializes a new instance of the class using the specified compression level, format, and strategy. + /// Initializes a new instance of the class using the specified compression level and strategy. /// /// The compression level to use. - /// The compression format to use. /// The compression strategy to use. /// is not a valid value. - /// Failed to create the instance. - public ZlibEncoder(CompressionLevel compressionLevel, ZlibCompressionFormat format, ZLibCompressionStrategy strategy) + /// Failed to create the instance. + public ZLibEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) { - ValidateCompressionLevel(compressionLevel); - - _disposed = false; - _finished = false; - _compressionLevel = compressionLevel; - _windowBits = GetWindowBits(format); - _strategy = strategy; - - _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - GetZLibNativeCompressionLevel(_compressionLevel), - _windowBits, - ZLibNative.Deflate_DefaultMemLevel, - (ZLibNative.CompressionStrategy)_strategy); + _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.ZLib_DefaultWindowBits); } /// - /// Initializes a new instance of the class using the specified options. + /// Initializes a new instance of the class using the specified options. /// /// The compression options. /// is null. - /// Failed to create the instance. - public ZlibEncoder(ZlibEncoderOptions options) - { - ArgumentNullException.ThrowIfNull(options); - - _disposed = false; - _finished = false; - _compressionLevel = options.CompressionLevel; - _windowBits = GetWindowBits(options.Format); - _strategy = options.CompressionStrategy; - - _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - GetZLibNativeCompressionLevel(_compressionLevel), - _windowBits, - ZLibNative.Deflate_DefaultMemLevel, - (ZLibNative.CompressionStrategy)_strategy); - } - - private static void ValidateCompressionLevel(CompressionLevel compressionLevel) - { - if (compressionLevel < CompressionLevel.Optimal || compressionLevel > CompressionLevel.SmallestSize) - { - throw new ArgumentOutOfRangeException(nameof(compressionLevel)); - } - } - - private static ZLibNative.CompressionLevel GetZLibNativeCompressionLevel(CompressionLevel compressionLevel) => - compressionLevel switch - { - CompressionLevel.Optimal => ZLibNative.CompressionLevel.DefaultCompression, - CompressionLevel.Fastest => ZLibNative.CompressionLevel.BestSpeed, - CompressionLevel.NoCompression => ZLibNative.CompressionLevel.NoCompression, - CompressionLevel.SmallestSize => ZLibNative.CompressionLevel.BestCompression, - _ => throw new ArgumentOutOfRangeException(nameof(compressionLevel)), - }; - - private static int GetWindowBits(ZlibCompressionFormat format) + /// Failed to create the instance. + public ZLibEncoder(ZLibCompressionOptions options) { - return format switch - { - ZlibCompressionFormat.Deflate => ZLibNative.Deflate_DefaultWindowBits, - ZlibCompressionFormat.ZLib => ZLibNative.ZLib_DefaultWindowBits, - ZlibCompressionFormat.GZip => ZLibNative.GZip_DefaultWindowBits, - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; + _deflateEncoder = new DeflateEncoder(options, ZLibNative.ZLib_DefaultWindowBits); } /// /// Frees and disposes unmanaged resources. /// - public void Dispose() - { - _disposed = true; - _state?.Dispose(); - _state = null; - } - - private void EnsureNotDisposed() - { - ObjectDisposedException.ThrowIf(_disposed, this); - } - - private void EnsureInitialized() - { - EnsureNotDisposed(); - if (_state is null) - { - throw new InvalidOperationException(SR.ZlibEncoder_NotInitialized); - } - } + public void Dispose() => _deflateEncoder.Dispose(); /// /// Gets the maximum expected compressed length for the provided input size. @@ -140,22 +66,7 @@ private void EnsureInitialized() /// The input size to get the maximum expected compressed length from. /// A number representing the maximum compressed length for the provided input size. /// is negative. - public static int GetMaxCompressedLength(int inputSize) - { - ArgumentOutOfRangeException.ThrowIfNegative(inputSize); - - // ZLib's compressBound formula: inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 13 - // For GZip, add 18 bytes for header/trailer - // We use a conservative estimate that works for all formats - long result = inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 32; - - if (result > int.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(inputSize)); - } - - return (int)result; - } + public static int GetMaxCompressedLength(int inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); /// /// Compresses a read-only byte span into a destination span. @@ -167,60 +78,7 @@ public static int GetMaxCompressedLength(int inputSize) /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. /// One of the enumeration values that describes the status with which the span-based operation finished. public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) - { - EnsureInitialized(); - Debug.Assert(_state is not null); - - bytesConsumed = 0; - bytesWritten = 0; - - if (_finished) - { - return OperationStatus.Done; - } - - ZLibNative.FlushCode flushCode = isFinalBlock ? ZLibNative.FlushCode.Finish : ZLibNative.FlushCode.NoFlush; - - unsafe - { - fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) - fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) - { - _state.NextIn = (IntPtr)inputPtr; - _state.AvailIn = (uint)source.Length; - _state.NextOut = (IntPtr)outputPtr; - _state.AvailOut = (uint)destination.Length; - - ZLibNative.ErrorCode errorCode = _state.Deflate(flushCode); - - bytesConsumed = source.Length - (int)_state.AvailIn; - bytesWritten = destination.Length - (int)_state.AvailOut; - - OperationStatus status = errorCode switch - { - ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 && _state.AvailOut > 0 - ? OperationStatus.Done - : _state.AvailOut == 0 - ? OperationStatus.DestinationTooSmall - : OperationStatus.Done, - ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, - ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 - ? OperationStatus.DestinationTooSmall - : OperationStatus.NeedMoreData, - ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, - _ => OperationStatus.InvalidData - }; - - // Track if compression is finished - if (isFinalBlock && errorCode == ZLibNative.ErrorCode.StreamEnd) - { - _finished = true; - } - - return status; - } - } - } + => _deflateEncoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); /// /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. @@ -229,28 +87,7 @@ public OperationStatus Compress(ReadOnlySpan source, Span destinatio /// When this method returns, the total number of bytes that were written to . /// One of the enumeration values that describes the status with which the operation finished. public OperationStatus Flush(Span destination, out int bytesWritten) - { - return Compress(ReadOnlySpan.Empty, destination, out _, out bytesWritten, isFinalBlock: false); - } - - /// - /// Resets the encoder to its initial state, allowing it to be reused for a new compression operation. - /// - /// The encoder has been disposed. - public void Reset() - { - EnsureNotDisposed(); - - _finished = false; - - // Dispose the old state and create a new one - _state?.Dispose(); - _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - GetZLibNativeCompressionLevel(_compressionLevel), - _windowBits, - ZLibNative.Deflate_DefaultMemLevel, - (ZLibNative.CompressionStrategy)_strategy); - } + => _deflateEncoder.Flush(destination, out bytesWritten); /// /// Tries to compress a source byte span into a destination span using the default compression level. @@ -260,24 +97,19 @@ public void Reset() /// When this method returns, the total number of bytes that were written to . /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) - { - return TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - } + => TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); /// - /// Tries to compress a source byte span into a destination span using the specified compression level and format. + /// Tries to compress a source byte span into a destination span using the specified compression level. /// /// A read-only span of bytes containing the source data to compress. /// When this method returns, a span of bytes where the compressed data is stored. /// When this method returns, the total number of bytes that were written to . /// The compression level to use. - /// The compression format to use. /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel, ZlibCompressionFormat format) + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) { - ValidateCompressionLevel(compressionLevel); - - using var encoder = new ZlibEncoder(compressionLevel, format); + using var encoder = new ZLibEncoder(compressionLevel); OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); return status == OperationStatus.Done && consumed == source.Length; diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs deleted file mode 100644 index 1309c084c781db..00000000000000 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.IO.Compression -{ - /// - /// Provides compression options for . - /// - public sealed class ZlibEncoderOptions - { - private int _compressionLevel = -1; - private ZLibCompressionStrategy _compressionStrategy; - private ZlibCompressionFormat _format = ZlibCompressionFormat.Deflate; - - /// - /// Gets or sets the compression level for the encoder. - /// - /// The value is less than -1 or greater than 9. - /// - /// The compression level can be any value between -1 and 9 (inclusive). - /// -1 requests the default compression level (currently equivalent to 6). - /// 0 gives no compression. - /// 1 gives best speed. - /// 9 gives best compression. - /// The default value is -1. - /// - public int CompressionLevel - { - get => _compressionLevel; - set - { - ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 9); - - _compressionLevel = value; - } - } - - /// - /// Gets or sets the compression strategy for the encoder. - /// - /// The value is not a valid value. - public ZLibCompressionStrategy CompressionStrategy - { - get => _compressionStrategy; - set - { - ArgumentOutOfRangeException.ThrowIfLessThan((int)value, (int)ZLibCompressionStrategy.Default, nameof(value)); - ArgumentOutOfRangeException.ThrowIfGreaterThan((int)value, (int)ZLibCompressionStrategy.Fixed, nameof(value)); - - _compressionStrategy = value; - } - } - - /// - /// Gets or sets the compression format for the encoder. - /// - /// The value is not a valid value. - public ZlibCompressionFormat Format - { - get => _format; - set - { - ArgumentOutOfRangeException.ThrowIfLessThan((int)value, (int)ZlibCompressionFormat.Deflate, nameof(value)); - ArgumentOutOfRangeException.ThrowIfGreaterThan((int)value, (int)ZlibCompressionFormat.GZip, nameof(value)); - - _format = value; - } - } - } -} From a21a800804cc0c113dd0e7c06b746505bc5127f4 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 10:52:11 +0100 Subject: [PATCH 06/15] Update the tests accordingly --- .../DeflateZLibGZipEncoderDecoderTests.cs | 623 +++++++++++++++ .../tests/System.IO.Compression.Tests.csproj | 2 +- .../tests/ZlibEncoderDecoderTests.cs | 723 ------------------ 3 files changed, 624 insertions(+), 724 deletions(-) create mode 100644 src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs delete mode 100644 src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs diff --git a/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs new file mode 100644 index 00000000000000..5a5ca839baafa6 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs @@ -0,0 +1,623 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Text; +using Xunit; + +namespace System.IO.Compression +{ + public class DeflateEncoderDecoderTests + { + private static readonly byte[] s_sampleData = Encoding.UTF8.GetBytes( + "Hello, World! This is a test string for compression. " + + "We need some repeated content to make compression effective. " + + "Hello, World! This is a test string for compression. " + + "The quick brown fox jumps over the lazy dog. " + + "Sphinx of black quartz, judge my vow."); + + #region DeflateEncoder Tests + + [Fact] + public void DeflateEncoder_Ctor_InvalidCompressionLevel_Throws() + { + Assert.Throws(() => new DeflateEncoder((CompressionLevel)(-1))); + Assert.Throws(() => new DeflateEncoder((CompressionLevel)99)); + } + + [Fact] + public void DeflateEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new DeflateEncoder(null!)); + } + + [Fact] + public void DeflateEncoder_Compress_Success() + { + using var encoder = new DeflateEncoder(CompressionLevel.Optimal); + byte[] destination = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, bytesConsumed); + Assert.True(bytesWritten > 0); + Assert.True(bytesWritten < s_sampleData.Length); + } + + [Fact] + public void DeflateEncoder_Dispose_MultipleCallsSafe() + { + var encoder = new DeflateEncoder(CompressionLevel.Optimal); + encoder.Dispose(); + encoder.Dispose(); + } + + [Fact] + public void DeflateEncoder_Compress_AfterDispose_Throws() + { + var encoder = new DeflateEncoder(CompressionLevel.Optimal); + encoder.Dispose(); + + byte[] buffer = new byte[100]; + Assert.Throws(() => + encoder.Compress(s_sampleData, buffer, out _, out _, isFinalBlock: true)); + } + + [Fact] + public void DeflateEncoder_GetMaxCompressedLength_ValidValues() + { + Assert.True(DeflateEncoder.GetMaxCompressedLength(0) >= 0); + Assert.True(DeflateEncoder.GetMaxCompressedLength(100) >= 100); + Assert.True(DeflateEncoder.GetMaxCompressedLength(1000) >= 1000); + } + + [Fact] + public void DeflateEncoder_GetMaxCompressedLength_NegativeInput_Throws() + { + Assert.Throws(() => DeflateEncoder.GetMaxCompressedLength(-1)); + } + + [Fact] + public void DeflateEncoder_TryCompress_Success() + { + byte[] destination = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + bool success = DeflateEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.True(success); + Assert.True(bytesWritten > 0); + } + + [Fact] + public void DeflateEncoder_TryCompress_DestinationTooSmall_ReturnsFalse() + { + byte[] destination = new byte[1]; + + bool success = DeflateEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.False(success); + } + + [Theory] + [InlineData(CompressionLevel.Optimal)] + [InlineData(CompressionLevel.NoCompression)] + [InlineData(CompressionLevel.Fastest)] + [InlineData(CompressionLevel.SmallestSize)] + public void DeflateEncoder_CompressionLevels(CompressionLevel level) + { + using var encoder = new DeflateEncoder(level); + byte[] destination = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Theory] + [InlineData(ZLibCompressionStrategy.Default)] + [InlineData(ZLibCompressionStrategy.Filtered)] + [InlineData(ZLibCompressionStrategy.HuffmanOnly)] + [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] + [InlineData(ZLibCompressionStrategy.Fixed)] + public void DeflateEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) + { + using var encoder = new DeflateEncoder(CompressionLevel.Optimal, strategy); + byte[] destination = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void DeflateEncoder_WithOptions() + { + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new DeflateEncoder(options); + byte[] destination = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + #endregion + + #region DeflateDecoder Tests + + [Fact] + public void DeflateDecoder_Decompress_Success() + { + byte[] compressed = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new DeflateEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + using var decoder = new DeflateDecoder(); + byte[] decompressed = new byte[s_sampleData.Length]; + + OperationStatus status = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressedSize, consumed); + Assert.Equal(s_sampleData.Length, written); + Assert.Equal(s_sampleData, decompressed); + } + + [Fact] + public void DeflateDecoder_Dispose_MultipleCallsSafe() + { + var decoder = new DeflateDecoder(); + decoder.Dispose(); + decoder.Dispose(); + } + + [Fact] + public void DeflateDecoder_Decompress_AfterDispose_Throws() + { + var decoder = new DeflateDecoder(); + decoder.Dispose(); + + byte[] buffer = new byte[100]; + Assert.Throws(() => + decoder.Decompress(buffer, buffer, out _, out _)); + } + + [Fact] + public void DeflateDecoder_TryDecompress_Success() + { + byte[] compressed = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new DeflateEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[s_sampleData.Length]; + + bool success = DeflateDecoder.TryDecompress(compressed.AsSpan(0, compressedSize), decompressed, out int bytesWritten); + + Assert.True(success); + Assert.Equal(s_sampleData.Length, bytesWritten); + Assert.Equal(s_sampleData, decompressed); + } + + [Fact] + public void DeflateDecoder_InvalidData_ReturnsInvalidData() + { + byte[] invalidData = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + + using var decoder = new DeflateDecoder(); + byte[] decompressed = new byte[100]; + + OperationStatus status = decoder.Decompress(invalidData, decompressed, out _, out _); + + Assert.Equal(OperationStatus.InvalidData, status); + } + + #endregion + + #region RoundTrip Tests + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void DeflateEncoder_Decoder_RoundTrip(int dataSize) + { + byte[] original = new byte[dataSize]; + new Random(42).NextBytes(original); + + byte[] compressed = new byte[DeflateEncoder.GetMaxCompressedLength(dataSize)]; + using var encoder = new DeflateEncoder(CompressionLevel.Optimal); + OperationStatus compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, compressStatus); + + byte[] decompressed = new byte[dataSize]; + using var decoder = new DeflateDecoder(); + OperationStatus decompressStatus = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); + + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(dataSize, decompressedSize); + Assert.Equal(original, decompressed); + } + + [Fact] + public void DeflateEncoder_Decoder_RoundTrip_AllCompressionLevels() + { + foreach (CompressionLevel level in Enum.GetValues()) + { + byte[] compressed = new byte[DeflateEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new DeflateEncoder(level); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[s_sampleData.Length]; + using var decoder = new DeflateDecoder(); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); + + Assert.Equal(s_sampleData, decompressed); + } + } + + #endregion + } + + public class ZLibEncoderDecoderTests + { + private static readonly byte[] s_sampleData = Encoding.UTF8.GetBytes( + "Hello, World! This is a test string for compression. " + + "We need some repeated content to make compression effective. " + + "Hello, World! This is a test string for compression. " + + "The quick brown fox jumps over the lazy dog. " + + "Sphinx of black quartz, judge my vow."); + + #region ZLibEncoder Tests + + [Fact] + public void ZLibEncoder_Ctor_InvalidCompressionLevel_Throws() + { + Assert.Throws(() => new ZLibEncoder((CompressionLevel)(-1))); + Assert.Throws(() => new ZLibEncoder((CompressionLevel)99)); + } + + [Fact] + public void ZLibEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new ZLibEncoder(null!)); + } + + [Fact] + public void ZLibEncoder_Compress_Success() + { + using var encoder = new ZLibEncoder(CompressionLevel.Optimal); + byte[] destination = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, bytesConsumed); + Assert.True(bytesWritten > 0); + Assert.True(bytesWritten < s_sampleData.Length); + } + + [Fact] + public void ZLibEncoder_TryCompress_Success() + { + byte[] destination = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + bool success = ZLibEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.True(success); + Assert.True(bytesWritten > 0); + } + + [Theory] + [InlineData(CompressionLevel.Optimal)] + [InlineData(CompressionLevel.NoCompression)] + [InlineData(CompressionLevel.Fastest)] + [InlineData(CompressionLevel.SmallestSize)] + public void ZLibEncoder_CompressionLevels(CompressionLevel level) + { + using var encoder = new ZLibEncoder(level); + byte[] destination = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void ZLibEncoder_WithOptions() + { + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new ZLibEncoder(options); + byte[] destination = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + #endregion + + #region ZLibDecoder Tests + + [Fact] + public void ZLibDecoder_Decompress_Success() + { + byte[] compressed = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new ZLibEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + using var decoder = new ZLibDecoder(); + byte[] decompressed = new byte[s_sampleData.Length]; + + OperationStatus status = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressedSize, consumed); + Assert.Equal(s_sampleData.Length, written); + Assert.Equal(s_sampleData, decompressed); + } + + [Fact] + public void ZLibDecoder_TryDecompress_Success() + { + byte[] compressed = new byte[ZLibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new ZLibEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[s_sampleData.Length]; + + bool success = ZLibDecoder.TryDecompress(compressed.AsSpan(0, compressedSize), decompressed, out int bytesWritten); + + Assert.True(success); + Assert.Equal(s_sampleData.Length, bytesWritten); + Assert.Equal(s_sampleData, decompressed); + } + + #endregion + + #region RoundTrip Tests + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void ZLibEncoder_Decoder_RoundTrip(int dataSize) + { + byte[] original = new byte[dataSize]; + new Random(42).NextBytes(original); + + byte[] compressed = new byte[ZLibEncoder.GetMaxCompressedLength(dataSize)]; + using var encoder = new ZLibEncoder(CompressionLevel.Optimal); + OperationStatus compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, compressStatus); + + byte[] decompressed = new byte[dataSize]; + using var decoder = new ZLibDecoder(); + OperationStatus decompressStatus = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); + + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(dataSize, decompressedSize); + Assert.Equal(original, decompressed); + } + + #endregion + } + + public class GZipEncoderDecoderTests + { + private static readonly byte[] s_sampleData = Encoding.UTF8.GetBytes( + "Hello, World! This is a test string for compression. " + + "We need some repeated content to make compression effective. " + + "Hello, World! This is a test string for compression. " + + "The quick brown fox jumps over the lazy dog. " + + "Sphinx of black quartz, judge my vow."); + + #region GZipEncoder Tests + + [Fact] + public void GZipEncoder_Ctor_InvalidCompressionLevel_Throws() + { + Assert.Throws(() => new GZipEncoder((CompressionLevel)(-1))); + Assert.Throws(() => new GZipEncoder((CompressionLevel)99)); + } + + [Fact] + public void GZipEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new GZipEncoder(null!)); + } + + [Fact] + public void GZipEncoder_Compress_Success() + { + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + byte[] destination = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, bytesConsumed); + Assert.True(bytesWritten > 0); + Assert.True(bytesWritten < s_sampleData.Length); + } + + [Fact] + public void GZipEncoder_TryCompress_Success() + { + byte[] destination = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + bool success = GZipEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); + + Assert.True(success); + Assert.True(bytesWritten > 0); + } + + [Theory] + [InlineData(CompressionLevel.Optimal)] + [InlineData(CompressionLevel.NoCompression)] + [InlineData(CompressionLevel.Fastest)] + [InlineData(CompressionLevel.SmallestSize)] + public void GZipEncoder_CompressionLevels(CompressionLevel level) + { + using var encoder = new GZipEncoder(level); + byte[] destination = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void GZipEncoder_WithOptions() + { + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new GZipEncoder(options); + byte[] destination = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + + OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, consumed); + Assert.True(written > 0); + } + + #endregion + + #region GZipDecoder Tests + + [Fact] + public void GZipDecoder_Decompress_Success() + { + byte[] compressed = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + using var decoder = new GZipDecoder(); + byte[] decompressed = new byte[s_sampleData.Length]; + + OperationStatus status = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out int consumed, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressedSize, consumed); + Assert.Equal(s_sampleData.Length, written); + Assert.Equal(s_sampleData, decompressed); + } + + [Fact] + public void GZipDecoder_TryDecompress_Success() + { + byte[] compressed = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[s_sampleData.Length]; + + bool success = GZipDecoder.TryDecompress(compressed.AsSpan(0, compressedSize), decompressed, out int bytesWritten); + + Assert.True(success); + Assert.Equal(s_sampleData.Length, bytesWritten); + Assert.Equal(s_sampleData, decompressed); + } + + #endregion + + #region RoundTrip Tests + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void GZipEncoder_Decoder_RoundTrip(int dataSize) + { + byte[] original = new byte[dataSize]; + new Random(42).NextBytes(original); + + byte[] compressed = new byte[GZipEncoder.GetMaxCompressedLength(dataSize)]; + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + OperationStatus compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, compressStatus); + + byte[] decompressed = new byte[dataSize]; + using var decoder = new GZipDecoder(); + OperationStatus decompressStatus = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); + + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(dataSize, decompressedSize); + Assert.Equal(original, decompressed); + } + + #endregion + + #region Cross-Format Compatibility Tests + + [Fact] + public void GZipEncoder_GZipStream_Interop() + { + byte[] compressed = new byte[GZipEncoder.GetMaxCompressedLength(s_sampleData.Length)]; + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + encoder.Compress(s_sampleData, compressed, out _, out int compressedSize, isFinalBlock: true); + + using var ms = new MemoryStream(compressed, 0, compressedSize); + using var gzipStream = new GZipStream(ms, CompressionMode.Decompress); + using var resultStream = new MemoryStream(); + gzipStream.CopyTo(resultStream); + + Assert.Equal(s_sampleData, resultStream.ToArray()); + } + + [Fact] + public void GZipStream_GZipDecoder_Interop() + { + using var ms = new MemoryStream(); + using (var gzipStream = new GZipStream(ms, CompressionLevel.Optimal, leaveOpen: true)) + { + gzipStream.Write(s_sampleData); + } + + byte[] compressed = ms.ToArray(); + byte[] decompressed = new byte[s_sampleData.Length]; + + using var decoder = new GZipDecoder(); + OperationStatus status = decoder.Decompress(compressed, decompressed, out _, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(s_sampleData.Length, written); + Assert.Equal(s_sampleData, decompressed); + } + + #endregion + } +} diff --git a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj index 5191c4f8ab1d7f..437a931d3848a9 100644 --- a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj +++ b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs deleted file mode 100644 index 0117dfcc339815..00000000000000 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ /dev/null @@ -1,723 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit; - -namespace System.IO.Compression -{ - public class ZlibEncoderDecoderTests - { - private static readonly byte[] s_sampleData = Encoding.UTF8.GetBytes( - "Hello, World! This is a test string for compression. " + - "We need some repeated content to make compression effective. " + - "Hello, World! This is a test string for compression. " + - "The quick brown fox jumps over the lazy dog. " + - "Sphinx of black quartz, judge my vow."); - - #region ZlibEncoder Tests - - [Fact] - public void ZlibEncoder_Ctor_InvalidCompressionLevel_Throws() - { - Assert.Throws(() => new ZlibEncoder((CompressionLevel)(-1), ZlibCompressionFormat.Deflate)); - Assert.Throws(() => new ZlibEncoder((CompressionLevel)99, ZlibCompressionFormat.Deflate)); - } - - [Fact] - public void ZlibEncoder_Ctor_InvalidFormat_Throws() - { - Assert.Throws(() => new ZlibEncoder(CompressionLevel.Optimal, (ZlibCompressionFormat)99)); - } - - [Fact] - public void ZlibEncoder_Ctor_NullOptions_Throws() - { - Assert.Throws(() => new ZlibEncoder(null!)); - } - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void ZlibEncoder_Compress_AllFormats(ZlibCompressionFormat format) - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, format); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - OperationStatus status = encoder.Compress(s_sampleData, destination, out int bytesConsumed, out int bytesWritten, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, bytesConsumed); - Assert.True(bytesWritten > 0); - Assert.True(bytesWritten < s_sampleData.Length); // Compression should reduce size - } - - [Fact] - public void ZlibEncoder_Dispose_MultipleCallsSafe() - { - var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - encoder.Dispose(); - encoder.Dispose(); // Should not throw - } - - [Fact] - public void ZlibEncoder_Compress_AfterDispose_Throws() - { - var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - encoder.Dispose(); - - byte[] buffer = new byte[100]; - Assert.Throws(() => - encoder.Compress(s_sampleData, buffer, out _, out _, isFinalBlock: true)); - } - - [Fact] - public void ZlibEncoder_Compress_AfterFinished_ReturnsDone() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - // First compression with final block - encoder.Compress(s_sampleData, destination, out _, out _, isFinalBlock: true); - - // Second call after finished should return Done immediately - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(0, consumed); - Assert.Equal(0, written); - } - - [Fact] - public void ZlibEncoder_Reset_AllowsReuse() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - // First compression - encoder.Compress(s_sampleData, destination, out _, out int firstBytesWritten, isFinalBlock: true); - - // Reset - encoder.Reset(); - - // Second compression should work - Array.Clear(destination); - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int secondBytesWritten, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, consumed); - Assert.Equal(firstBytesWritten, secondBytesWritten); // Should produce same output - } - - [Fact] - public void ZlibEncoder_GetMaxCompressedLength_ValidValues() - { - Assert.True(ZlibEncoder.GetMaxCompressedLength(0) >= 0); - Assert.True(ZlibEncoder.GetMaxCompressedLength(100) >= 100); - Assert.True(ZlibEncoder.GetMaxCompressedLength(1000) >= 1000); - } - - [Fact] - public void ZlibEncoder_GetMaxCompressedLength_NegativeInput_Throws() - { - Assert.Throws(() => ZlibEncoder.GetMaxCompressedLength(-1)); - } - - [Fact] - public void ZlibEncoder_TryCompress_Success() - { - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - bool success = ZlibEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); - - Assert.True(success); - Assert.True(bytesWritten > 0); - } - - [Fact] - public void ZlibEncoder_TryCompress_DestinationTooSmall_ReturnsFalse() - { - byte[] destination = new byte[1]; // Too small - - bool success = ZlibEncoder.TryCompress(s_sampleData, destination, out int bytesWritten); - - Assert.False(success); - } - - [Theory] - [InlineData(CompressionLevel.Optimal)] // Default - maps to level 6 - [InlineData(CompressionLevel.NoCompression)] // No compression - [InlineData(CompressionLevel.Fastest)] // Best speed - maps to level 1 - [InlineData(CompressionLevel.SmallestSize)] // Best compression - maps to level 9 - public void ZlibEncoder_CompressionLevels(CompressionLevel level) - { - using var encoder = new ZlibEncoder(level, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, consumed); - Assert.True(written > 0); - } - - [Theory] - [InlineData(ZLibCompressionStrategy.Default)] - [InlineData(ZLibCompressionStrategy.Filtered)] - [InlineData(ZLibCompressionStrategy.HuffmanOnly)] - [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] - [InlineData(ZLibCompressionStrategy.Fixed)] - public void ZlibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate, strategy); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, consumed); - Assert.True(written > 0); - } - - [Fact] - public void ZlibEncoder_Flush() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - // Write some data without finalizing - encoder.Compress(s_sampleData.AsSpan(0, 50), destination, out _, out int written1, isFinalBlock: false); - - // Flush - may return Done, DestinationTooSmall, or NeedMoreData depending on internal state - OperationStatus status = encoder.Flush(destination.AsSpan(written1), out int flushedBytes); - - // Just verify it returns a valid status and doesn't throw - Assert.True( - status == OperationStatus.Done || - status == OperationStatus.DestinationTooSmall || - status == OperationStatus.NeedMoreData, - $"Unexpected status: {status}"); - } - - [Fact] - public void ZlibEncoder_DestinationTooSmall() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[5]; // Very small - - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.DestinationTooSmall, status); - Assert.True(consumed >= 0); - Assert.True(written >= 0); - } - - [Fact] - public void ZlibEncoder_EmptySource() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[100]; - - OperationStatus status = encoder.Compress(ReadOnlySpan.Empty, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(0, consumed); - Assert.True(written > 0); // Should still write end-of-stream marker - } - - [Fact] - public void ZlibEncoder_WithOptions() - { - var options = new ZlibEncoderOptions - { - CompressionLevel = 9, - Format = ZlibCompressionFormat.GZip, - CompressionStrategy = ZLibCompressionStrategy.Filtered - }; - - using var encoder = new ZlibEncoder(options); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - OperationStatus status = encoder.Compress(s_sampleData, destination, out int consumed, out int written, isFinalBlock: true); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, consumed); - Assert.True(written > 0); - } - - #endregion - - #region ZlibDecoder Tests - - [Fact] - public void ZlibDecoder_Ctor_InvalidFormat_Throws() - { - Assert.Throws(() => new ZlibDecoder((ZlibCompressionFormat)99)); - } - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void ZlibDecoder_Decompress_AllFormats(ZlibCompressionFormat format) - { - // First, compress the data - byte[] compressed = CompressData(s_sampleData, format); - - // Then decompress - using var decoder = new ZlibDecoder(format); - byte[] decompressed = new byte[s_sampleData.Length * 2]; // Extra room - - OperationStatus status = decoder.Decompress(compressed, decompressed, out int bytesConsumed, out int bytesWritten); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(compressed.Length, bytesConsumed); - Assert.Equal(s_sampleData.Length, bytesWritten); - Assert.Equal(s_sampleData, decompressed.AsSpan(0, bytesWritten).ToArray()); - } - - [Fact] - public void ZlibDecoder_Dispose_MultipleCallsSafe() - { - var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - decoder.Dispose(); - decoder.Dispose(); // Should not throw - } - - [Fact] - public void ZlibDecoder_Decompress_AfterDispose_Throws() - { - var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - decoder.Dispose(); - - byte[] buffer = new byte[100]; - Assert.Throws(() => - decoder.Decompress(s_sampleData, buffer, out _, out _)); - } - - [Fact] - public void ZlibDecoder_Decompress_AfterFinished_ReturnsDone() - { - byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[s_sampleData.Length * 2]; - - // First decompression - decoder.Decompress(compressed, decompressed, out _, out _); - - // Second call after finished should return Done immediately - OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int written); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(0, consumed); - Assert.Equal(0, written); - } - - [Fact] - public void ZlibDecoder_Reset_AllowsReuse() - { - byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[s_sampleData.Length * 2]; - - // First decompression - decoder.Decompress(compressed, decompressed, out _, out int firstBytesWritten); - - // Reset - decoder.Reset(); - - // Second decompression should work - Array.Clear(decompressed); - OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int secondBytesWritten); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(compressed.Length, consumed); - Assert.Equal(firstBytesWritten, secondBytesWritten); - Assert.Equal(s_sampleData, decompressed.AsSpan(0, secondBytesWritten).ToArray()); - } - - [Fact] - public void ZlibDecoder_TryDecompress_Success() - { - byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[s_sampleData.Length * 2]; - - bool success = ZlibDecoder.TryDecompress(compressed, decompressed, out int bytesWritten, ZlibCompressionFormat.Deflate); - - Assert.True(success); - Assert.Equal(s_sampleData.Length, bytesWritten); - Assert.Equal(s_sampleData, decompressed.AsSpan(0, bytesWritten).ToArray()); - } - - [Fact] - public void ZlibDecoder_TryDecompress_DestinationTooSmall_ReturnsFalse() - { - byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[1]; // Too small - - bool success = ZlibDecoder.TryDecompress(compressed, decompressed, out int bytesWritten, ZlibCompressionFormat.Deflate); - - Assert.False(success); - } - - [Fact] - public void ZlibDecoder_DestinationTooSmall() - { - byte[] compressed = CompressData(s_sampleData, ZlibCompressionFormat.Deflate); - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[5]; // Too small - - OperationStatus status = decoder.Decompress(compressed, decompressed, out int consumed, out int written); - - Assert.Equal(OperationStatus.DestinationTooSmall, status); - } - - [Fact] - public void ZlibDecoder_EmptySource() - { - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[100]; - - OperationStatus status = decoder.Decompress(ReadOnlySpan.Empty, decompressed, out int consumed, out int written); - - Assert.Equal(OperationStatus.NeedMoreData, status); - Assert.Equal(0, consumed); - Assert.Equal(0, written); - } - - [Fact] - public void ZlibDecoder_InvalidData() - { - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] garbage = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC, 0xFB }; - byte[] decompressed = new byte[100]; - - OperationStatus status = decoder.Decompress(garbage, decompressed, out _, out _); - - Assert.Equal(OperationStatus.InvalidData, status); - } - - #endregion - - #region Round-Trip Tests - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void RoundTrip_WithState(ZlibCompressionFormat format) - { - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - byte[] decompressed = new byte[s_sampleData.Length]; - - // Compress - using (var encoder = new ZlibEncoder(CompressionLevel.Optimal, format)) - { - OperationStatus compressStatus = encoder.Compress(s_sampleData, compressed, out _, out int written, isFinalBlock: true); - Assert.Equal(OperationStatus.Done, compressStatus); - compressed = compressed.AsSpan(0, written).ToArray(); - } - - // Decompress - using (var decoder = new ZlibDecoder(format)) - { - OperationStatus decompressStatus = decoder.Decompress(compressed, decompressed, out int consumed, out int written); - Assert.Equal(OperationStatus.Done, decompressStatus); - Assert.Equal(compressed.Length, consumed); - Assert.Equal(s_sampleData.Length, written); - } - - Assert.Equal(s_sampleData, decompressed); - } - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void RoundTrip_Static(ZlibCompressionFormat format) - { - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - byte[] decompressed = new byte[s_sampleData.Length]; - - bool compressSuccess = ZlibEncoder.TryCompress(s_sampleData, compressed, out int compressedSize, CompressionLevel.Optimal, format); - Assert.True(compressSuccess); - - compressed = compressed.AsSpan(0, compressedSize).ToArray(); - - bool decompressSuccess = ZlibDecoder.TryDecompress(compressed, decompressed, out int decompressedSize, format); - Assert.True(decompressSuccess); - Assert.Equal(s_sampleData.Length, decompressedSize); - - Assert.Equal(s_sampleData, decompressed); - } - - [Theory] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - [InlineData(100000)] - public void RoundTrip_VariousSizes(int size) - { - byte[] original = new byte[size]; - Random.Shared.NextBytes(original); - - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size)]; - byte[] decompressed = new byte[size]; - - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); - - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); - - Assert.Equal(size, decompressedSize); - Assert.Equal(original, decompressed); - } - - [Fact] - public void RoundTrip_Chunks() - { - int chunkSize = 100; - int totalSize = 2000; - byte[] original = new byte[totalSize]; - Random.Shared.NextBytes(original); - - byte[] allCompressed = new byte[ZlibEncoder.GetMaxCompressedLength(totalSize)]; - int totalCompressed = 0; - - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - - // Compress in chunks - for (int i = 0; i < totalSize; i += chunkSize) - { - int remaining = Math.Min(chunkSize, totalSize - i); - bool isFinal = (i + remaining) >= totalSize; - - OperationStatus status = encoder.Compress( - original.AsSpan(i, remaining), - allCompressed.AsSpan(totalCompressed), - out int consumed, - out int written, - isFinalBlock: isFinal); - - totalCompressed += written; - - if (!isFinal) - { - // Flush intermediate data - encoder.Flush(allCompressed.AsSpan(totalCompressed), out int flushed); - totalCompressed += flushed; - } - } - - // Decompress - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - byte[] decompressed = new byte[totalSize]; - - OperationStatus decompressStatus = decoder.Decompress( - allCompressed.AsSpan(0, totalCompressed), - decompressed, - out int bytesConsumed, - out int bytesWritten); - - Assert.Equal(OperationStatus.Done, decompressStatus); - Assert.Equal(totalSize, bytesWritten); - Assert.Equal(original, decompressed); - } - - #endregion - - #region Comparison with Stream-based APIs - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void Compare_EncoderOutput_MatchesStreamOutput(ZlibCompressionFormat format) - { - // Compress with span-based API - byte[] spanCompressed = CompressData(s_sampleData, format); - - // Compress with stream-based API - byte[] streamCompressed = CompressWithStream(s_sampleData, format); - - // Both should decompress to the same data - byte[] fromSpan = DecompressWithStream(spanCompressed, format); - byte[] fromStream = DecompressWithStream(streamCompressed, format); - - Assert.Equal(s_sampleData, fromSpan); - Assert.Equal(s_sampleData, fromStream); - } - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void Compare_StreamCompressed_CanDecompressWithDecoder(ZlibCompressionFormat format) - { - // Compress with stream - byte[] streamCompressed = CompressWithStream(s_sampleData, format); - - // Decompress with span-based decoder - using var decoder = new ZlibDecoder(format); - byte[] decompressed = new byte[s_sampleData.Length * 2]; - - OperationStatus status = decoder.Decompress(streamCompressed, decompressed, out int consumed, out int written); - - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(s_sampleData.Length, written); - Assert.Equal(s_sampleData, decompressed.AsSpan(0, written).ToArray()); - } - - [Theory] - [InlineData(ZlibCompressionFormat.Deflate)] - [InlineData(ZlibCompressionFormat.ZLib)] - [InlineData(ZlibCompressionFormat.GZip)] - public void Compare_EncoderCompressed_CanDecompressWithStream(ZlibCompressionFormat format) - { - // Compress with span-based encoder - byte[] spanCompressed = CompressData(s_sampleData, format); - - // Decompress with stream - byte[] decompressed = DecompressWithStream(spanCompressed, format); - - Assert.Equal(s_sampleData, decompressed); - } - - #endregion - - #region Edge Cases - - [Fact] - public void Compress_HighlyCompressibleData() - { - // All zeros - should compress very well - byte[] zeros = new byte[10000]; - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(zeros.Length)]; - - using var encoder = new ZlibEncoder(CompressionLevel.SmallestSize, ZlibCompressionFormat.Deflate); - encoder.Compress(zeros, compressed, out _, out int written, isFinalBlock: true); - - // Should compress to much smaller size - Assert.True(written < zeros.Length / 10, $"Expected significant compression, got {written} bytes from {zeros.Length} bytes"); - } - - [Fact] - public void Compress_IncompressibleData() - { - // Random data - won't compress well - byte[] random = new byte[1000]; - new Random(42).NextBytes(random); - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(random.Length)]; - - using var encoder = new ZlibEncoder(CompressionLevel.SmallestSize, ZlibCompressionFormat.Deflate); - encoder.Compress(random, compressed, out _, out int written, isFinalBlock: true); - - // Random data might even expand slightly - Assert.True(written > 0); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(100)] - public void RoundTrip_SmallData(int size) - { - byte[] original = new byte[size]; - if (size > 0) - { - Random.Shared.NextBytes(original); - } - - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(size) + 50]; - byte[] decompressed = new byte[Math.Max(size, 1)]; - - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - var compressStatus = encoder.Compress(original, compressed, out _, out int compressedSize, isFinalBlock: true); - Assert.Equal(OperationStatus.Done, compressStatus); - - if (size > 0) - { - using var decoder = new ZlibDecoder(ZlibCompressionFormat.Deflate); - var decompressStatus = decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out int decompressedSize); - Assert.Equal(OperationStatus.Done, decompressStatus); - Assert.Equal(size, decompressedSize); - Assert.Equal(original, decompressed.AsSpan(0, size).ToArray()); - } - } - - [Fact] - public void MultipleResets_Work() - { - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, ZlibCompressionFormat.Deflate); - byte[] destination = new byte[ZlibEncoder.GetMaxCompressedLength(s_sampleData.Length)]; - - for (int i = 0; i < 5; i++) - { - encoder.Compress(s_sampleData, destination, out _, out int written, isFinalBlock: true); - Assert.True(written > 0); - encoder.Reset(); - } - } - - #endregion - - #region Helper Methods - - private static byte[] CompressData(byte[] data, ZlibCompressionFormat format) - { - byte[] compressed = new byte[ZlibEncoder.GetMaxCompressedLength(data.Length)]; - using var encoder = new ZlibEncoder(CompressionLevel.Optimal, format); - encoder.Compress(data, compressed, out _, out int written, isFinalBlock: true); - return compressed.AsSpan(0, written).ToArray(); - } - - private static byte[] CompressWithStream(byte[] data, ZlibCompressionFormat format) - { - using var output = new MemoryStream(); - using (Stream compressor = CreateCompressionStream(output, format)) - { - compressor.Write(data, 0, data.Length); - } - return output.ToArray(); - } - - private static byte[] DecompressWithStream(byte[] data, ZlibCompressionFormat format) - { - using var input = new MemoryStream(data); - using Stream decompressor = CreateDecompressionStream(input, format); - using var output = new MemoryStream(); - decompressor.CopyTo(output); - return output.ToArray(); - } - - private static Stream CreateCompressionStream(Stream stream, ZlibCompressionFormat format) - { - return format switch - { - ZlibCompressionFormat.Deflate => new DeflateStream(stream, CompressionLevel.Optimal, leaveOpen: true), - ZlibCompressionFormat.ZLib => new ZLibStream(stream, CompressionLevel.Optimal, leaveOpen: true), - ZlibCompressionFormat.GZip => new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true), - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; - } - - private static Stream CreateDecompressionStream(Stream stream, ZlibCompressionFormat format) - { - return format switch - { - ZlibCompressionFormat.Deflate => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), - ZlibCompressionFormat.ZLib => new ZLibStream(stream, CompressionMode.Decompress, leaveOpen: true), - ZlibCompressionFormat.GZip => new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true), - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; - } - - #endregion - } -} From 4b87dcd7d36475cb8249095d6922a57f538a6af3 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 11:35:25 +0100 Subject: [PATCH 07/15] Remove ZlibEncoder and decoder with lower L --- .../src/System/IO/Compression/ZlibDecoder.cs | 55 -------- .../src/System/IO/Compression/ZlibEncoder.cs | 118 ------------------ 2 files changed, 173 deletions(-) delete mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs delete mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs deleted file mode 100644 index 5975cbf2377a6d..00000000000000 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; - -namespace System.IO.Compression -{ - /// - /// Provides methods and static methods to decode data compressed in the ZLib data format in a streamless, non-allocating, and performant manner. - /// - public sealed class ZLibDecoder : IDisposable - { - private readonly DeflateDecoder _deflateDecoder; - - /// - /// Initializes a new instance of the class. - /// - /// Failed to create the instance. - public ZLibDecoder() - { - _deflateDecoder = new DeflateDecoder(ZLibNative.ZLib_DefaultWindowBits); - } - - /// - /// Frees and disposes unmanaged resources. - /// - public void Dispose() => _deflateDecoder.Dispose(); - - /// - /// Decompresses a read-only byte span into a destination span. - /// - /// A read-only span of bytes containing the compressed source data. - /// When this method returns, a byte span where the decompressed data is stored. - /// When this method returns, the total number of bytes that were read from . - /// When this method returns, the total number of bytes that were written to . - /// One of the enumeration values that describes the status with which the span-based operation finished. - public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) - => _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); - - /// - /// Tries to decompress a source byte span into a destination span. - /// - /// A read-only span of bytes containing the compressed source data. - /// When this method returns, a span of bytes where the decompressed data is stored. - /// When this method returns, the total number of bytes that were written to . - /// if the decompression operation was successful; otherwise. - public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) - { - using var decoder = new ZLibDecoder(); - OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); - - return status == OperationStatus.Done && consumed == source.Length; - } - } -} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs deleted file mode 100644 index 59b30a96e555c4..00000000000000 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; - -namespace System.IO.Compression -{ - /// - /// Provides methods and static methods to encode data in a streamless, non-allocating, and performant manner using the ZLib data format specification. - /// - public sealed class ZLibEncoder : IDisposable - { - private readonly DeflateEncoder _deflateEncoder; - - /// - /// Initializes a new instance of the class using the default compression level. - /// - /// Failed to create the instance. - public ZLibEncoder() - : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) - { - } - - /// - /// Initializes a new instance of the class using the specified compression level. - /// - /// The compression level to use. - /// is not a valid value. - /// Failed to create the instance. - public ZLibEncoder(CompressionLevel compressionLevel) - : this(compressionLevel, ZLibCompressionStrategy.Default) - { - } - - /// - /// Initializes a new instance of the class using the specified compression level and strategy. - /// - /// The compression level to use. - /// The compression strategy to use. - /// is not a valid value. - /// Failed to create the instance. - public ZLibEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) - { - _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.ZLib_DefaultWindowBits); - } - - /// - /// Initializes a new instance of the class using the specified options. - /// - /// The compression options. - /// is null. - /// Failed to create the instance. - public ZLibEncoder(ZLibCompressionOptions options) - { - _deflateEncoder = new DeflateEncoder(options, ZLibNative.ZLib_DefaultWindowBits); - } - - /// - /// Frees and disposes unmanaged resources. - /// - public void Dispose() => _deflateEncoder.Dispose(); - - /// - /// Gets the maximum expected compressed length for the provided input size. - /// - /// The input size to get the maximum expected compressed length from. - /// A number representing the maximum compressed length for the provided input size. - /// is negative. - public static int GetMaxCompressedLength(int inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); - - /// - /// Compresses a read-only byte span into a destination span. - /// - /// A read-only span of bytes containing the source data to compress. - /// When this method returns, a byte span where the compressed data is stored. - /// When this method returns, the total number of bytes that were read from . - /// When this method returns, the total number of bytes that were written to . - /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. - /// One of the enumeration values that describes the status with which the span-based operation finished. - public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) - => _deflateEncoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); - - /// - /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. - /// - /// When this method returns, a span of bytes where the compressed data will be stored. - /// When this method returns, the total number of bytes that were written to . - /// One of the enumeration values that describes the status with which the operation finished. - public OperationStatus Flush(Span destination, out int bytesWritten) - => _deflateEncoder.Flush(destination, out bytesWritten); - - /// - /// Tries to compress a source byte span into a destination span using the default compression level. - /// - /// A read-only span of bytes containing the source data to compress. - /// When this method returns, a span of bytes where the compressed data is stored. - /// When this method returns, the total number of bytes that were written to . - /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) - => TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); - - /// - /// Tries to compress a source byte span into a destination span using the specified compression level. - /// - /// A read-only span of bytes containing the source data to compress. - /// When this method returns, a span of bytes where the compressed data is stored. - /// When this method returns, the total number of bytes that were written to . - /// The compression level to use. - /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) - { - using var encoder = new ZLibEncoder(compressionLevel); - OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); - - return status == OperationStatus.Done && consumed == source.Length; - } - } -} From aabc3a62bd4c909cb4696fd7d79a8065bc0e6c5c Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 11:36:46 +0100 Subject: [PATCH 08/15] Add ZLibEncoder and Decoder with capital L --- .../src/System/IO/Compression/ZLibDecoder.cs | 55 ++++++++ .../src/System/IO/Compression/ZLibEncoder.cs | 118 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibDecoder.cs create mode 100644 src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibDecoder.cs new file mode 100644 index 00000000000000..5975cbf2377a6d --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibDecoder.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to decode data compressed in the ZLib data format in a streamless, non-allocating, and performant manner. + /// + public sealed class ZLibDecoder : IDisposable + { + private readonly DeflateDecoder _deflateDecoder; + + /// + /// Initializes a new instance of the class. + /// + /// Failed to create the instance. + public ZLibDecoder() + { + _deflateDecoder = new DeflateDecoder(ZLibNative.ZLib_DefaultWindowBits); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() => _deflateDecoder.Dispose(); + + /// + /// Decompresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a byte span where the decompressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) + => _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + + /// + /// Tries to decompress a source byte span into a destination span. + /// + /// A read-only span of bytes containing the compressed source data. + /// When this method returns, a span of bytes where the decompressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the decompression operation was successful; otherwise. + public static bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) + { + using var decoder = new ZLibDecoder(); + OperationStatus status = decoder.Decompress(source, destination, out int consumed, out bytesWritten); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs new file mode 100644 index 00000000000000..914ba1a903d767 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.IO.Compression +{ + /// + /// Provides methods and static methods to encode data in a streamless, non-allocating, and performant manner using the ZLib data format specification. + /// + public sealed class ZLibEncoder : IDisposable + { + private readonly DeflateEncoder _deflateEncoder; + + /// + /// Initializes a new instance of the class using the default compression level. + /// + /// Failed to create the instance. + public ZLibEncoder() + : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level. + /// + /// The compression level to use. + /// is not a valid value. + /// Failed to create the instance. + public ZLibEncoder(CompressionLevel compressionLevel) + : this(compressionLevel, ZLibCompressionStrategy.Default) + { + } + + /// + /// Initializes a new instance of the class using the specified compression level and strategy. + /// + /// The compression level to use. + /// The compression strategy to use. + /// is not a valid value. + /// Failed to create the instance. + public ZLibEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) + { + _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.ZLib_DefaultWindowBits); + } + + /// + /// Initializes a new instance of the class using the specified options. + /// + /// The compression options. + /// is null. + /// Failed to create the instance. + public ZLibEncoder(ZLibCompressionOptions options) + { + _deflateEncoder = new DeflateEncoder(options, ZLibNative.ZLib_DefaultWindowBits); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() => _deflateEncoder.Dispose(); + + /// + /// Gets the maximum expected compressed length for the provided input size. + /// + /// The input size to get the maximum expected compressed length from. + /// A number representing the maximum compressed length for the provided input size. + /// is negative. + public static int GetMaxCompressedLength(int inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); + + /// + /// Compresses a read-only byte span into a destination span. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a byte span where the compressed data is stored. + /// When this method returns, the total number of bytes that were read from . + /// When this method returns, the total number of bytes that were written to . + /// to finalize the internal stream, which prevents adding more input data when this method returns; to allow the encoder to postpone the production of output until it has processed enough input. + /// One of the enumeration values that describes the status with which the span-based operation finished. + public OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) + => _deflateEncoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + /// + /// Compresses an empty read-only span of bytes into its destination, ensuring that output is produced for all the processed input. + /// + /// When this method returns, a span of bytes where the compressed data will be stored. + /// When this method returns, the total number of bytes that were written to . + /// One of the enumeration values that describes the status with which the operation finished. + public OperationStatus Flush(Span destination, out int bytesWritten) + => _deflateEncoder.Flush(destination, out bytesWritten); + + /// + /// Tries to compress a source byte span into a destination span using the default compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) + => TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); + + /// + /// Tries to compress a source byte span into a destination span using the specified compression level. + /// + /// A read-only span of bytes containing the source data to compress. + /// When this method returns, a span of bytes where the compressed data is stored. + /// When this method returns, the total number of bytes that were written to . + /// The compression level to use. + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + { + using var encoder = new ZLibEncoder(compressionLevel); + OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); + + return status == OperationStatus.Done && consumed == source.Length; + } + } +} \ No newline at end of file From cfb3d7e1e602d987dafb56a34e8fb3cc6a65331e Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 12:08:46 +0100 Subject: [PATCH 09/15] Change GetMaxCompressedLengt's inputLength to long --- .../ref/System.IO.Compression.cs | 6 +++--- .../src/System/IO/Compression/DeflateEncoder.cs | 11 ++--------- .../src/System/IO/Compression/GZipEncoder.cs | 4 ++-- .../src/System/IO/Compression/ZLibEncoder.cs | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 03580cf34ac249..24c108e076b599 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -34,7 +34,7 @@ public DeflateEncoder(System.IO.Compression.ZLibCompressionOptions options) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } - public static int GetMaxCompressedLength(int inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputSize) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } } @@ -90,7 +90,7 @@ public GZipEncoder(System.IO.Compression.ZLibCompressionOptions options) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } - public static int GetMaxCompressedLength(int inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputSize) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } } @@ -214,7 +214,7 @@ public ZLibEncoder(System.IO.Compression.ZLibCompressionOptions options) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } - public static int GetMaxCompressedLength(int inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputSize) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, System.IO.Compression.CompressionLevel compressionLevel) { throw null; } } diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs index 3f7140aae36a0e..183f02784c677a 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -149,20 +149,13 @@ private void EnsureInitialized() /// The input size to get the maximum expected compressed length from. /// A number representing the maximum compressed length for the provided input size. /// is negative. - public static int GetMaxCompressedLength(int inputSize) + public static long GetMaxCompressedLength(long inputSize) { ArgumentOutOfRangeException.ThrowIfNegative(inputSize); // ZLib's compressBound formula: inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 13 // We use a conservative estimate - long result = inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 18; - - if (result > int.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(inputSize)); - } - - return (int)result; + return inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 18; } /// diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs index 8ec69608851382..7169b980586576 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs @@ -66,10 +66,10 @@ public GZipEncoder(ZLibCompressionOptions options) /// The input size to get the maximum expected compressed length from. /// A number representing the maximum compressed length for the provided input size. /// is negative. - public static int GetMaxCompressedLength(int inputSize) + public static long GetMaxCompressedLength(long inputSize) { // GZip has a larger header than raw deflate, so add extra overhead - int baseLength = DeflateEncoder.GetMaxCompressedLength(inputSize); + long baseLength = DeflateEncoder.GetMaxCompressedLength(inputSize); // GZip adds ~18 bytes header/trailer overhead return baseLength + 10; diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs index 914ba1a903d767..60151ae5a8a136 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs @@ -66,7 +66,7 @@ public ZLibEncoder(ZLibCompressionOptions options) /// The input size to get the maximum expected compressed length from. /// A number representing the maximum compressed length for the provided input size. /// is negative. - public static int GetMaxCompressedLength(int inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); + public static long GetMaxCompressedLength(long inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); /// /// Compresses a read-only byte span into a destination span. From f031eedd41403287ee398add332e92e4f4a120a0 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Jan 2026 13:45:28 +0100 Subject: [PATCH 10/15] "File is required to end with a single newline character" --- .../src/System/IO/Compression/ZLibEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs index 60151ae5a8a136..96d7b197a2d256 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs @@ -115,4 +115,4 @@ public static bool TryCompress(ReadOnlySpan source, Span destination return status == OperationStatus.Done && consumed == source.Length; } } -} \ No newline at end of file +} From 1d18eec05d07fb021a2eded5b03962ba10ca6b18 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 20 Jan 2026 16:22:20 +0100 Subject: [PATCH 11/15] Add Interop layer for zlib's compressBound and use it inside GetMaxCompressedLength --- src/libraries/Common/src/Interop/Interop.zlib.cs | 3 +++ .../src/System/IO/Compression/DeflateEncoder.cs | 4 +--- .../System.IO.Compression.Native.def | 1 + .../System.IO.Compression.Native_unixexports.src | 1 + src/native/libs/System.IO.Compression.Native/pal_zlib.c | 5 +++++ 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/Interop/Interop.zlib.cs b/src/libraries/Common/src/Interop/Interop.zlib.cs index 610772cb7e7699..0ca40a2c712211 100644 --- a/src/libraries/Common/src/Interop/Interop.zlib.cs +++ b/src/libraries/Common/src/Interop/Interop.zlib.cs @@ -37,5 +37,8 @@ internal static unsafe partial ZLibNative.ErrorCode DeflateInit2_( [LibraryImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_Crc32")] internal static unsafe partial uint crc32(uint crc, byte* buffer, int len); + + [LibraryImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_CompressBound")] + internal static partial uint compressBound(uint sourceLen); } } diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs index 183f02784c677a..4c0b0e19eb179b 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -153,9 +153,7 @@ public static long GetMaxCompressedLength(long inputSize) { ArgumentOutOfRangeException.ThrowIfNegative(inputSize); - // ZLib's compressBound formula: inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 13 - // We use a conservative estimate - return inputSize + (inputSize >> 12) + (inputSize >> 14) + (inputSize >> 25) + 18; + return (long)Interop.ZLib.compressBound((uint)inputSize); } /// diff --git a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def index dda6568e500ff6..18ffc77b01f420 100644 --- a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def +++ b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def @@ -21,3 +21,4 @@ EXPORTS CompressionNative_InflateEnd CompressionNative_InflateInit2_ CompressionNative_InflateReset2_ + CompressionNative_CompressBound diff --git a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native_unixexports.src b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native_unixexports.src index 01602bc09765c0..a30d8ca11b0a27 100644 --- a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native_unixexports.src +++ b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native_unixexports.src @@ -21,3 +21,4 @@ CompressionNative_Inflate CompressionNative_InflateEnd CompressionNative_InflateInit2_ CompressionNative_InflateReset2_ +CompressionNative_CompressBound diff --git a/src/native/libs/System.IO.Compression.Native/pal_zlib.c b/src/native/libs/System.IO.Compression.Native/pal_zlib.c index d932eb522e30ee..38c5c728bdc000 100644 --- a/src/native/libs/System.IO.Compression.Native/pal_zlib.c +++ b/src/native/libs/System.IO.Compression.Native/pal_zlib.c @@ -208,3 +208,8 @@ uint32_t CompressionNative_Crc32(uint32_t crc, uint8_t* buffer, int32_t len) assert(result <= UINT32_MAX); return (uint32_t)result; } + +uint32_t CompressionNative_CompressBound(uint32_t sourceLen) +{ + return (uint32_t)compressBound(sourceLen); +} From 01fba5a4f2e37068242e69838e5ec3938b649163 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 23 Jan 2026 13:47:39 +0100 Subject: [PATCH 12/15] Fix gzip's header+trailer size --- .../src/System/IO/Compression/GZipEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs index 7169b980586576..322c110b04ae33 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs @@ -71,8 +71,8 @@ public static long GetMaxCompressedLength(long inputSize) // GZip has a larger header than raw deflate, so add extra overhead long baseLength = DeflateEncoder.GetMaxCompressedLength(inputSize); - // GZip adds ~18 bytes header/trailer overhead - return baseLength + 10; + // GZip adds 18 bytes: 10-byte header + 8-byte trailer (CRC32 + original size) + return baseLength + 18; } /// From 03fbcbc3fedfec57da58e06178c7fa6a648dcde7 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 23 Jan 2026 13:51:52 +0100 Subject: [PATCH 13/15] Add CompressionNative_CompressBound to pal_zlib.h --- src/native/libs/System.IO.Compression.Native/pal_zlib.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/native/libs/System.IO.Compression.Native/pal_zlib.h b/src/native/libs/System.IO.Compression.Native/pal_zlib.h index 222f100377981a..56dfdaba17de45 100644 --- a/src/native/libs/System.IO.Compression.Native/pal_zlib.h +++ b/src/native/libs/System.IO.Compression.Native/pal_zlib.h @@ -140,3 +140,12 @@ updated CRC-32. Returns the updated CRC-32. */ FUNCTIONEXPORT uint32_t FUNCTIONCALLINGCONVENTION CompressionNative_Crc32(uint32_t crc, uint8_t* buffer, int32_t len); + +/* +Calculates and returns an upper bound on the compressed size after deflate compressing sourceLen bytes. +This is a worst-case estimate that accounts for incompressible data and zlib wrapper overhead. +The actual compressed size will typically be smaller. + +Returns the maximum number of bytes the compressed output could require. +*/ +FUNCTIONEXPORT uint32_t FUNCTIONCALLINGCONVENTION CompressionNative_CompressBound(uint32_t sourceLen); From 665ae104fadb3c88dbb65d780f8847c5b360c9f8 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 23 Jan 2026 13:58:38 +0100 Subject: [PATCH 14/15] Remove empty line --- .../System.IO.Compression.Native.def | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def index e2a0c4a94f7754..736cbbcab8aebd 100644 --- a/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def +++ b/src/native/libs/System.IO.Compression.Native/System.IO.Compression.Native.def @@ -53,4 +53,4 @@ EXPORTS ZSTD_DCtx_refPrefix ZSTD_CCtx_refPrefix ZSTD_CCtx_setPledgedSrcSize - ZDICT_trainFromBuffer + ZDICT_trainFromBuffer \ No newline at end of file From ee0a437145a5c3ded1c6a0ffa3ec3c9825639549 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 26 Jan 2026 13:48:20 +0100 Subject: [PATCH 15/15] Add CompressionNative_CompressBound to entrypoints.c --- src/native/libs/System.IO.Compression.Native/entrypoints.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/native/libs/System.IO.Compression.Native/entrypoints.c b/src/native/libs/System.IO.Compression.Native/entrypoints.c index 533ba5aabdf9a2..84767e534df162 100644 --- a/src/native/libs/System.IO.Compression.Native/entrypoints.c +++ b/src/native/libs/System.IO.Compression.Native/entrypoints.c @@ -41,6 +41,7 @@ static const Entry s_compressionNative[] = DllImportEntry(CompressionNative_InflateEnd) DllImportEntry(CompressionNative_InflateInit2_) DllImportEntry(CompressionNative_InflateReset2_) + DllImportEntry(CompressionNative_CompressBound) #if !defined(TARGET_WASM) DllImportEntry(ZSTD_createCCtx) DllImportEntry(ZSTD_createDCtx)