From e402958743ee573cebaba5cbf1debc14fa2dcd7b Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 12 Jan 2026 20:46:21 +0100 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] "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/30] 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/30] 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/30] 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/30] 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/30] 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) From 238aa584d267e0985a1b3a665e80660efd02c85d Mon Sep 17 00:00:00 2001 From: iremyux Date: Wed, 4 Feb 2026 14:49:03 +0100 Subject: [PATCH 16/30] Use EncoderDecoderTestBase for tests --- .../tests/DeflateEncoderDecoderTests.cs | 179 +++++ .../DeflateZLibGZipEncoderDecoderTests.cs | 623 ------------------ .../tests/GZipEncoderDecoderTests.cs | 202 ++++++ .../tests/System.IO.Compression.Tests.csproj | 5 +- .../tests/ZlibEncoderDecoderTests.cs | 165 +++++ 5 files changed, 550 insertions(+), 624 deletions(-) create mode 100644 src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs delete mode 100644 src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs create mode 100644 src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs create mode 100644 src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs diff --git a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs new file mode 100644 index 00000000000000..08a1a881882d28 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -0,0 +1,179 @@ +// 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 : EncoderDecoderTestBase + { + protected override bool SupportsDictionaries => false; + protected override bool SupportsReset => false; + + protected override string WindowLogParamName => "windowLog"; + protected override string InputLengthParamName => "inputSize"; + + // Quality maps to zlib compression level (0-9) + protected override int ValidQuality => 6; + protected override int ValidWindowLog => 15; + + protected override int InvalidQualityTooLow => -2; + protected override int InvalidQualityTooHigh => 10; + protected override int InvalidWindowLogTooLow => 7; + protected override int InvalidWindowLogTooHigh => 16; + + public class DeflateEncoderAdapter : EncoderAdapter + { + private readonly DeflateEncoder _encoder; + + public DeflateEncoderAdapter(DeflateEncoder encoder) + { + _encoder = encoder; + } + + public override OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) => + _encoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + public override OperationStatus Flush(Span destination, out int bytesWritten) => + _encoder.Flush(destination, out bytesWritten); + + public override void Dispose() => _encoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + public class DeflateDecoderAdapter : DecoderAdapter + { + private readonly DeflateDecoder _decoder; + + public DeflateDecoderAdapter(DeflateDecoder decoder) + { + _decoder = decoder; + } + + public override OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) => + _decoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + + public override void Dispose() => _decoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + protected override EncoderAdapter CreateEncoder() => + new DeflateEncoderAdapter(new DeflateEncoder()); + + protected override EncoderAdapter CreateEncoder(int quality, int windowLog) => + new DeflateEncoderAdapter(new DeflateEncoder(quality, windowLog)); + + protected override EncoderAdapter CreateEncoder(DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override DecoderAdapter CreateDecoder() => + new DeflateDecoderAdapter(new DeflateDecoder()); + + protected override DecoderAdapter CreateDecoder(DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override DictionaryAdapter CreateDictionary(ReadOnlySpan dictionaryData, int quality) => + throw new NotSupportedException(); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + DeflateEncoder.TryCompress(source, destination, out bytesWritten); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) => + DeflateEncoder.TryCompress(source, destination, out bytesWritten, quality, windowLog); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + DeflateDecoder.TryDecompress(source, destination, out bytesWritten); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override long GetMaxCompressedLength(long inputSize) => + DeflateEncoder.GetMaxCompressedLength(inputSize); + + #region Deflate-specific Tests + + [Fact] + public void DeflateEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new DeflateEncoder(null!)); + } + + [Theory] + [InlineData(ZLibCompressionStrategy.Default)] + [InlineData(ZLibCompressionStrategy.Filtered)] + [InlineData(ZLibCompressionStrategy.HuffmanOnly)] + [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] + [InlineData(ZLibCompressionStrategy.Fixed)] + public void DeflateEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) + { + byte[] input = CreateTestData(); + using var encoder = new DeflateEncoder(CompressionLevel.Optimal, strategy); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void DeflateEncoder_WithOptions() + { + byte[] input = CreateTestData(); + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new DeflateEncoder(options); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [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); + } + + [Fact] + public void DeflateEncoder_Decoder_RoundTrip_AllCompressionLevels() + { + byte[] input = CreateTestData(); + + foreach (CompressionLevel level in Enum.GetValues()) + { + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = new DeflateEncoder(level); + encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[input.Length]; + using var decoder = new DeflateDecoder(); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); + + Assert.Equal(input, decompressed); + } + } + + #endregion + } +} diff --git a/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs deleted file mode 100644 index 5a5ca839baafa6..00000000000000 --- a/src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs +++ /dev/null @@ -1,623 +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.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/GZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs new file mode 100644 index 00000000000000..2f04c98779d76a --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs @@ -0,0 +1,202 @@ +// 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 Xunit; + +namespace System.IO.Compression +{ + public class GZipEncoderDecoderTests : EncoderDecoderTestBase + { + protected override bool SupportsDictionaries => false; + protected override bool SupportsReset => false; + + protected override string WindowLogParamName => "windowLog"; + protected override string InputLengthParamName => "inputSize"; + + // Quality maps to zlib compression level (0-9) + protected override int ValidQuality => 6; + protected override int ValidWindowLog => 15; + + protected override int InvalidQualityTooLow => -2; + protected override int InvalidQualityTooHigh => 10; + protected override int InvalidWindowLogTooLow => 7; + protected override int InvalidWindowLogTooHigh => 16; + + public class GZipEncoderAdapter : EncoderAdapter + { + private readonly GZipEncoder _encoder; + + public GZipEncoderAdapter(GZipEncoder encoder) + { + _encoder = encoder; + } + + public override OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) => + _encoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + public override OperationStatus Flush(Span destination, out int bytesWritten) => + _encoder.Flush(destination, out bytesWritten); + + public override void Dispose() => _encoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + public class GZipDecoderAdapter : DecoderAdapter + { + private readonly GZipDecoder _decoder; + + public GZipDecoderAdapter(GZipDecoder decoder) + { + _decoder = decoder; + } + + public override OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) => + _decoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + + public override void Dispose() => _decoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + protected override EncoderAdapter CreateEncoder() => + new GZipEncoderAdapter(new GZipEncoder()); + + protected override EncoderAdapter CreateEncoder(int quality, int windowLog) => + new GZipEncoderAdapter(new GZipEncoder(quality, windowLog)); + + protected override EncoderAdapter CreateEncoder(DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override DecoderAdapter CreateDecoder() => + new GZipDecoderAdapter(new GZipDecoder()); + + protected override DecoderAdapter CreateDecoder(DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override DictionaryAdapter CreateDictionary(ReadOnlySpan dictionaryData, int quality) => + throw new NotSupportedException(); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + GZipEncoder.TryCompress(source, destination, out bytesWritten); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) => + GZipEncoder.TryCompress(source, destination, out bytesWritten, quality, windowLog); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + GZipDecoder.TryDecompress(source, destination, out bytesWritten); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override long GetMaxCompressedLength(long inputSize) => + GZipEncoder.GetMaxCompressedLength(inputSize); + + #region GZip-specific Tests + + [Fact] + public void GZipEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new GZipEncoder(null!)); + } + + [Theory] + [InlineData(ZLibCompressionStrategy.Default)] + [InlineData(ZLibCompressionStrategy.Filtered)] + [InlineData(ZLibCompressionStrategy.HuffmanOnly)] + [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] + [InlineData(ZLibCompressionStrategy.Fixed)] + public void GZipEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) + { + byte[] input = CreateTestData(); + using var encoder = new GZipEncoder(CompressionLevel.Optimal, strategy); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void GZipEncoder_WithOptions() + { + byte[] input = CreateTestData(); + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new GZipEncoder(options); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void GZipEncoder_Decoder_RoundTrip_AllCompressionLevels() + { + byte[] input = CreateTestData(); + + foreach (CompressionLevel level in Enum.GetValues()) + { + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = new GZipEncoder(level); + encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[input.Length]; + using var decoder = new GZipDecoder(); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); + + Assert.Equal(input, decompressed); + } + } + + [Fact] + public void GZipEncoder_GZipStream_Interop() + { + byte[] input = CreateTestData(); + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = new GZipEncoder(CompressionLevel.Optimal); + encoder.Compress(input, 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(input, resultStream.ToArray()); + } + + [Fact] + public void GZipStream_GZipDecoder_Interop() + { + byte[] input = CreateTestData(); + using var ms = new MemoryStream(); + using (var gzipStream = new GZipStream(ms, CompressionLevel.Optimal, leaveOpen: true)) + { + gzipStream.Write(input); + } + + byte[] compressed = ms.ToArray(); + byte[] decompressed = new byte[input.Length]; + + using var decoder = new GZipDecoder(); + OperationStatus status = decoder.Decompress(compressed, decompressed, out _, out int written); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, written); + Assert.Equal(input, 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 437a931d3848a9..26828801690943 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,10 @@ - + + + + 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..39527f0ab0a7f0 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -0,0 +1,165 @@ +// 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 Xunit; + +namespace System.IO.Compression +{ + public class ZLibEncoderDecoderTests : EncoderDecoderTestBase + { + protected override bool SupportsDictionaries => false; + protected override bool SupportsReset => false; + + protected override string WindowLogParamName => "windowLog"; + protected override string InputLengthParamName => "inputSize"; + + // Quality maps to zlib compression level (0-9) + protected override int ValidQuality => 6; + protected override int ValidWindowLog => 15; + + protected override int InvalidQualityTooLow => -2; + protected override int InvalidQualityTooHigh => 10; + protected override int InvalidWindowLogTooLow => 7; + protected override int InvalidWindowLogTooHigh => 16; + + public class ZLibEncoderAdapter : EncoderAdapter + { + private readonly ZLibEncoder _encoder; + + public ZLibEncoderAdapter(ZLibEncoder encoder) + { + _encoder = encoder; + } + + public override OperationStatus Compress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) => + _encoder.Compress(source, destination, out bytesConsumed, out bytesWritten, isFinalBlock); + + public override OperationStatus Flush(Span destination, out int bytesWritten) => + _encoder.Flush(destination, out bytesWritten); + + public override void Dispose() => _encoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + public class ZLibDecoderAdapter : DecoderAdapter + { + private readonly ZLibDecoder _decoder; + + public ZLibDecoderAdapter(ZLibDecoder decoder) + { + _decoder = decoder; + } + + public override OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) => + _decoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + + public override void Dispose() => _decoder.Dispose(); + public override void Reset() => throw new NotSupportedException(); + } + + protected override EncoderAdapter CreateEncoder() => + new ZLibEncoderAdapter(new ZLibEncoder()); + + protected override EncoderAdapter CreateEncoder(int quality, int windowLog) => + new ZLibEncoderAdapter(new ZLibEncoder(quality, windowLog)); + + protected override EncoderAdapter CreateEncoder(DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override DecoderAdapter CreateDecoder() => + new ZLibDecoderAdapter(new ZLibDecoder()); + + protected override DecoderAdapter CreateDecoder(DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override DictionaryAdapter CreateDictionary(ReadOnlySpan dictionaryData, int quality) => + throw new NotSupportedException(); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + ZLibEncoder.TryCompress(source, destination, out bytesWritten); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) => + ZLibEncoder.TryCompress(source, destination, out bytesWritten, quality, windowLog); + + protected override bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary, int windowLog) => + throw new NotSupportedException(); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten) => + ZLibDecoder.TryDecompress(source, destination, out bytesWritten); + + protected override bool TryDecompress(ReadOnlySpan source, Span destination, out int bytesWritten, DictionaryAdapter dictionary) => + throw new NotSupportedException(); + + protected override long GetMaxCompressedLength(long inputSize) => + ZLibEncoder.GetMaxCompressedLength(inputSize); + + #region ZLib-specific Tests + + [Fact] + public void ZLibEncoder_Ctor_NullOptions_Throws() + { + Assert.Throws(() => new ZLibEncoder(null!)); + } + + [Theory] + [InlineData(ZLibCompressionStrategy.Default)] + [InlineData(ZLibCompressionStrategy.Filtered)] + [InlineData(ZLibCompressionStrategy.HuffmanOnly)] + [InlineData(ZLibCompressionStrategy.RunLengthEncoding)] + [InlineData(ZLibCompressionStrategy.Fixed)] + public void ZLibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) + { + byte[] input = CreateTestData(); + using var encoder = new ZLibEncoder(CompressionLevel.Optimal, strategy); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void ZLibEncoder_WithOptions() + { + byte[] input = CreateTestData(); + var options = new ZLibCompressionOptions + { + CompressionLevel = 9, + CompressionStrategy = ZLibCompressionStrategy.Filtered + }; + + using var encoder = new ZLibEncoder(options); + byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; + + OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(input.Length, consumed); + Assert.True(written > 0); + } + + [Fact] + public void ZLibEncoder_Decoder_RoundTrip_AllCompressionLevels() + { + byte[] input = CreateTestData(); + + foreach (CompressionLevel level in Enum.GetValues()) + { + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = new ZLibEncoder(level); + encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[input.Length]; + using var decoder = new ZLibDecoder(); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); + + Assert.Equal(input, decompressed); + } + } + + #endregion + } +} From aeebd784ec6d8d28e112a3d67ea1ba875dce0e92 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 9 Feb 2026 12:40:06 +0100 Subject: [PATCH 17/30] Expose quality and windowLog for encoders & remove compressionLevel enum --- .../ref/System.IO.Compression.cs | 25 +- .../System/IO/Compression/DeflateEncoder.cs | 217 +++++++++++++----- .../src/System/IO/Compression/GZipEncoder.cs | 66 +++--- .../src/System/IO/Compression/ZLibEncoder.cs | 64 ++++-- 4 files changed, 255 insertions(+), 117 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 24c108e076b599..c6e3056c809032 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -28,15 +28,16 @@ public void Dispose() { } 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(int quality) { } + public DeflateEncoder(int quality, int windowLog) { } 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 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; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality, int windowLog) { throw null; } } public partial class DeflateStream : System.IO.Stream { @@ -84,15 +85,16 @@ public void Dispose() { } 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(int quality) { } + public GZipEncoder(int quality, int windowLog) { } 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 long GetMaxCompressedLength(long inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputLength) { 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 static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality, int windowLog) { throw null; } } public partial class GZipStream : System.IO.Stream { @@ -208,15 +210,16 @@ public void Dispose() { } public sealed partial class ZLibEncoder : System.IDisposable { public ZLibEncoder() { } - public ZLibEncoder(System.IO.Compression.CompressionLevel compressionLevel) { } - public ZLibEncoder(System.IO.Compression.CompressionLevel compressionLevel, System.IO.Compression.ZLibCompressionStrategy strategy) { } + public ZLibEncoder(int quality) { } + public ZLibEncoder(int quality, int windowLog) { } 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 long GetMaxCompressedLength(long inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputLength) { 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 static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality) { throw null; } + public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality, int windowLog) { throw null; } } public sealed partial class ZLibStream : System.IO.Stream { 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 4c0b0e19eb179b..7c85b50878f1a1 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 @@ -21,34 +21,27 @@ public sealed class DeflateEncoder : IDisposable private readonly ZLibCompressionStrategy _strategy; /// - /// Initializes a new instance of the class using the default compression level. + /// The default quality value (optimal compression). /// - /// Failed to create the instance. - public DeflateEncoder() - : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) - { - } + internal const int DefaultQuality = 6; /// - /// Initializes a new instance of the class using the specified compression level. + /// Initializes a new instance of the class using the default quality. /// - /// The compression level to use. - /// is not a valid value. /// Failed to create the instance. - public DeflateEncoder(CompressionLevel compressionLevel) - : this(compressionLevel, ZLibCompressionStrategy.Default) + public DeflateEncoder() + : this(DefaultQuality) { } /// - /// Initializes a new instance of the class using the specified compression level and strategy. + /// Initializes a new instance of the class using the specified quality. /// - /// The compression level to use. - /// The compression strategy to use. - /// is not a valid value. + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// is not in the valid range (0-9). /// Failed to create the instance. - public DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) - : this(compressionLevel, strategy, ZLibNative.Deflate_DefaultWindowBits) + public DeflateEncoder(int quality) + : this(quality, DefaultWindowLog) { } @@ -59,21 +52,60 @@ public DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy /// is null. /// Failed to create the instance. public DeflateEncoder(ZLibCompressionOptions options) - : this(options, ZLibNative.Deflate_DefaultWindowBits) + : this(options, CompressionFormat.Deflate) + { + } + + /// + /// Initializes a new instance of the class using the specified quality and window size. + /// + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. + /// is not in the valid range (0-9), or is not in the valid range (8-15). + /// Failed to create the instance. + public DeflateEncoder(int quality, int windowLog) + : this(quality, windowLog, CompressionFormat.Deflate) + { + } + + /// + /// Specifies the compression format for zlib-based encoders. + /// + internal enum CompressionFormat { + /// Raw deflate format (no header/trailer). + Deflate, + /// ZLib format (zlib header/trailer). + ZLib, + /// GZip format (gzip header/trailer). + GZip } /// - /// Internal constructor to specify windowBits for different compression formats. + /// Internal constructor that accepts quality, windowLog (8-15), and format. + /// Validates both parameters and transforms windowLog to windowBits based on format. /// - internal DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy, int windowBits) + internal DeflateEncoder(int quality, int windowLog, CompressionFormat format) { - ValidateCompressionLevel(compressionLevel); + ValidateQuality(quality); + ValidateWindowLog(windowLog); _disposed = false; _finished = false; - _zlibCompressionLevel = GetZLibNativeCompressionLevel(compressionLevel); - _strategy = strategy; + _zlibCompressionLevel = (ZLibNative.CompressionLevel)quality; + _strategy = ZLibCompressionStrategy.Default; + + // Compute windowBits based on the compression format: + // - Deflate: negative windowLog produces raw deflate (no header/trailer) + // - ZLib: positive windowLog produces zlib format + // - GZip: windowLog + 16 produces gzip format + int windowBits = format switch + { + CompressionFormat.Deflate => -windowLog, + CompressionFormat.ZLib => windowLog, + CompressionFormat.GZip => windowLog + 16, + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( _zlibCompressionLevel, @@ -83,9 +115,9 @@ internal DeflateEncoder(CompressionLevel compressionLevel, ZLibCompressionStrate } /// - /// Internal constructor to specify windowBits with options. + /// Internal constructor that accepts ZLibCompressionOptions and format. /// - internal DeflateEncoder(ZLibCompressionOptions options, int windowBits) + internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format) { ArgumentNullException.ThrowIfNull(options); @@ -94,6 +126,15 @@ internal DeflateEncoder(ZLibCompressionOptions options, int windowBits) _zlibCompressionLevel = (ZLibNative.CompressionLevel)options.CompressionLevel; _strategy = options.CompressionStrategy; + // Compute windowBits based on the compression format using default window log: + int windowBits = format switch + { + CompressionFormat.Deflate => -DefaultWindowLog, + CompressionFormat.ZLib => DefaultWindowLog, + CompressionFormat.GZip => DefaultWindowLog + 16, + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( _zlibCompressionLevel, windowBits, @@ -101,23 +142,44 @@ internal DeflateEncoder(ZLibCompressionOptions options, int windowBits) (ZLibNative.CompressionStrategy)_strategy); } - private static void ValidateCompressionLevel(CompressionLevel compressionLevel) + + /// + /// The minimum quality value for compression (no compression). + /// + internal const int MinQuality = 0; + + /// + /// The maximum quality value for compression (best compression). + /// + internal const int MaxQuality = 9; + + /// + /// The minimum window log value (256 bytes window). + /// + internal const int MinWindowLog = 8; + + /// + /// The maximum window log value (32KB window). + /// + internal const int MaxWindowLog = 15; + + /// + /// The default window log value (32KB window). + /// + internal const int DefaultWindowLog = MaxWindowLog; + + + private static void ValidateQuality(int quality) { - if (compressionLevel < CompressionLevel.Optimal || compressionLevel > CompressionLevel.SmallestSize) - { - throw new ArgumentOutOfRangeException(nameof(compressionLevel)); - } + ArgumentOutOfRangeException.ThrowIfLessThan(quality, MinQuality, nameof(quality)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(quality, MaxQuality, nameof(quality)); } - 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 void ValidateWindowLog(int windowLog) + { + ArgumentOutOfRangeException.ThrowIfLessThan(windowLog, MinWindowLog, nameof(windowLog)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(windowLog, MaxWindowLog, nameof(windowLog)); + } /// /// Frees and disposes unmanaged resources. @@ -146,14 +208,14 @@ private void EnsureInitialized() /// /// Gets the maximum expected compressed length for the provided input size. /// - /// The input size to get the maximum expected compressed length from. + /// 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 long GetMaxCompressedLength(long inputSize) + /// is negative. + public static long GetMaxCompressedLength(long inputLength) { - ArgumentOutOfRangeException.ThrowIfNegative(inputSize); + ArgumentOutOfRangeException.ThrowIfNegative(inputLength); - return (long)Interop.ZLib.compressBound((uint)inputSize); + return (long)Interop.ZLib.compressBound((uint)inputLength); } /// @@ -197,7 +259,7 @@ public OperationStatus Compress(ReadOnlySpan source, Span destinatio OperationStatus status = errorCode switch { - ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 && _state.AvailOut > 0 + ZLibNative.ErrorCode.Ok => _state.AvailIn == 0 ? OperationStatus.Done : _state.AvailOut == 0 ? OperationStatus.DestinationTooSmall @@ -205,7 +267,9 @@ public OperationStatus Compress(ReadOnlySpan source, Span destinatio ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 ? OperationStatus.DestinationTooSmall - : OperationStatus.NeedMoreData, + : _state.AvailIn == 0 + ? OperationStatus.Done + : OperationStatus.NeedMoreData, ZLibNative.ErrorCode.DataError => OperationStatus.InvalidData, _ => OperationStatus.InvalidData }; @@ -229,34 +293,77 @@ public OperationStatus Compress(ReadOnlySpan source, Span destinatio /// 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); + EnsureInitialized(); + Debug.Assert(_state is not null); + + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + unsafe + { + fixed (byte* outputPtr = &MemoryMarshal.GetReference(destination)) + { + _state.NextIn = IntPtr.Zero; + _state.AvailIn = 0; + _state.NextOut = (IntPtr)outputPtr; + _state.AvailOut = (uint)destination.Length; + + ZLibNative.ErrorCode errorCode = _state.Deflate(ZLibNative.FlushCode.SyncFlush); + + bytesWritten = destination.Length - (int)_state.AvailOut; + + return errorCode switch + { + ZLibNative.ErrorCode.Ok => OperationStatus.Done, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.Done, + _ => OperationStatus.InvalidData + }; + } + } } /// - /// Tries to compress a source byte span into a destination span using the default compression level. + /// Tries to compress a source byte span into a destination span using the default quality. /// /// 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, DefaultQuality, DefaultWindowLog); + + /// + /// Tries to compress a source byte span into a destination span using the specified quality. + /// + /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) { - return TryCompress(source, destination, out bytesWritten, CompressionLevel.Optimal); + return TryCompress(source, destination, out bytesWritten, quality, DefaultWindowLog); } /// - /// Tries to compress a source byte span into a destination span using the specified compression level. + /// Tries to compress a source byte span into a destination span using the specified quality and window size. /// /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) { - ValidateCompressionLevel(compressionLevel); - - using var encoder = new DeflateEncoder(compressionLevel); + using var encoder = new DeflateEncoder(quality, windowLog); 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/GZipEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs index 322c110b04ae33..4450b1d6d57c90 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 @@ -13,46 +13,46 @@ public sealed class GZipEncoder : IDisposable private readonly DeflateEncoder _deflateEncoder; /// - /// Initializes a new instance of the class using the default compression level. + /// Initializes a new instance of the class using the default quality. /// /// Failed to create the instance. public GZipEncoder() - : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + : this(DeflateEncoder.DefaultQuality, DeflateEncoder.DefaultWindowLog) { } /// - /// Initializes a new instance of the class using the specified compression level. + /// Initializes a new instance of the class using the specified quality. /// - /// The compression level to use. - /// is not a valid value. + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// is not in the valid range (0-9). /// Failed to create the instance. - public GZipEncoder(CompressionLevel compressionLevel) - : this(compressionLevel, ZLibCompressionStrategy.Default) + public GZipEncoder(int quality) + : this(quality, DeflateEncoder.DefaultWindowLog) { } /// - /// Initializes a new instance of the class using the specified compression level and strategy. + /// Initializes a new instance of the class using the specified options. /// - /// The compression level to use. - /// The compression strategy to use. - /// is not a valid value. + /// The compression options. + /// is null. /// Failed to create the instance. - public GZipEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) + public GZipEncoder(ZLibCompressionOptions options) { - _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.GZip_DefaultWindowBits); + _deflateEncoder = new DeflateEncoder(options, DeflateEncoder.CompressionFormat.GZip); } /// - /// Initializes a new instance of the class using the specified options. + /// Initializes a new instance of the class using the specified quality and window size. /// - /// The compression options. - /// is null. + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. + /// is not in the valid range (0-9), or is not in the valid range (8-15). /// Failed to create the instance. - public GZipEncoder(ZLibCompressionOptions options) + public GZipEncoder(int quality, int windowLog) { - _deflateEncoder = new DeflateEncoder(options, ZLibNative.GZip_DefaultWindowBits); + _deflateEncoder = new DeflateEncoder(quality, windowLog, DeflateEncoder.CompressionFormat.GZip); } /// @@ -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 long GetMaxCompressedLength(long inputSize) + public static long GetMaxCompressedLength(long inputLength) { // GZip has a larger header than raw deflate, so add extra overhead - long baseLength = DeflateEncoder.GetMaxCompressedLength(inputSize); + long baseLength = DeflateEncoder.GetMaxCompressedLength(inputLength); // GZip adds 18 bytes: 10-byte header + 8-byte trailer (CRC32 + original size) return baseLength + 18; @@ -97,26 +97,40 @@ 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. + /// Tries to compress a source byte span into a destination span using the default quality. /// /// 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); + => TryCompress(source, destination, out bytesWritten, DeflateEncoder.DefaultQuality, DeflateEncoder.DefaultWindowLog); + + /// + /// Tries to compress a source byte span into a destination span using the specified quality. + /// + /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) + { + return TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); + } /// - /// Tries to compress a source byte span into a destination span using the specified compression level. + /// Tries to compress a source byte span into a destination span using the specified quality and window size. /// /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) { - using var encoder = new GZipEncoder(compressionLevel); + using var encoder = new GZipEncoder(quality, windowLog); 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/ZLibEncoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs index 96d7b197a2d256..94341d0186ade4 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 @@ -13,46 +13,46 @@ public sealed class ZLibEncoder : IDisposable private readonly DeflateEncoder _deflateEncoder; /// - /// Initializes a new instance of the class using the default compression level. + /// Initializes a new instance of the class using the default quality. /// /// Failed to create the instance. public ZLibEncoder() - : this(CompressionLevel.Optimal, ZLibCompressionStrategy.Default) + : this(DeflateEncoder.DefaultQuality) { } /// - /// Initializes a new instance of the class using the specified compression level. + /// Initializes a new instance of the class using the specified quality. /// - /// The compression level to use. - /// is not a valid value. + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// is not in the valid range (0-9). /// Failed to create the instance. - public ZLibEncoder(CompressionLevel compressionLevel) - : this(compressionLevel, ZLibCompressionStrategy.Default) + public ZLibEncoder(int quality) + : this(quality, DeflateEncoder.DefaultWindowLog) { } /// - /// Initializes a new instance of the class using the specified compression level and strategy. + /// Initializes a new instance of the class using the specified options. /// - /// The compression level to use. - /// The compression strategy to use. - /// is not a valid value. + /// The compression options. + /// is null. /// Failed to create the instance. - public ZLibEncoder(CompressionLevel compressionLevel, ZLibCompressionStrategy strategy) + public ZLibEncoder(ZLibCompressionOptions options) { - _deflateEncoder = new DeflateEncoder(compressionLevel, strategy, ZLibNative.ZLib_DefaultWindowBits); + _deflateEncoder = new DeflateEncoder(options, DeflateEncoder.CompressionFormat.ZLib); } /// - /// Initializes a new instance of the class using the specified options. + /// Initializes a new instance of the class using the specified quality and window size. /// - /// The compression options. - /// is null. + /// The compression quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. + /// is not in the valid range (0-9), or is not in the valid range (8-15). /// Failed to create the instance. - public ZLibEncoder(ZLibCompressionOptions options) + public ZLibEncoder(int quality, int windowLog) { - _deflateEncoder = new DeflateEncoder(options, ZLibNative.ZLib_DefaultWindowBits); + _deflateEncoder = new DeflateEncoder(quality, windowLog, DeflateEncoder.CompressionFormat.ZLib); } /// @@ -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 long GetMaxCompressedLength(long inputSize) => DeflateEncoder.GetMaxCompressedLength(inputSize); + public static long GetMaxCompressedLength(long inputLength) => DeflateEncoder.GetMaxCompressedLength(inputLength); /// /// Compresses a read-only byte span into a destination span. @@ -90,26 +90,40 @@ 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. + /// Tries to compress a source byte span into a destination span using the default quality. /// /// 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); + => TryCompress(source, destination, out bytesWritten, DeflateEncoder.DefaultQuality, DeflateEncoder.DefaultWindowLog); + + /// + /// Tries to compress a source byte span into a destination span using the specified quality. + /// + /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// if the compression operation was successful; otherwise. + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) + { + return TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); + } /// - /// Tries to compress a source byte span into a destination span using the specified compression level. + /// Tries to compress a source byte span into a destination span using the specified quality and window size. /// /// 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 quality value between 0 (no compression) and 9 (maximum compression). + /// The base-2 logarithm of the window size (8-15). Larger values result in better compression at the expense of memory usage. /// if the compression operation was successful; otherwise. - public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, CompressionLevel compressionLevel) + public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int windowLog) { - using var encoder = new ZLibEncoder(compressionLevel); + using var encoder = new ZLibEncoder(quality, windowLog); OperationStatus status = encoder.Compress(source, destination, out int consumed, out bytesWritten, isFinalBlock: true); return status == OperationStatus.Done && consumed == source.Length; From ab9ee4a74287ddd070a2b9f79134d902cab1d7ec Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 9 Feb 2026 15:31:40 +0100 Subject: [PATCH 18/30] Add WindowLog to ZLibCompressionOptions --- .../ref/System.IO.Compression.cs | 1 + .../System/IO/Compression/DeflateEncoder.cs | 10 +++++---- .../IO/Compression/ZLibCompressionOptions.cs | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 4 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 c6e3056c809032..cae8b7d4ae4fbe 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -191,6 +191,7 @@ public sealed partial class ZLibCompressionOptions public ZLibCompressionOptions() { } public int CompressionLevel { get { throw null; } set { } } public System.IO.Compression.ZLibCompressionStrategy CompressionStrategy { get { throw null; } set { } } + public int WindowLog { get { throw null; } set { } } } public enum ZLibCompressionStrategy { 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 7c85b50878f1a1..1169c99030dd38 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 @@ -126,12 +126,14 @@ internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format _zlibCompressionLevel = (ZLibNative.CompressionLevel)options.CompressionLevel; _strategy = options.CompressionStrategy; - // Compute windowBits based on the compression format using default window log: + int windowLog = options.WindowLog; + + // Compute windowBits based on the compression format: int windowBits = format switch { - CompressionFormat.Deflate => -DefaultWindowLog, - CompressionFormat.ZLib => DefaultWindowLog, - CompressionFormat.GZip => DefaultWindowLog + 16, + CompressionFormat.Deflate => -windowLog, + CompressionFormat.ZLib => windowLog, + CompressionFormat.GZip => windowLog + 16, _ => throw new ArgumentOutOfRangeException(nameof(format)) }; diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs index 387e52d713841e..98adc677f92db0 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs @@ -10,6 +10,7 @@ public sealed class ZLibCompressionOptions { private int _compressionLevel = -1; private ZLibCompressionStrategy _strategy; + private int _windowLog = DeflateEncoder.DefaultWindowLog; /// /// Gets or sets the compression level for a compression stream. @@ -45,6 +46,26 @@ public ZLibCompressionStrategy CompressionStrategy _strategy = value; } } + + /// + /// Gets or sets the base-2 logarithm of the window size for a compression stream. + /// + /// The value is less than 8 or greater than 15. + /// + /// Can accept any value between 8 and 15 (inclusive). Larger values result in better compression at the expense of memory usage. + /// The default value is 15 (32KB window). + /// + public int WindowLog + { + get => _windowLog; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, DeflateEncoder.MinWindowLog); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, DeflateEncoder.MaxWindowLog); + + _windowLog = value; + } + } } /// From cdb1cb45ecfef4e415c0f54f7d14494fd5e7292a Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 12 Feb 2026 14:56:16 +0100 Subject: [PATCH 19/30] Change the parameter name from inputSize to inputLength --- .../src/System/IO/Compression/GZipEncoder.cs | 4 ++-- .../src/System/IO/Compression/ZLibEncoder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 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 4450b1d6d57c90..8ecef150a8d04f 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 @@ -63,9 +63,9 @@ public GZipEncoder(int quality, int windowLog) /// /// Gets the maximum expected compressed length for the provided input size. /// - /// The input size to get the maximum expected compressed length from. + /// 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. + /// is negative. public static long GetMaxCompressedLength(long inputLength) { // GZip has a larger header than raw deflate, so add extra overhead 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 94341d0186ade4..d98ff6f6b8576e 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 @@ -63,9 +63,9 @@ public ZLibEncoder(int quality, int windowLog) /// /// Gets the maximum expected compressed length for the provided input size. /// - /// The input size to get the maximum expected compressed length from. + /// 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. + /// is negative. public static long GetMaxCompressedLength(long inputLength) => DeflateEncoder.GetMaxCompressedLength(inputLength); /// From 46c5a8fbd525e997925a81b2ca8c23bc17bb1ecf Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 12 Feb 2026 14:57:16 +0100 Subject: [PATCH 20/30] Add WindowLog to ZLibCompressionOptions --- .../IO/Compression/ZLibCompressionOptions.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs index 98adc677f92db0..ab50785312383f 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs @@ -10,7 +10,7 @@ public sealed class ZLibCompressionOptions { private int _compressionLevel = -1; private ZLibCompressionStrategy _strategy; - private int _windowLog = DeflateEncoder.DefaultWindowLog; + private int _windowLog = -1; /// /// Gets or sets the compression level for a compression stream. @@ -50,18 +50,21 @@ public ZLibCompressionStrategy CompressionStrategy /// /// Gets or sets the base-2 logarithm of the window size for a compression stream. /// - /// The value is less than 8 or greater than 15. + /// The value is less than -1 or greater than 15, or between 0 and 7. /// - /// Can accept any value between 8 and 15 (inclusive). Larger values result in better compression at the expense of memory usage. - /// The default value is 15 (32KB window). + /// Can accept -1 or any value between 8 and 15 (inclusive). Larger values result in better compression at the expense of memory usage. + /// -1 requests the default window log which is currently equivalent to 15 (32KB window). The default value is -1. /// public int WindowLog { get => _windowLog; set { - ArgumentOutOfRangeException.ThrowIfLessThan(value, DeflateEncoder.MinWindowLog); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, DeflateEncoder.MaxWindowLog); + if (value != -1) + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, DeflateEncoder.MinWindowLog); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, DeflateEncoder.MaxWindowLog); + } _windowLog = value; } From e35f0826e8551893d81371b7a4f0797148837b19 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 13 Feb 2026 12:50:42 +0100 Subject: [PATCH 21/30] VAlidate inputLength --- .../src/System/IO/Compression/DeflateEncoder.cs | 6 ++++-- .../src/System/IO/Compression/GZipEncoder.cs | 2 +- .../src/System/IO/Compression/ZLibEncoder.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) 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 1169c99030dd38..9056b850865c44 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 @@ -126,7 +126,8 @@ internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format _zlibCompressionLevel = (ZLibNative.CompressionLevel)options.CompressionLevel; _strategy = options.CompressionStrategy; - int windowLog = options.WindowLog; + // -1 means use the default window log + int windowLog = options.WindowLog == -1 ? DefaultWindowLog : options.WindowLog; // Compute windowBits based on the compression format: int windowBits = format switch @@ -212,10 +213,11 @@ 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. + /// is negative or exceeds . public static long GetMaxCompressedLength(long inputLength) { ArgumentOutOfRangeException.ThrowIfNegative(inputLength); + ArgumentOutOfRangeException.ThrowIfGreaterThan(inputLength, uint.MaxValue); return (long)Interop.ZLib.compressBound((uint)inputLength); } 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 8ecef150a8d04f..c2d6c315360779 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 @@ -65,7 +65,7 @@ public GZipEncoder(int quality, int windowLog) /// /// 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. + /// is negative or exceeds . public static long GetMaxCompressedLength(long inputLength) { // GZip has a larger header than raw deflate, so add extra overhead 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 d98ff6f6b8576e..6d832a358839cc 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 @@ -65,7 +65,7 @@ public ZLibEncoder(int quality, int windowLog) /// /// 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. + /// is negative or exceeds . public static long GetMaxCompressedLength(long inputLength) => DeflateEncoder.GetMaxCompressedLength(inputLength); /// From fb4b1c8c0334a8b939f4c98f43557f6f3f5da98e Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 13 Feb 2026 12:56:00 +0100 Subject: [PATCH 22/30] Remove EnsureInitialized --- .../src/Resources/Strings.resx | 19 +------------------ .../System/IO/Compression/DeflateDecoder.cs | 11 +---------- .../System/IO/Compression/DeflateEncoder.cs | 13 ++----------- 3 files changed, 4 insertions(+), 39 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index 1c07e306dacb0c..4df30b11d87c9c 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -170,24 +170,7 @@ The underlying compression routine returned an unexpected error code: '{0}'. - - The DeflateEncoder has not been initialized. Use the constructor to initialize. - - - The DeflateDecoder has not been initialized. Use the constructor to initialize. - - - The ZLibEncoder has not been initialized. Use the constructor to initialize. - - - The ZLibDecoder has not been initialized. Use the constructor to initialize. - - - 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/DeflateDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs index 85ee39d2674d75..cba935dd0078de 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs @@ -47,15 +47,6 @@ 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. /// @@ -66,7 +57,7 @@ private void EnsureInitialized() /// 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(); + EnsureNotDisposed(); Debug.Assert(_state is not null); bytesConsumed = 0; 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 9056b850865c44..6a7fc446ff3a51 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 @@ -199,15 +199,6 @@ 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. /// @@ -233,7 +224,7 @@ public static long GetMaxCompressedLength(long inputLength) /// 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(); + EnsureNotDisposed(); Debug.Assert(_state is not null); bytesConsumed = 0; @@ -297,7 +288,7 @@ public OperationStatus Compress(ReadOnlySpan source, Span destinatio /// One of the enumeration values that describes the status with which the operation finished. public OperationStatus Flush(Span destination, out int bytesWritten) { - EnsureInitialized(); + EnsureNotDisposed(); Debug.Assert(_state is not null); bytesWritten = 0; From f82ddc46d6707d626ac87bf2b8c2ee36e1db689e Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 13 Feb 2026 17:53:58 +0100 Subject: [PATCH 23/30] Fix tests --- .../tests/DeflateEncoderDecoderTests.cs | 11 ++++++++--- .../tests/GZipEncoderDecoderTests.cs | 13 +++++++++---- .../tests/ZlibEncoderDecoderTests.cs | 11 ++++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs index 08a1a881882d28..7cb44acfc9fe6e 100644 --- a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -112,7 +112,12 @@ public void DeflateEncoder_Ctor_NullOptions_Throws() public void DeflateEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) { byte[] input = CreateTestData(); - using var encoder = new DeflateEncoder(CompressionLevel.Optimal, strategy); + var options = new ZLibCompressionOptions + { + CompressionLevel = 6, + CompressionStrategy = strategy + }; + using var encoder = new DeflateEncoder(options); byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); @@ -160,10 +165,10 @@ public void DeflateEncoder_Decoder_RoundTrip_AllCompressionLevels() { byte[] input = CreateTestData(); - foreach (CompressionLevel level in Enum.GetValues()) + for (int quality = 0; quality <= 9; quality++) { byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new DeflateEncoder(level); + using var encoder = new DeflateEncoder(quality); encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); byte[] decompressed = new byte[input.Length]; diff --git a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs index 2f04c98779d76a..10b86ad54383d3 100644 --- a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs @@ -111,7 +111,12 @@ public void GZipEncoder_Ctor_NullOptions_Throws() public void GZipEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) { byte[] input = CreateTestData(); - using var encoder = new GZipEncoder(CompressionLevel.Optimal, strategy); + var options = new ZLibCompressionOptions + { + CompressionLevel = 6, + CompressionStrategy = strategy + }; + using var encoder = new GZipEncoder(options); byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); @@ -146,10 +151,10 @@ public void GZipEncoder_Decoder_RoundTrip_AllCompressionLevels() { byte[] input = CreateTestData(); - foreach (CompressionLevel level in Enum.GetValues()) + for (int quality = 0; quality <= 9; quality++) { byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new GZipEncoder(level); + using var encoder = new GZipEncoder(quality); encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); byte[] decompressed = new byte[input.Length]; @@ -165,7 +170,7 @@ public void GZipEncoder_GZipStream_Interop() { byte[] input = CreateTestData(); byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new GZipEncoder(CompressionLevel.Optimal); + using var encoder = new GZipEncoder(6); encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); using var ms = new MemoryStream(compressed, 0, compressedSize); diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs index 39527f0ab0a7f0..6087ae735f4846 100644 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -111,7 +111,12 @@ public void ZLibEncoder_Ctor_NullOptions_Throws() public void ZLibEncoder_CompressionStrategies(ZLibCompressionStrategy strategy) { byte[] input = CreateTestData(); - using var encoder = new ZLibEncoder(CompressionLevel.Optimal, strategy); + var options = new ZLibCompressionOptions + { + CompressionLevel = 6, + CompressionStrategy = strategy + }; + using var encoder = new ZLibEncoder(options); byte[] destination = new byte[GetMaxCompressedLength(input.Length)]; OperationStatus status = encoder.Compress(input, destination, out int consumed, out int written, isFinalBlock: true); @@ -146,10 +151,10 @@ public void ZLibEncoder_Decoder_RoundTrip_AllCompressionLevels() { byte[] input = CreateTestData(); - foreach (CompressionLevel level in Enum.GetValues()) + for (int quality = 0; quality <= 9; quality++) { byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new ZLibEncoder(level); + using var encoder = new ZLibEncoder(quality); encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); byte[] decompressed = new byte[input.Length]; From 89ba2150893c88352c92973e706a0087a6fb0639 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 13 Feb 2026 18:08:44 +0100 Subject: [PATCH 24/30] Fix tests --- .../System.IO.Compression/tests/DeflateEncoderDecoderTests.cs | 2 +- .../System.IO.Compression/tests/GZipEncoderDecoderTests.cs | 2 +- .../System.IO.Compression/tests/ZlibEncoderDecoderTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs index 7cb44acfc9fe6e..8fad3484bad882 100644 --- a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -13,7 +13,7 @@ public class DeflateEncoderDecoderTests : EncoderDecoderTestBase protected override bool SupportsReset => false; protected override string WindowLogParamName => "windowLog"; - protected override string InputLengthParamName => "inputSize"; + protected override string InputLengthParamName => "inputLength"; // Quality maps to zlib compression level (0-9) protected override int ValidQuality => 6; diff --git a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs index 10b86ad54383d3..e4478103ed8fc1 100644 --- a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs @@ -12,7 +12,7 @@ public class GZipEncoderDecoderTests : EncoderDecoderTestBase protected override bool SupportsReset => false; protected override string WindowLogParamName => "windowLog"; - protected override string InputLengthParamName => "inputSize"; + protected override string InputLengthParamName => "inputLength"; // Quality maps to zlib compression level (0-9) protected override int ValidQuality => 6; diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs index 6087ae735f4846..dc832ecf94e518 100644 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -12,7 +12,7 @@ public class ZLibEncoderDecoderTests : EncoderDecoderTestBase protected override bool SupportsReset => false; protected override string WindowLogParamName => "windowLog"; - protected override string InputLengthParamName => "inputSize"; + protected override string InputLengthParamName => "inputLength"; // Quality maps to zlib compression level (0-9) protected override int ValidQuality => 6; From 6f05f5366c8a66dc637edaa2b3f82a35b674d629 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 13 Feb 2026 18:09:00 +0100 Subject: [PATCH 25/30] Change the parameter name from inputSize to inputLength --- .../System.IO.Compression/ref/System.IO.Compression.cs | 2 +- .../src/System/IO/Compression/DeflateDecoder.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 cae8b7d4ae4fbe..36c0af0fd5f90d 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 long GetMaxCompressedLength(long inputSize) { throw null; } + public static long GetMaxCompressedLength(long inputLength) { 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, int quality) { throw null; } public static bool TryCompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten, int quality, int windowLog) { throw null; } diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs index cba935dd0078de..e1fe4e0d0aaa4c 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs @@ -68,6 +68,11 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat return OperationStatus.Done; } + if (destination.IsEmpty && source.Length > 0) + { + return OperationStatus.DestinationTooSmall; + } + unsafe { fixed (byte* inputPtr = &MemoryMarshal.GetReference(source)) From c6384e42ddbc1eddedfd6657df6611f5a4a1e967 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 16 Feb 2026 11:55:11 +0100 Subject: [PATCH 26/30] Remove unnecessary parameters --- .../src/System/IO/Compression/DeflateEncoder.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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 6a7fc446ff3a51..834ce08f229a33 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 @@ -16,10 +16,6 @@ public sealed class DeflateEncoder : IDisposable private bool _disposed; private bool _finished; - // Store construction parameters for encoder initialization - private readonly ZLibNative.CompressionLevel _zlibCompressionLevel; - private readonly ZLibCompressionStrategy _strategy; - /// /// The default quality value (optimal compression). /// @@ -92,8 +88,6 @@ internal DeflateEncoder(int quality, int windowLog, CompressionFormat format) _disposed = false; _finished = false; - _zlibCompressionLevel = (ZLibNative.CompressionLevel)quality; - _strategy = ZLibCompressionStrategy.Default; // Compute windowBits based on the compression format: // - Deflate: negative windowLog produces raw deflate (no header/trailer) @@ -108,10 +102,10 @@ internal DeflateEncoder(int quality, int windowLog, CompressionFormat format) }; _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - _zlibCompressionLevel, + (ZLibNative.CompressionLevel)quality, windowBits, ZLibNative.Deflate_DefaultMemLevel, - (ZLibNative.CompressionStrategy)_strategy); + ZLibNative.CompressionStrategy.DefaultStrategy); } /// @@ -123,8 +117,6 @@ internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format _disposed = false; _finished = false; - _zlibCompressionLevel = (ZLibNative.CompressionLevel)options.CompressionLevel; - _strategy = options.CompressionStrategy; // -1 means use the default window log int windowLog = options.WindowLog == -1 ? DefaultWindowLog : options.WindowLog; @@ -139,10 +131,10 @@ internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format }; _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( - _zlibCompressionLevel, + (ZLibNative.CompressionLevel)options.CompressionLevel, windowBits, ZLibNative.Deflate_DefaultMemLevel, - (ZLibNative.CompressionStrategy)_strategy); + (ZLibNative.CompressionStrategy)options.CompressionStrategy); } From f3dd1c4a3eb2d3f18f75be2fbc0c9f2c9f8f5841 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 16 Feb 2026 11:59:10 +0100 Subject: [PATCH 27/30] Move test to TestBase --- .../IO/Compression/EncoderDecoderTestBase.cs | 19 +++++++++++++++++++ .../tests/DeflateEncoderDecoderTests.cs | 19 ------------------- .../tests/GZipEncoderDecoderTests.cs | 19 ------------------- .../tests/ZlibEncoderDecoderTests.cs | 19 ------------------- 4 files changed, 19 insertions(+), 57 deletions(-) diff --git a/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs b/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs index d06bfb38177fff..e0ee13f91dd8f8 100644 --- a/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs +++ b/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs @@ -664,6 +664,25 @@ public void RoundTrip_Chunks() } } + [Fact] + public void RoundTrip_AllCompressionLevels() + { + byte[] input = CreateTestData(); + + for (int quality = 0; quality <= 9; quality++) + { + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = CreateEncoder(quality, ValidWindowLog); + encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); + + byte[] decompressed = new byte[input.Length]; + using var decoder = CreateDecoder(); + decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); + + Assert.Equal(input, decompressed); + } + } + public static byte[] CreateTestData(int size = 1000) { // Create test data of specified size diff --git a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs index 8fad3484bad882..b38f4de47a4fe9 100644 --- a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -160,25 +160,6 @@ public void DeflateDecoder_InvalidData_ReturnsInvalidData() Assert.Equal(OperationStatus.InvalidData, status); } - [Fact] - public void DeflateEncoder_Decoder_RoundTrip_AllCompressionLevels() - { - byte[] input = CreateTestData(); - - for (int quality = 0; quality <= 9; quality++) - { - byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new DeflateEncoder(quality); - encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); - - byte[] decompressed = new byte[input.Length]; - using var decoder = new DeflateDecoder(); - decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); - - Assert.Equal(input, decompressed); - } - } - #endregion } } diff --git a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs index e4478103ed8fc1..8932ab0df600bd 100644 --- a/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs @@ -146,25 +146,6 @@ public void GZipEncoder_WithOptions() Assert.True(written > 0); } - [Fact] - public void GZipEncoder_Decoder_RoundTrip_AllCompressionLevels() - { - byte[] input = CreateTestData(); - - for (int quality = 0; quality <= 9; quality++) - { - byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new GZipEncoder(quality); - encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); - - byte[] decompressed = new byte[input.Length]; - using var decoder = new GZipDecoder(); - decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); - - Assert.Equal(input, decompressed); - } - } - [Fact] public void GZipEncoder_GZipStream_Interop() { diff --git a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs index dc832ecf94e518..f75a33eda3ed82 100644 --- a/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -146,25 +146,6 @@ public void ZLibEncoder_WithOptions() Assert.True(written > 0); } - [Fact] - public void ZLibEncoder_Decoder_RoundTrip_AllCompressionLevels() - { - byte[] input = CreateTestData(); - - for (int quality = 0; quality <= 9; quality++) - { - byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; - using var encoder = new ZLibEncoder(quality); - encoder.Compress(input, compressed, out _, out int compressedSize, isFinalBlock: true); - - byte[] decompressed = new byte[input.Length]; - using var decoder = new ZLibDecoder(); - decoder.Decompress(compressed.AsSpan(0, compressedSize), decompressed, out _, out _); - - Assert.Equal(input, decompressed); - } - } - #endregion } } From 91b2890738759f96dfbf90ff15910ddb0991581f Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 16 Feb 2026 11:59:40 +0100 Subject: [PATCH 28/30] Remove unused library --- .../System.IO.Compression/tests/DeflateEncoderDecoderTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs index b38f4de47a4fe9..721a95bde83693 100644 --- a/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Text; using Xunit; namespace System.IO.Compression From e52c80e81d84d17fd3a3ca33b6f917ad9229e8e3 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 16 Feb 2026 18:02:06 +0100 Subject: [PATCH 29/30] Remove function body --- .../src/System/IO/Compression/DeflateEncoder.cs | 4 +--- .../src/System/IO/Compression/GZipEncoder.cs | 4 +--- .../src/System/IO/Compression/ZLibEncoder.cs | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) 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 834ce08f229a33..966b8d1b450565 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 @@ -335,9 +335,7 @@ public static bool TryCompress(ReadOnlySpan source, Span destination /// The compression quality value between 0 (no compression) and 9 (maximum compression). /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) - { - return TryCompress(source, destination, out bytesWritten, quality, DefaultWindowLog); - } + => TryCompress(source, destination, out bytesWritten, quality, DefaultWindowLog); /// /// Tries to compress a source byte span into a destination span using the specified quality and window size. 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 c2d6c315360779..fdc553def8349c 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 @@ -115,9 +115,7 @@ public static bool TryCompress(ReadOnlySpan source, Span destination /// The compression quality value between 0 (no compression) and 9 (maximum compression). /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) - { - return TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); - } + => TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); /// /// Tries to compress a source byte span into a destination span using the specified quality and window size. 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 6d832a358839cc..82f817d4f4b0c9 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 @@ -108,9 +108,7 @@ public static bool TryCompress(ReadOnlySpan source, Span destination /// The compression quality value between 0 (no compression) and 9 (maximum compression). /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality) - { - return TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); - } + => TryCompress(source, destination, out bytesWritten, quality, DeflateEncoder.DefaultWindowLog); /// /// Tries to compress a source byte span into a destination span using the specified quality and window size. From 802d228dcbb123e45507e63d2c344afc878a59e5 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 16 Feb 2026 18:08:42 +0100 Subject: [PATCH 30/30] Add EnsureNotDisposed method to Gzip & Zlib too --- .../src/System/IO/Compression/GZipDecoder.cs | 17 ++++++++++++-- .../src/System/IO/Compression/GZipEncoder.cs | 22 ++++++++++++++++--- .../src/System/IO/Compression/ZLibDecoder.cs | 17 ++++++++++++-- .../src/System/IO/Compression/ZLibEncoder.cs | 22 ++++++++++++++++--- 4 files changed, 68 insertions(+), 10 deletions(-) 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 index 685946c1e00eac..74006573b8b82b 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs @@ -11,6 +11,7 @@ namespace System.IO.Compression public sealed class GZipDecoder : IDisposable { private readonly DeflateDecoder _deflateDecoder; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -24,7 +25,16 @@ public GZipDecoder() /// /// Frees and disposes unmanaged resources. /// - public void Dispose() => _deflateDecoder.Dispose(); + public void Dispose() + { + _disposed = true; + _deflateDecoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } /// /// Decompresses a read-only byte span into a destination span. @@ -35,7 +45,10 @@ public GZipDecoder() /// 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); + { + EnsureNotDisposed(); + return _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + } /// /// Tries to decompress a source byte span into a destination span. 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 fdc553def8349c..0a8e5b767e71d6 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 @@ -11,6 +11,7 @@ namespace System.IO.Compression public sealed class GZipEncoder : IDisposable { private readonly DeflateEncoder _deflateEncoder; + private bool _disposed; /// /// Initializes a new instance of the class using the default quality. @@ -58,7 +59,16 @@ public GZipEncoder(int quality, int windowLog) /// /// Frees and disposes unmanaged resources. /// - public void Dispose() => _deflateEncoder.Dispose(); + public void Dispose() + { + _disposed = true; + _deflateEncoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } /// /// Gets the maximum expected compressed length for the provided input size. @@ -85,7 +95,10 @@ public static long GetMaxCompressedLength(long inputLength) /// 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); + { + EnsureNotDisposed(); + return _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. @@ -94,7 +107,10 @@ 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) - => _deflateEncoder.Flush(destination, out bytesWritten); + { + EnsureNotDisposed(); + return _deflateEncoder.Flush(destination, out bytesWritten); + } /// /// Tries to compress a source byte span into a destination span using the default quality. 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 5975cbf2377a6d..ccc943ebaf8905 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 @@ -11,6 +11,7 @@ namespace System.IO.Compression public sealed class ZLibDecoder : IDisposable { private readonly DeflateDecoder _deflateDecoder; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -24,7 +25,16 @@ public ZLibDecoder() /// /// Frees and disposes unmanaged resources. /// - public void Dispose() => _deflateDecoder.Dispose(); + public void Dispose() + { + _disposed = true; + _deflateDecoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } /// /// Decompresses a read-only byte span into a destination span. @@ -35,7 +45,10 @@ public ZLibDecoder() /// 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); + { + EnsureNotDisposed(); + return _deflateDecoder.Decompress(source, destination, out bytesConsumed, out bytesWritten); + } /// /// Tries to decompress a source byte span into a destination span. 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 82f817d4f4b0c9..cb84da2652b621 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 @@ -11,6 +11,7 @@ namespace System.IO.Compression public sealed class ZLibEncoder : IDisposable { private readonly DeflateEncoder _deflateEncoder; + private bool _disposed; /// /// Initializes a new instance of the class using the default quality. @@ -58,7 +59,16 @@ public ZLibEncoder(int quality, int windowLog) /// /// Frees and disposes unmanaged resources. /// - public void Dispose() => _deflateEncoder.Dispose(); + public void Dispose() + { + _disposed = true; + _deflateEncoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } /// /// Gets the maximum expected compressed length for the provided input size. @@ -78,7 +88,10 @@ public ZLibEncoder(int quality, int windowLog) /// 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); + { + EnsureNotDisposed(); + return _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. @@ -87,7 +100,10 @@ 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) - => _deflateEncoder.Flush(destination, out bytesWritten); + { + EnsureNotDisposed(); + return _deflateEncoder.Flush(destination, out bytesWritten); + } /// /// Tries to compress a source byte span into a destination span using the default quality.