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/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 412fa9f2a25535..24c108e076b599 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 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 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 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 partial class GZipStream : System.IO.Stream { public GZipStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { } @@ -158,6 +198,26 @@ 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(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 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 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..1c07e306dacb0c 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -170,6 +170,24 @@ 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.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); + } + + 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..4c0b0e19eb179b --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -0,0 +1,265 @@ +// 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 long GetMaxCompressedLength(long inputSize) + { + ArgumentOutOfRangeException.ThrowIfNegative(inputSize); + + return (long)Interop.ZLib.compressBound((uint)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) + { + 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..322c110b04ae33 --- /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 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: 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) + => _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/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..96d7b197a2d256 --- /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 long GetMaxCompressedLength(long 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; + } + } +} 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 fa2d85fc0656da..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,6 +47,7 @@ + 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);