Add span-based Deflate, ZLib and GZip encoder/decoder APIs#123145
Add span-based Deflate, ZLib and GZip encoder/decoder APIs#123145iremyux wants to merge 89 commits intodotnet:mainfrom
Conversation
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoderOptions.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/ref/System.IO.Compression.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibDecoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZlibEncoder.cs
Outdated
Show resolved
Hide resolved
| CompressionLevel.Fastest => ZLibNative.CompressionLevel.BestSpeed, | ||
| CompressionLevel.NoCompression => ZLibNative.CompressionLevel.NoCompression, | ||
| CompressionLevel.SmallestSize => ZLibNative.CompressionLevel.BestCompression, | ||
| _ => throw new ArgumentOutOfRangeException(nameof(compressionLevel)), |
There was a problem hiding this comment.
This would fail on valid native compression levels not covered by the CompressionLevel enum. Instead I think it should check if the value is is < -1 or > 9 to throw out of range instead.
There was a problem hiding this comment.
Also to add on to the above, now those who want compression levels that just happen to == a value in the CompressionLevel enum will now not be able to use those compression levels either. Perhaps a solution to this is to expose a version of the ctor with CompressionLevel and a version with int that gets casted to ZLibNative.CompressionLevel after a range check.
src/libraries/System.IO.Compression/tests/DeflateZLibGZipEncoderDecoderTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs
Show resolved
Hide resolved
| public static long GetMaxCompressedLength(long inputLength) | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegative(inputLength); | ||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(inputLength, uint.MaxValue); | ||
|
|
||
| // compressBound() returns the upper bound for zlib-wrapped deflate output. | ||
| // For raw deflate (no header/trailer) this slightly overestimates, which is safe. | ||
| return (long)Interop.ZLib.compressBound((uint)inputLength); | ||
| } |
There was a problem hiding this comment.
GetMaxCompressedLength relies on Interop.ZLib.compressBound which currently returns a 32-bit value. For sufficiently large inputLength, the true bound can exceed 32 bits, leading to truncation and an incorrect (too small) maximum length. Either tighten the allowed inputLength range to what the underlying bound can represent safely, or switch the native + interop compressBound plumbing to a wider return type and add managed overflow checks (similar to ZstandardEncoder.GetMaxCompressedLength).
There was a problem hiding this comment.
It feels weird to accept long as input but then throw if it is outside of 32-bit uint range. Can we make larger numbers work?
src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateDecoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs
Show resolved
Hide resolved
…ZLibCompressionOptions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs:25
- ZstandardUtils.WindowLog_Max is platform-dependent (31 on 64-bit, 30 on 32-bit). Hardcoding MaxWindowLog=31 and InvalidWindowLogTooHigh=32 will likely fail on 32-bit test runs. Consider using Environment.Is64BitProcess (or referencing ZstandardUtils.WindowLog_Max) to set MaxWindowLog and the corresponding invalid-too-high value.
protected override int MinWindowLog => 10;
protected override int MaxWindowLog => 31;
protected override int InvalidQualityTooLow => -(1 << 17) - 1;
protected override int InvalidQualityTooHigh => 23;
protected override int InvalidWindowLogTooLow => 9;
protected override int InvalidWindowLogTooHigh => 32;
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs
Show resolved
Hide resolved
src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibCompressionOptions.cs
Show resolved
Hide resolved
src/libraries/System.IO.Compression/tests/Zstandard/ZstandardEncoderDecoderTests.cs
Show resolved
Hide resolved
| private static int ResolveWindowBits(int windowLog, CompressionFormat format) | ||
| { | ||
| // zlib-ng rejects windowBits 8 for raw deflate and gzip; classic zlib silently upgrades to 9. | ||
| if (format != CompressionFormat.ZLib) | ||
| { | ||
| windowLog = Math.Max(windowLog, 9); | ||
| } | ||
|
|
||
| return format switch | ||
| { | ||
| CompressionFormat.Deflate => -windowLog, | ||
| CompressionFormat.ZLib => windowLog, | ||
| CompressionFormat.GZip => windowLog + 16, | ||
| _ => throw new ArgumentOutOfRangeException(nameof(format)) | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we move this to some shared class (ZLibNative?) and reuse this in DeflateStream etc?
| public static long GetMaxCompressedLength(long inputLength) | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegative(inputLength); | ||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(inputLength, uint.MaxValue); | ||
|
|
||
| // compressBound() returns the upper bound for zlib-wrapped deflate output. | ||
| // For raw deflate (no header/trailer) this slightly overestimates, which is safe. | ||
| return (long)Interop.ZLib.compressBound((uint)inputLength); | ||
| } |
There was a problem hiding this comment.
It feels weird to accept long as input but then throw if it is outside of 32-bit uint range. Can we make larger numbers work?
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we revert this whitespace change?
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we revert this whitespace change?
| } | ||
| }, testScenario.ToString()).Dispose(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we revert this whitespace change?
|
|
||
| namespace System.IO.Compression | ||
| { | ||
| public abstract class ZLibEncoderDecoderTestBase : EncoderDecoderTestBase |
There was a problem hiding this comment.
This file can be moved to System.IO.Compression.Tests, as it is only used by its tests.
This PR introduces new span-based, streamless compression and decompression APIs for Deflate, ZLib, and GZip formats, matching the existing
BrotliEncoder/BrotliDecoderpattern.New APIs
DeflateEncoder/DeflateDecoderZLibEncoder/ZLibDecoderGZipEncoder/GZipDecoderThese classes provide:
Compress(),Decompress(), andFlush()TryCompress() andTryDecompress()for simple scenariosGetMaxCompressedLength()to calculate buffer sizesCloses #62113
Closes #39327
Closes #44793