From 0b0d8968e479be3f0316394f5f8ecbac334587be Mon Sep 17 00:00:00 2001 From: Vlad Brezae Date: Mon, 10 Aug 2026 19:13:46 +0300 Subject: [PATCH] Fix DeflateEncoder.GetMaxCompressedLength conservative bound Previous code was using the implementation from compressBound(). This api returns a conservative bound when using the compress() APIs, which our .NET implementation doesn't use (it uses deflate directly). The compress API does a deflate with default configuration, while our API allows custom configuration (via the quality level, window size and a few others). This means that the compressBound is not valid when using the deflate API with custom parameters. zlib exposes the deflateBound method which can be used to obtain a conservative size. The problem with this API is that it requires to know the parameters of the compression, which we don't have available in our .NET API, given GetMaxCompressedLenght is static, applying universally to all compressions. While it is not yet decided if we will add a new API to support obtaining the bound from the library, this commit adds a workaround, by computing the maximum bound for every compression configuration exposed by our .NET APIs The formula is obtained from our zlib-ng sources in deflate.c. Mobile uses zlib however. Validated that this change works correctly on maccatalyst, ios simulator and that the current logic stands with the current open source code for zlib (which applies to android). The actual bound used by Apple and Android can always change, so we do have a small risk that our hardcoded limit will diverge. I believe the only safe solution for this would be to make use of the actual deflateBound method, which requires adjustement to our API. --- .../IO/Compression/EncoderDecoderTestBase.cs | 1 - .../System/IO/Compression/DeflateEncoder.cs | 32 ++++++------------- .../src/System/IO/Compression/GZipEncoder.cs | 7 ++-- .../src/System/IO/Compression/ZLibEncoder.cs | 4 +-- 4 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs b/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs index 9492c2bb185b16..af66929496225b 100644 --- a/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs +++ b/src/libraries/Common/tests/System/IO/Compression/EncoderDecoderTestBase.cs @@ -776,7 +776,6 @@ public void RoundTrip_AllCompressionLevels() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/127563", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android)] public void RoundTrip_AllWindowLogs() { byte[] input = CreateTestData(); 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 index f90a62291459f7..c1a96a48a88aab 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateEncoder.cs @@ -144,30 +144,16 @@ public static long GetMaxCompressedLength(long inputLength) { ArgumentOutOfRangeException.ThrowIfNegative(inputLength); - // For inputs up to 2 GiB, delegate to the native compressBound() function, which returns - // the exact upper bound for the zlib implementation linked into the current process - // (either classic zlib or zlib-ng, depending on platform and build flags). The 2^31 - // threshold keeps the value within the uint P/Invoke signature on all platforms. - - // Browser/WASI builds do not link the native compression library, - // so fall through to the managed formula on those platforms. - if (inputLength <= (1L << 31) && !OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) - { - return Interop.ZLib.compressBound((uint)inputLength); - } - - // For larger inputs, compute the bound in managed code using zlib-ng's quick-strategy - // formula. It is strictly larger than classic zlib's compressBound(), so it is a safe - // upper bound regardless of which implementation is linked at runtime. - // See: src/native/external/zlib-ng/compress.c and zutil.h. - // Use ulong to avoid overflow; reject inputs whose bound does not fit in long. + // This method does not know which windowLog2 or quality the caller will use, so + // compute the maximum of the bounds used by zlib-ng's deflateBound(). The code below mimics the logic + // to compute the maximum possible size returned by deflateBound. Our API doesn't expose all knobs + // so z_stream's strstart and gzhead are always null. We also don't handle the s390 corner case. ulong sourceLength = (ulong)inputLength; - ulong maxCompressedLength = sourceLength - + (sourceLength == 0 ? 1u : 0u) - + (sourceLength < 9 ? 1u : 0u) - + ((sourceLength + 7) >> 3) - + 3 // DEFLATE_BLOCK_OVERHEAD: (3 + 15 + 6) >> 3 - + 6; // ZLIB_WRAPLEN: zlib header (2 bytes) + Adler32 trailer (4 bytes) + const ulong wrapLength = 6; // GZIP_WRAPLEN is 18, GZipEncoder compensates for the rest + ulong maxCompressedLength = sourceLength + ((sourceLength + 7) >> 3) + ((sourceLength + 63) >> 6) + 5; + ulong storedBlockBound = sourceLength + (sourceLength >> 5) + (sourceLength >> 7) + (sourceLength >> 11) + 7; + + maxCompressedLength = Math.Max(maxCompressedLength, storedBlockBound) + wrapLength; if (maxCompressedLength > long.MaxValue) { 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 index 8c5442bf497f76..32aa377da137c6 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/GZipEncoder.cs @@ -78,10 +78,9 @@ private void EnsureNotDisposed() /// is negative. public static long GetMaxCompressedLength(long inputLength) { - // compressBound() returns the upper bound for zlib-wrapped deflate, which includes - // 6 bytes of zlib overhead (2-byte header + 4-byte Adler32 trailer). - // GZip format uses 18 bytes of overhead (10-byte header + 8-byte CRC32/size trailer), - // which is 12 bytes more than the zlib overhead already included in compressBound(). + // DeflateEncoder.GetMaxCompressedLength() returns the bound for zlib-wrapped deflate, + // where wrap length is at most 6 bytes. GZip format uses 18 bytes of overhead, which is + // 12 bytes more than the conservative zlib overhead already included in that bound. long maxCompressedLength = DeflateEncoder.GetMaxCompressedLength(inputLength); if (maxCompressedLength > long.MaxValue - 12) 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 index 0749efc0b55ffa..891d49510ae2e7 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZLibEncoder.cs @@ -78,9 +78,9 @@ private void EnsureNotDisposed() /// is negative. public static long GetMaxCompressedLength(long inputLength) { - // compressBound() returns the upper bound for zlib-wrapped deflate output, which already + // DeflateEncoder.GetMaxCompressedLength() returns a bound for deflate output that already // accounts for zlib's 2-byte header and 4-byte Adler32 trailer. ZLibEncoder produces - // zlib-format output, so this value is the exact upper bound with no additional overhead needed. + // zlib-format output, so this value is a valid upper bound with no additional overhead needed. return DeflateEncoder.GetMaxCompressedLength(inputLength); }