Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ZLib, Brotli compression options #105430

Merged
merged 11 commits into from
Jul 29, 2024
16 changes: 5 additions & 11 deletions src/libraries/Common/src/System/IO/Compression/ZLibNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ public enum CompressionLevel : int
/// </summary>
public enum CompressionStrategy : int
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
{
DefaultStrategy = 0
DefaultStrategy = 0,
Filtered = 1,
HuffmanOnly = 2,
RunLengthEncoding = 3,
Fixed = 4
}

/// <summary>
Expand Down Expand Up @@ -199,7 +203,6 @@ public enum State

private volatile State _initializationState;


public ZLibStreamHandle()
: base(new IntPtr(-1), true)
{
Expand All @@ -217,7 +220,6 @@ public State InitializationState
get { return _initializationState; }
}


protected override bool ReleaseHandle() =>
InitializationState switch
{
Expand Down Expand Up @@ -257,14 +259,12 @@ private void EnsureNotDisposed()
ObjectDisposedException.ThrowIf(InitializationState == State.Disposed, this);
}


private void EnsureState(State requiredState)
{
if (InitializationState != requiredState)
throw new InvalidOperationException("InitializationState != " + requiredState.ToString());
}


public unsafe ErrorCode DeflateInit2_(CompressionLevel level, int windowBits, int memLevel, CompressionStrategy strategy)
{
EnsureNotDisposed();
Expand All @@ -279,7 +279,6 @@ public unsafe ErrorCode DeflateInit2_(CompressionLevel level, int windowBits, in
}
}


public unsafe ErrorCode Deflate(FlushCode flush)
{
EnsureNotDisposed();
Expand All @@ -291,7 +290,6 @@ public unsafe ErrorCode Deflate(FlushCode flush)
}
}


public unsafe ErrorCode DeflateEnd()
{
EnsureNotDisposed();
Expand All @@ -306,7 +304,6 @@ public unsafe ErrorCode DeflateEnd()
}
}


public unsafe ErrorCode InflateInit2_(int windowBits)
{
EnsureNotDisposed();
Expand All @@ -321,7 +318,6 @@ public unsafe ErrorCode InflateInit2_(int windowBits)
}
}


public unsafe ErrorCode Inflate(FlushCode flush)
{
EnsureNotDisposed();
Expand All @@ -333,7 +329,6 @@ public unsafe ErrorCode Inflate(FlushCode flush)
}
}


public unsafe ErrorCode InflateEnd()
{
EnsureNotDisposed();
Expand All @@ -359,7 +354,6 @@ public static ErrorCode CreateZLibStreamForDeflate(out ZLibStreamHandle zLibStre
return zLibStreamHandle.DeflateInit2_(level, windowBits, memLevel, strategy);
}


public static ErrorCode CreateZLibStreamForInflate(out ZLibStreamHandle zLibStreamHandle, int windowBits)
{
zLibStreamHandle = new ZLibStreamHandle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ public static IEnumerable<object[]> UncompressedTestFiles()
yield return new object[] { Path.Combine("UncompressedTestFiles", "sum") };
yield return new object[] { Path.Combine("UncompressedTestFiles", "xargs.1") };
}
public static IEnumerable<object[]> UncompressedTestFilesZLib()
{
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.doc") };
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.pdf") };
yield return new object[] { Path.Combine("UncompressedTestFiles", "sum") };
}
public static IEnumerable<object[]> ZLibOptionsRoundTripTestData()
{
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.doc"), new ZLibCompressionOptions() { CompressionLevel = 0, CompressionStrategy = ZLibCompressionStrategy.Default } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.docx"), new ZLibCompressionOptions() { CompressionLevel = 3, CompressionStrategy = ZLibCompressionStrategy.Filtered } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.pdf"), new ZLibCompressionOptions() { CompressionLevel = 5, CompressionStrategy = ZLibCompressionStrategy.RunLengthEncoding } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "TestDocument.txt"), new ZLibCompressionOptions() { CompressionLevel = 7, CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "alice29.txt"), new ZLibCompressionOptions() { CompressionLevel = 9, CompressionStrategy = ZLibCompressionStrategy.Fixed } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "asyoulik.txt"), new ZLibCompressionOptions() { CompressionLevel = 2, CompressionStrategy = ZLibCompressionStrategy.RunLengthEncoding } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "cp.html"), new ZLibCompressionOptions() { CompressionLevel = 4, CompressionStrategy = ZLibCompressionStrategy.Default } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "fields.c"), new ZLibCompressionOptions() { CompressionLevel = 6, CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "grammar.lsp"), new ZLibCompressionOptions() { CompressionLevel = 8, CompressionStrategy = ZLibCompressionStrategy.Default } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "kennedy.xls"), new ZLibCompressionOptions() { CompressionLevel = 1, CompressionStrategy = ZLibCompressionStrategy.Fixed } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "lcet10.txt"), new ZLibCompressionOptions() { CompressionLevel = 1, CompressionStrategy = ZLibCompressionStrategy.Filtered } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "plrabn12.txt"), new ZLibCompressionOptions() { CompressionLevel = 2, CompressionStrategy = ZLibCompressionStrategy.RunLengthEncoding } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "ptt5"), new ZLibCompressionOptions() { CompressionLevel = 3, CompressionStrategy = ZLibCompressionStrategy.Default } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "sum"), new ZLibCompressionOptions() { CompressionLevel = 4, CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly } };
yield return new object[] { Path.Combine("UncompressedTestFiles", "xargs.1"), new ZLibCompressionOptions() { CompressionLevel = 5, CompressionStrategy = ZLibCompressionStrategy.Filtered } };
}
protected virtual string UncompressedTestFile() => Path.Combine("UncompressedTestFiles", "TestDocument.pdf");
protected abstract string CompressedTestFile(string uncompressedPath);
}
Expand All @@ -37,6 +61,7 @@ public abstract class CompressionStreamTestBase : CompressionTestBase
public abstract Stream CreateStream(Stream stream, CompressionMode mode, bool leaveOpen);
public abstract Stream CreateStream(Stream stream, CompressionLevel level);
public abstract Stream CreateStream(Stream stream, CompressionLevel level, bool leaveOpen);
public abstract Stream CreateStream(Stream stream, ZLibCompressionOptions options, bool leaveOpen);
public abstract Stream BaseStream(Stream stream);
public virtual int BufferSize { get => 8192; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,14 @@ public async Task TestLeaveOpenAfterValidDecompress()
[Fact]
public void Ctor_ArgumentValidation()
{
Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionLevel.Fastest));
Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionMode.Decompress));
Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionMode.Compress));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionLevel.Fastest));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionMode.Decompress));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionMode.Compress));

Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionLevel.Fastest, true));
Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionMode.Decompress, false));
Assert.Throws<ArgumentNullException>(() => CreateStream(null, CompressionMode.Compress, true));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionLevel.Fastest, true));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionMode.Decompress, false));
Assert.Throws<ArgumentNullException>("stream", () => CreateStream(null, CompressionMode.Compress, true));
Assert.Throws<ArgumentNullException>("compressionOptions", () => CreateStream(new MemoryStream(), null, true));

AssertExtensions.Throws<ArgumentException>("mode", () => CreateStream(new MemoryStream(), (CompressionMode)42));
AssertExtensions.Throws<ArgumentException>("mode", () => CreateStream(new MemoryStream(), (CompressionMode)43, true));
Expand Down Expand Up @@ -471,17 +472,18 @@ public async Task BaseStream_ValidAfterDisposeWithTrueLeaveOpen(CompressionMode
}

[Theory]
[MemberData(nameof(UncompressedTestFiles))]
[MemberData(nameof(UncompressedTestFilesZLib))]
public async Task CompressionLevel_SizeInOrder(string testFile)
{
using var uncompressedStream = await LocalMemoryStream.readAppFileAsync(testFile);

async Task<long> GetLengthAsync(CompressionLevel compressionLevel)
{
uncompressedStream.Position = 0;
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
using var mms = new MemoryStream();
using var compressor = CreateStream(mms, compressionLevel);
await uncompressedStream.CopyToAsync(compressor);
compressor.Flush();
await compressor.FlushAsync();
return mms.Length;
}

Expand All @@ -494,6 +496,69 @@ async Task<long> GetLengthAsync(CompressionLevel compressionLevel)
Assert.True(fastestLength >= optimalLength);
Assert.True(optimalLength >= smallestLength);
}

[Theory]
[MemberData(nameof(ZLibOptionsRoundTripTestData))]
public async Task RoundTripWithZLibCompressionOptions(string testFile, ZLibCompressionOptions options)
{
using var uncompressedStream = await LocalMemoryStream.readAppFileAsync(testFile);
var compressedStream = await CompressTestFile(uncompressedStream, options);
using var decompressor = CreateStream(compressedStream, mode: CompressionMode.Decompress);
using var decompressorOutput = new MemoryStream();
await decompressor.CopyToAsync(decompressorOutput);
await decompressor.DisposeAsync();
decompressorOutput.Position = 0;
uncompressedStream.Position = 0;

byte[] uncompressedStreamBytes = uncompressedStream.ToArray();
byte[] decompressorOutputBytes = decompressorOutput.ToArray();

Assert.Equal(uncompressedStreamBytes.Length, decompressorOutputBytes.Length);
for (int i = 0; i < uncompressedStreamBytes.Length; i++)
{
Assert.Equal(uncompressedStreamBytes[i], decompressorOutputBytes[i]);
}
}

private async Task<MemoryStream> CompressTestFile(LocalMemoryStream testStream, ZLibCompressionOptions options)
{
var compressorOutput = new MemoryStream();
using (var compressionStream = CreateStream(compressorOutput, options, leaveOpen: true))
{
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await testStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await compressionStream.WriteAsync(buffer, 0, bytesRead);
}
}

compressorOutput.Position = 0;
return compressorOutput;
}

protected async Task CompressionLevel_SizeInOrderBase(string testFile)
{
using var uncompressedStream = await LocalMemoryStream.readAppFileAsync(testFile);

async Task<long> GetLengthAsync(int compressionLevel)
{
uncompressedStream.Position = 0;
using var mms = new MemoryStream();
using var compressor = CreateStream(mms, new ZLibCompressionOptions() { CompressionLevel = compressionLevel, CompressionStrategy = ZLibCompressionStrategy.Default }, leaveOpen: false);
await uncompressedStream.CopyToAsync(compressor);
await compressor.FlushAsync();
return mms.Length;
}

long prev = await GetLengthAsync(0);
for (int i = 1; i < 10; i++)
{
long cur = await GetLengthAsync(i);
Assert.True(cur <= prev, $"Expected {cur} <= {prev} for quality {i}");
prev = cur;
}
}
}

public enum TestScenario
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

namespace System.IO.Compression
{
public sealed class BrotliCompressionOptions
{
public int Quality { get; set; }
}
public partial struct BrotliDecoder : System.IDisposable
{
private object _dummy;
Expand All @@ -28,6 +32,7 @@ public void Dispose() { }
}
public sealed partial class BrotliStream : System.IO.Stream
{
public BrotliStream(System.IO.Stream stream, System.IO.Compression.BrotliCompressionOptions compressionOptions, bool leaveOpen = false) { }
public BrotliStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel) { }
public BrotliStream(System.IO.Stream stream, System.IO.Compression.CompressionLevel compressionLevel, bool leaveOpen) { }
public BrotliStream(System.IO.Stream stream, System.IO.Compression.CompressionMode mode) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="$(CommonPath)Interop\Interop.Brotli.cs" />
<!-- The native compression lib uses a BROTLI_BOOL type analogous to the Windows BOOL type -->
<Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" />
<Compile Include="System\IO\Compression\enc\BrotliCompressionOptions.cs" />
<Compile Include="System\IO\Compression\enc\BrotliStream.Compress.cs" />
<Compile Include="System\IO\Compression\dec\BrotliStream.Decompress.cs" />
<Compile Include="System\IO\Compression\BrotliUtils.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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
{
/// <summary>
/// Provides compression options to be used with <see cref="BrotliStream"/>.
/// </summary>
public sealed class BrotliCompressionOptions
{
private int _quality = BrotliUtils.Quality_Default;

/// <summary>
/// Gets or sets the compression quality for a Brotli compression stream.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException" accessor="set">The value is less than 0 or greater than 11.</exception>
/// <remarks>
/// The higher the quality, the slower the compression. Range is from 0 to 11. The default value is 4.
/// </remarks>
public int Quality
{
get => _quality;
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 0, nameof(value));
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 11, nameof(value));

_quality = value;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,33 @@ public sealed partial class BrotliStream : Stream
/// <summary>Initializes a new instance of the <see cref="System.IO.Compression.BrotliStream" /> class by using the specified stream and compression level.</summary>
/// <param name="stream">The stream to which compressed data is written.</param>
/// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression efficiency when compressing data to the stream.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is <see langword="null" />.</exception>
public BrotliStream(Stream stream, CompressionLevel compressionLevel) : this(stream, compressionLevel, leaveOpen: false) { }

/// <summary>Initializes a new instance of the <see cref="System.IO.Compression.BrotliStream" /> class by using the specified stream and compression level, and optionally leaves the stream open.</summary>
/// <param name="stream">The stream to which compressed data is written.</param>
/// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression efficiency when compressing data to the stream.</param>
/// <param name="leaveOpen"><see langword="true" /> to leave the stream open after disposing the <see cref="System.IO.Compression.BrotliStream" /> object; otherwise, <see langword="false" />.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is <see langword="null" />.</exception>
public BrotliStream(Stream stream, CompressionLevel compressionLevel, bool leaveOpen) : this(stream, CompressionMode.Compress, leaveOpen)
{
_encoder.SetQuality(BrotliUtils.GetQualityFromCompressionLevel(compressionLevel));
}

/// <summary>
/// Initializes a new instance of the <see cref="System.IO.Compression.BrotliStream" /> class by using the specified stream and compression options, and optionally leaves the stream open.
/// </summary>
/// <param name="stream">The stream to which compressed data is written.</param>
/// <param name="compressionOptions">The Brotli options for fine tuning the compression stream.</param>
/// <param name="leaveOpen"><see langword="true" /> to leave the stream open after disposing the <see cref="System.IO.Compression.BrotliStream" /> object; otherwise, <see langword="false" />.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> or <paramref name="compressionOptions"/> is <see langword="null" />.</exception>
public BrotliStream(Stream stream, BrotliCompressionOptions compressionOptions, bool leaveOpen = false) : this(stream, CompressionMode.Compress, leaveOpen)
{
ArgumentNullException.ThrowIfNull(compressionOptions);

_encoder.SetQuality(compressionOptions.Quality);
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>Writes compressed bytes to the underlying stream from the specified byte array.</summary>
/// <param name="buffer">The buffer containing the data to compress.</param>
/// <param name="offset">The byte offset in <paramref name="buffer" /> from which the bytes will be read.</param>
Expand Down
Loading
Loading