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/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/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 412fa9f2a25535..36c0af0fd5f90d 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,27 @@ 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(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 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; } + } public partial class DeflateStream : System.IO.Stream { public DeflateStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } @@ -54,6 +75,27 @@ 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(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 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; } + } public partial class GZipStream : System.IO.Stream { public GZipStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } @@ -149,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 { @@ -158,6 +201,27 @@ public enum ZLibCompressionStrategy RunLengthEncoding = 3, Fixed = 4, } + public sealed partial class ZLibDecoder : System.IDisposable + { + 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 static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } + } + public sealed partial class ZLibEncoder : System.IDisposable + { + public ZLibEncoder() { } + 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 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; } + } 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 5fd5e9e3cedc88..4df30b11d87c9c 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -170,6 +170,7 @@ The underlying compression routine returned an unexpected error code: '{0}'. + 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..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,7 +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); + } + + /// + /// 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) + { + EnsureNotDisposed(); + Debug.Assert(_state is not null); + + bytesConsumed = 0; + bytesWritten = 0; + + if (_finished) + { + return OperationStatus.Done; + } + + if (destination.IsEmpty && source.Length > 0) + { + return OperationStatus.DestinationTooSmall; + } + + 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..966b8d1b450565 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -0,0 +1,357 @@ +// 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; + + /// + /// The default quality value (optimal compression). + /// + internal const int DefaultQuality = 6; + + /// + /// Initializes a new instance of the class using the default quality. + /// + /// Failed to create the instance. + public DeflateEncoder() + : this(DefaultQuality) + { + } + + /// + /// Initializes a new instance of the class using the specified quality. + /// + /// 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(int quality) + : this(quality, DefaultWindowLog) + { + } + + /// + /// 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, 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 that accepts quality, windowLog (8-15), and format. + /// Validates both parameters and transforms windowLog to windowBits based on format. + /// + internal DeflateEncoder(int quality, int windowLog, CompressionFormat format) + { + ValidateQuality(quality); + ValidateWindowLog(windowLog); + + _disposed = false; + _finished = false; + + // 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( + (ZLibNative.CompressionLevel)quality, + windowBits, + ZLibNative.Deflate_DefaultMemLevel, + ZLibNative.CompressionStrategy.DefaultStrategy); + } + + /// + /// Internal constructor that accepts ZLibCompressionOptions and format. + /// + internal DeflateEncoder(ZLibCompressionOptions options, CompressionFormat format) + { + ArgumentNullException.ThrowIfNull(options); + + _disposed = false; + _finished = false; + + // -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 + { + CompressionFormat.Deflate => -windowLog, + CompressionFormat.ZLib => windowLog, + CompressionFormat.GZip => windowLog + 16, + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + + _state = ZLibNative.ZLibStreamHandle.CreateForDeflate( + (ZLibNative.CompressionLevel)options.CompressionLevel, + windowBits, + ZLibNative.Deflate_DefaultMemLevel, + (ZLibNative.CompressionStrategy)options.CompressionStrategy); + } + + + /// + /// 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) + { + ArgumentOutOfRangeException.ThrowIfLessThan(quality, MinQuality, nameof(quality)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(quality, MaxQuality, nameof(quality)); + } + + private static void ValidateWindowLog(int windowLog) + { + ArgumentOutOfRangeException.ThrowIfLessThan(windowLog, MinWindowLog, nameof(windowLog)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(windowLog, MaxWindowLog, nameof(windowLog)); + } + + /// + /// Frees and disposes unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + _state?.Dispose(); + _state = null; + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + /// + /// 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 or exceeds . + public static long GetMaxCompressedLength(long inputLength) + { + ArgumentOutOfRangeException.ThrowIfNegative(inputLength); + ArgumentOutOfRangeException.ThrowIfGreaterThan(inputLength, uint.MaxValue); + + return (long)Interop.ZLib.compressBound((uint)inputLength); + } + + /// + /// 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) + { + EnsureNotDisposed(); + 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 + ? OperationStatus.Done + : _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : OperationStatus.Done, + ZLibNative.ErrorCode.StreamEnd => OperationStatus.Done, + ZLibNative.ErrorCode.BufError => _state.AvailOut == 0 + ? OperationStatus.DestinationTooSmall + : _state.AvailIn == 0 + ? OperationStatus.Done + : 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) + { + EnsureNotDisposed(); + 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 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) + => 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. + /// + /// 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). + /// 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, int quality, int windowLog) + { + 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/GZipDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs new file mode 100644 index 00000000000000..74006573b8b82b --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipDecoder.cs @@ -0,0 +1,68 @@ +// 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; + private bool _disposed; + + /// + /// 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() + { + _disposed = true; + _deflateDecoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + /// + /// 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) + { + EnsureNotDisposed(); + return _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..0a8e5b767e71d6 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs @@ -0,0 +1,153 @@ +// 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; + private bool _disposed; + + /// + /// Initializes a new instance of the class using the default quality. + /// + /// Failed to create the instance. + public GZipEncoder() + : this(DeflateEncoder.DefaultQuality, DeflateEncoder.DefaultWindowLog) + { + } + + /// + /// Initializes a new instance of the class using the specified quality. + /// + /// 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(int quality) + : this(quality, DeflateEncoder.DefaultWindowLog) + { + } + + /// + /// 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, DeflateEncoder.CompressionFormat.GZip); + } + + /// + /// 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 GZipEncoder(int quality, int windowLog) + { + _deflateEncoder = new DeflateEncoder(quality, windowLog, DeflateEncoder.CompressionFormat.GZip); + } + + /// + /// Frees and disposes unmanaged resources. + /// + 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. + /// + /// 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 or exceeds . + public static long GetMaxCompressedLength(long inputLength) + { + // GZip has a larger header than raw deflate, so add extra overhead + long baseLength = DeflateEncoder.GetMaxCompressedLength(inputLength); + + // GZip adds 18 bytes: 10-byte header + 8-byte trailer (CRC32 + original size) + return baseLength + 18; + } + + /// + /// 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) + { + 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. + /// + /// 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) + { + EnsureNotDisposed(); + return _deflateEncoder.Flush(destination, out bytesWritten); + } + + /// + /// 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, 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) + => 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. + /// + /// 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). + /// 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, int quality, int windowLog) + { + 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/ZLibCompressionOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs index 387e52d713841e..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,6 +10,7 @@ public sealed class ZLibCompressionOptions { private int _compressionLevel = -1; private ZLibCompressionStrategy _strategy; + private int _windowLog = -1; /// /// Gets or sets the compression level for a compression stream. @@ -45,6 +46,29 @@ 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 -1 or greater than 15, or between 0 and 7. + /// + /// 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 + { + if (value != -1) + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, DeflateEncoder.MinWindowLog); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, DeflateEncoder.MaxWindowLog); + } + + _windowLog = value; + } + } } /// 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..ccc943ebaf8905 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibDecoder.cs @@ -0,0 +1,68 @@ +// 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; + private bool _disposed; + + /// + /// 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() + { + _disposed = true; + _deflateDecoder.Dispose(); + } + + private void EnsureNotDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + /// + /// 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) + { + EnsureNotDisposed(); + return _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..cb84da2652b621 --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs @@ -0,0 +1,146 @@ +// 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; + private bool _disposed; + + /// + /// Initializes a new instance of the class using the default quality. + /// + /// Failed to create the instance. + public ZLibEncoder() + : this(DeflateEncoder.DefaultQuality) + { + } + + /// + /// Initializes a new instance of the class using the specified quality. + /// + /// 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(int quality) + : this(quality, DeflateEncoder.DefaultWindowLog) + { + } + + /// + /// 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, DeflateEncoder.CompressionFormat.ZLib); + } + + /// + /// 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 ZLibEncoder(int quality, int windowLog) + { + _deflateEncoder = new DeflateEncoder(quality, windowLog, DeflateEncoder.CompressionFormat.ZLib); + } + + /// + /// Frees and disposes unmanaged resources. + /// + 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. + /// + /// 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 or exceeds . + public static long GetMaxCompressedLength(long inputLength) => DeflateEncoder.GetMaxCompressedLength(inputLength); + + /// + /// 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) + { + 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. + /// + /// 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) + { + EnsureNotDisposed(); + return _deflateEncoder.Flush(destination, out bytesWritten); + } + + /// + /// 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, 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) + => 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. + /// + /// 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). + /// 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, int quality, int windowLog) + { + 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; + } + } +} 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..721a95bde83693 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/DeflateEncoderDecoderTests.cs @@ -0,0 +1,164 @@ +// 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 DeflateEncoderDecoderTests : EncoderDecoderTestBase + { + protected override bool SupportsDictionaries => false; + protected override bool SupportsReset => false; + + protected override string WindowLogParamName => "windowLog"; + protected override string InputLengthParamName => "inputLength"; + + // 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(); + 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); + + 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); + } + + #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..8932ab0df600bd --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/GZipEncoderDecoderTests.cs @@ -0,0 +1,188 @@ +// 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 => "inputLength"; + + // 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(); + 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); + + 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_GZipStream_Interop() + { + byte[] input = CreateTestData(); + byte[] compressed = new byte[GetMaxCompressedLength(input.Length)]; + using var encoder = new GZipEncoder(6); + 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 fa2d85fc0656da..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,6 +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..f75a33eda3ed82 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/ZlibEncoderDecoderTests.cs @@ -0,0 +1,151 @@ +// 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 => "inputLength"; + + // 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(); + 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); + + 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); + } + + #endregion + } +} 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 1dd180a79a3239..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 @@ -21,6 +21,7 @@ EXPORTS CompressionNative_InflateEnd CompressionNative_InflateInit2_ CompressionNative_InflateReset2_ + CompressionNative_CompressBound ZSTD_createCCtx ZSTD_createDCtx ZSTD_freeCCtx 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 f22e8648ff5b42..4b6725b1c0820d 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,6 +21,7 @@ CompressionNative_Inflate CompressionNative_InflateEnd CompressionNative_InflateInit2_ CompressionNative_InflateReset2_ +CompressionNative_CompressBound ZSTD_createCCtx ZSTD_createDCtx ZSTD_freeCCtx 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) 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); +} 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);