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 f9ce507af62804..3861dd42123ed7 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -285,6 +285,7 @@ public sealed partial class ZstandardDecoder : System.IDisposable { public ZstandardDecoder() { } public ZstandardDecoder(int maxWindowLog) { } + public ZstandardDecoder(System.IO.Compression.ZstandardDecompressionOptions decompressionOptions) { } public ZstandardDecoder(System.IO.Compression.ZstandardDictionary dictionary) { } public ZstandardDecoder(System.IO.Compression.ZstandardDictionary dictionary, int maxWindowLog) { } public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } @@ -297,6 +298,14 @@ public void SetPrefix(System.ReadOnlyMemory prefix) { } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")] + public sealed partial class ZstandardDecompressionOptions + { + public ZstandardDecompressionOptions() { } + public System.IO.Compression.ZstandardDictionary? Dictionary { get { throw null; } set { } } + public int MaxWindowLog { get { throw null; } set { } } + } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")] public sealed partial class ZstandardDictionary : System.IDisposable { internal ZstandardDictionary() { } @@ -338,6 +347,7 @@ public ZstandardStream(System.IO.Stream stream, System.IO.Compression.Compressio public ZstandardStream(System.IO.Stream stream, System.IO.Compression.CompressionMode mode, System.IO.Compression.ZstandardDictionary dictionary, bool leaveOpen = false) { } public ZstandardStream(System.IO.Stream stream, System.IO.Compression.ZstandardCompressionOptions compressionOptions, bool leaveOpen = false) { } public ZstandardStream(System.IO.Stream stream, System.IO.Compression.ZstandardDecoder decoder, bool leaveOpen = false) { } + public ZstandardStream(System.IO.Stream stream, System.IO.Compression.ZstandardDecompressionOptions decompressionOptions, bool leaveOpen = false) { } public ZstandardStream(System.IO.Stream stream, System.IO.Compression.ZstandardEncoder encoder, bool leaveOpen = false) { } public System.IO.Stream BaseStream { get { throw null; } } public override bool CanRead { get { throw null; } } 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 070415553fe72a..3a6ccc53f6ebd4 100644 --- a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj +++ b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj @@ -92,6 +92,7 @@ + diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/Zstandard.PlatformNotSupported.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/Zstandard.PlatformNotSupported.cs index ffea4a91d779f0..1da10593dd660c 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/Zstandard.PlatformNotSupported.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/Zstandard.PlatformNotSupported.cs @@ -1,8 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace System.IO.Compression { + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] + public sealed class ZstandardDecompressionOptions + { + public ZstandardDecompressionOptions() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); + public ZstandardDictionary? Dictionary { get => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); set => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); } + public int MaxWindowLog { get => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); set => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); } + } + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed class ZstandardCompressionOptions @@ -28,6 +39,7 @@ public sealed class ZstandardDecoder : IDisposable { public ZstandardDecoder() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardDecoder(int maxWindowLog) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); + public ZstandardDecoder(ZstandardDecompressionOptions decompressionOptions) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardDecoder(ZstandardDictionary dictionary) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardDecoder(ZstandardDictionary dictionary, int maxWindowLog) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public System.Buffers.OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); @@ -84,6 +96,7 @@ public sealed partial class ZstandardStream : Stream public ZstandardStream(Stream stream, CompressionMode mode, ZstandardDictionary dictionary, bool leaveOpen = false) : base() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardStream(Stream stream, ZstandardCompressionOptions compressionOptions, bool leaveOpen = false) : base() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardStream(Stream stream, ZstandardDecoder decoder, bool leaveOpen = false) : base() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); + public ZstandardStream(Stream stream, ZstandardDecompressionOptions decompressionOptions, bool leaveOpen = false) : base() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public ZstandardStream(Stream stream, ZstandardEncoder encoder, bool leaveOpen = false) : base() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public Stream BaseStream => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); public override bool CanRead => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ZstandardCompression); diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecoder.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecoder.cs index c5a89178bcd6be..4c347d389c9e9e 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecoder.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -78,6 +79,37 @@ public ZstandardDecoder(ZstandardDictionary dictionary) } } + /// Initializes a new instance of the class with the specified decompression options. + /// The options to use for Zstandard decompression. + /// is null. + /// Failed to create the instance. + public ZstandardDecoder(ZstandardDecompressionOptions decompressionOptions) + { + ArgumentNullException.ThrowIfNull(decompressionOptions); + + _disposed = false; + + InitializeDecoder(); + + try + { + if (decompressionOptions.MaxWindowLog != 0) + { + SetWindowLog(decompressionOptions.MaxWindowLog); + } + + if (decompressionOptions.Dictionary is not null) + { + SetDictionary(decompressionOptions.Dictionary); + } + } + catch + { + _context.Dispose(); + throw; + } + } + /// Initializes a new instance of the class with the specified dictionary and maximum window size. /// The decompression dictionary to use. /// The maximum window size to use for decompression, expressed as base 2 logarithm. diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecompressionOptions.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecompressionOptions.cs new file mode 100644 index 00000000000000..ed1cea2457dcce --- /dev/null +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecompressionOptions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Compression +{ + /// Provides decompression options to be used with Zstandard decompression. + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] + public sealed class ZstandardDecompressionOptions + { + + /// Gets or sets the maximum allowed window size when decompressing payloads, expressed as base 2 logarithm. + /// The maximum window size for decompression, expressed as base 2 logarithm. + /// + /// The valid range is from to . + /// Value 0 indicates the implementation-defined default window size. + /// + /// The value is not 0 and is not between and . + public int MaxWindowLog + { + get; + set + { + if (value != 0) + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, ZstandardUtils.WindowLog_Min, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, ZstandardUtils.WindowLog_Max, nameof(value)); + } + + field = value; + } + } + + /// Gets or sets the dictionary to use for decompression. + /// The decompression dictionary, or if no dictionary is used. + public ZstandardDictionary? Dictionary { get; set; } + } +} diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardStream.Decompress.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardStream.Decompress.cs index 1e11905d9b69f4..87cc625a45ab50 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardStream.Decompress.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardStream.Decompress.cs @@ -32,6 +32,23 @@ public ZstandardStream(Stream stream, ZstandardDecoder decoder, bool leaveOpen = _encoderOwned = false; } + /// Initializes a new instance of the class by using the specified stream, decompression options, and optionally leaves the stream open. + /// The stream from which data to decompress is read. + /// The options to use for Zstandard decompression. + /// to leave the stream open after the object is disposed; otherwise, . + /// or is . + /// does not support reading. + public ZstandardStream(Stream stream, ZstandardDecompressionOptions decompressionOptions, bool leaveOpen = false) + { + ArgumentNullException.ThrowIfNull(decompressionOptions); + + Init(stream, CompressionMode.Decompress); + _mode = CompressionMode.Decompress; + _leaveOpen = leaveOpen; + + _decoder = new ZstandardDecoder(decompressionOptions); + } + private bool TryDecompress(Span destination, out int bytesWritten, out OperationStatus lastResult) { Debug.Assert(_decoder != null); diff --git a/src/libraries/System.IO.Compression/tests/Zstandard/CompressionStreamUnitTests.Zstandard.cs b/src/libraries/System.IO.Compression/tests/Zstandard/CompressionStreamUnitTests.Zstandard.cs index aa6f961de3d77b..2ecc205c8aff32 100644 --- a/src/libraries/System.IO.Compression/tests/Zstandard/CompressionStreamUnitTests.Zstandard.cs +++ b/src/libraries/System.IO.Compression/tests/Zstandard/CompressionStreamUnitTests.Zstandard.cs @@ -282,5 +282,82 @@ public void StreamTruncation_IsDetected(TestScenario testScenario) }, testScenario.ToString()).Dispose(); } + [Fact] + public void ZstandardStream_WithDecompressionOptions_DecompressesData() + { + byte[] testData = ZstandardTestUtils.CreateTestData(); + byte[] compressedData = new byte[ZstandardEncoder.GetMaxCompressedLength(testData.Length)]; + Assert.True(ZstandardEncoder.TryCompress(testData, compressedData, out int compressedLength)); + Array.Resize(ref compressedData, compressedLength); + + ZstandardDecompressionOptions options = new(); + using MemoryStream input = new(compressedData); + using MemoryStream output = new(); + + using (ZstandardStream decompressionStream = new(input, options, leaveOpen: true)) + { + decompressionStream.CopyTo(output); + } + + Assert.Equal(testData, output.ToArray()); + } + + [Fact] + public void ZstandardStream_WithDecompressionOptions_NullOptions_ThrowsArgumentNullException() + { + using MemoryStream input = new(); + Assert.Throws("decompressionOptions", () => new ZstandardStream(input, (ZstandardDecompressionOptions)null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ZstandardStream_WithDecompressionOptions_WithDictionary_RoundTrips(bool async) + { + byte[] dictionaryData = ZstandardTestUtils.CreateSampleDictionary(); + using ZstandardDictionary dictionary = ZstandardDictionary.Create(dictionaryData); + + byte[] testData = ZstandardTestUtils.CreateTestData(5000); + + using MemoryStream compressedStream = new(); + using (ZstandardStream compressionStream = new(compressedStream, CompressionMode.Compress, dictionary, leaveOpen: true)) + { + if (async) + await compressionStream.WriteAsync(testData, 0, testData.Length); + else + compressionStream.Write(testData, 0, testData.Length); + } + + compressedStream.Position = 0; + + ZstandardDecompressionOptions options = new() { Dictionary = dictionary }; + using MemoryStream decompressedStream = new(); + using (ZstandardStream decompressionStream = new(compressedStream, options)) + { + if (async) + await decompressionStream.CopyToAsync(decompressedStream); + else + decompressionStream.CopyTo(decompressedStream); + } + + Assert.Equal(testData, decompressedStream.ToArray()); + } + + [Fact] + public void ZstandardStream_WithDecompressionOptions_DisposedStream_ThrowsObjectDisposedException() + { + byte[] testData = ZstandardTestUtils.CreateTestData(); + byte[] compressedData = new byte[ZstandardEncoder.GetMaxCompressedLength(testData.Length)]; + Assert.True(ZstandardEncoder.TryCompress(testData, compressedData, out int compressedLength)); + Array.Resize(ref compressedData, compressedLength); + + ZstandardDecompressionOptions options = new(); + using MemoryStream input = new(compressedData); + ZstandardStream decompressionStream = new(input, options); + decompressionStream.Dispose(); + + Assert.Throws(() => decompressionStream.Read(new byte[1], 0, 1)); + } + } } diff --git a/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardCompressionOptionsTests.cs b/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardCompressionOptionsTests.cs index 34af5d26bd2ccf..f5b6f5a9bfdfbc 100644 --- a/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardCompressionOptionsTests.cs +++ b/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardCompressionOptionsTests.cs @@ -76,4 +76,45 @@ public void TargetBlockSize_SetOutOfRange_ThrowsArgumentOutOfRangeException(int Assert.Throws(() => options.TargetBlockSize = targetBlockSize); } } -} \ No newline at end of file + + public class ZstandardDecompressionOptionsTests + { + [Theory] + [InlineData(0)] + [InlineData(10)] + [InlineData(23)] + [InlineData(30)] + public void MaxWindowLog_SetToValidRange_Succeeds(int maxWindowLog) + { + ZstandardDecompressionOptions options = new(); + options.MaxWindowLog = maxWindowLog; + Assert.Equal(maxWindowLog, options.MaxWindowLog); + } + + [Theory] + [InlineData(1)] + [InlineData(9)] + [InlineData(32)] + public void MaxWindowLog_SetOutOfRange_ThrowsArgumentOutOfRangeException(int maxWindowLog) + { + ZstandardDecompressionOptions options = new(); + Assert.Throws(() => options.MaxWindowLog = maxWindowLog); + } + + [Fact] + public void Dictionary_SetAndGet_RoundTrips() + { + using ZstandardDictionary dictionary = ZstandardDictionary.Create(ZstandardTestUtils.CreateSampleDictionary()); + ZstandardDecompressionOptions options = new(); + options.Dictionary = dictionary; + Assert.Same(dictionary, options.Dictionary); + } + + [Fact] + public void Dictionary_DefaultValue_IsNull() + { + ZstandardDecompressionOptions options = new(); + Assert.Null(options.Dictionary); + } + } +} diff --git a/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs b/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs index 8faff4e341014b..8eeace81547b8c 100644 --- a/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs @@ -576,5 +576,51 @@ public void Compress_AppendChecksum_RoundTrip(bool corrupt) Assert.Equal(input, decompressed); } } + + [Fact] + public void Decoder_Ctor_DecompressionOptions_Null_ThrowsArgumentNullException() + { + Assert.Throws("decompressionOptions", () => new ZstandardDecoder((ZstandardDecompressionOptions)null!)); + } + + [Theory] + [InlineData(0)] + [InlineData(10)] + public void Decoder_Ctor_DecompressionOptions_Succeeds(int maxWindowLog) + { + ZstandardDecompressionOptions options = new() { MaxWindowLog = maxWindowLog }; + using ZstandardDecoder decoder = new(options); + + byte[] testData = ZstandardTestUtils.CreateTestData(100); + byte[] compressed = new byte[ZstandardEncoder.GetMaxCompressedLength(testData.Length)]; + Assert.True(ZstandardEncoder.TryCompress(testData, compressed, out int compressedLength)); + + byte[] decompressed = new byte[testData.Length]; + OperationStatus status = decoder.Decompress(compressed.AsSpan(0, compressedLength), decompressed, out _, out int bytesWritten); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(testData, decompressed.AsSpan(0, bytesWritten).ToArray()); + } + + [Fact] + public void Decoder_Ctor_DecompressionOptions_WithDictionary_RoundTrips() + { + byte[] dictionaryData = ZstandardTestUtils.CreateSampleDictionary(); + using ZstandardDictionary dictionary = ZstandardDictionary.Create(dictionaryData); + + byte[] testData = ZstandardTestUtils.CreateTestData(500); + + // Compress with dictionary + using ZstandardEncoder encoder = new(dictionary); + byte[] compressed = new byte[ZstandardEncoder.GetMaxCompressedLength(testData.Length)]; + Assert.Equal(OperationStatus.Done, encoder.Compress(testData, compressed, out _, out int compressedLength, isFinalBlock: true)); + + // Decompress with ZstandardDecompressionOptions containing the dictionary + ZstandardDecompressionOptions options = new() { Dictionary = dictionary }; + using ZstandardDecoder decoder = new(options); + byte[] decompressed = new byte[testData.Length]; + OperationStatus status = decoder.Decompress(compressed.AsSpan(0, compressedLength), decompressed, out _, out int bytesWritten); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(testData, decompressed.AsSpan(0, bytesWritten).ToArray()); + } } } \ No newline at end of file